diff options
Diffstat (limited to 'client')
31 files changed, 1216 insertions, 60 deletions
diff --git a/client/src/main/java/com/vaadin/client/ui/VRichTextArea.java b/client/src/main/java/com/vaadin/client/ui/VRichTextArea.java new file mode 100644 index 0000000000..75bec94dca --- /dev/null +++ b/client/src/main/java/com/vaadin/client/ui/VRichTextArea.java @@ -0,0 +1,415 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * 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 com.vaadin.client.ui; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.Consumer; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.dom.client.BodyElement; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.IFrameElement; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwt.event.dom.client.KeyPressHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.Focusable; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.HasEnabled; +import com.google.gwt.user.client.ui.RichTextArea; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.BrowserInfo; +import com.vaadin.client.ConnectorMap; +import com.vaadin.client.ui.ShortcutActionHandler.ShortcutActionHandlerOwner; +import com.vaadin.client.ui.richtextarea.VRichTextToolbar; + +/** + * This class implements a basic client side rich text editor component. + * + * @author Vaadin Ltd. + * + */ +public class VRichTextArea extends Composite implements Field, KeyPressHandler, + KeyDownHandler, Focusable, HasEnabled { + + /** + * The input node CSS classname. + */ + public static final String CLASSNAME = "v-richtextarea"; + + /** For internal use only. May be removed or replaced in the future. */ + public String id; + + /** For internal use only. May be removed or replaced in the future. */ + public ApplicationConnection client; + + /** For internal use only. May be removed or replaced in the future. */ + public boolean immediate = false; + + /** For internal use only. May be removed or replaced in the future. */ + public RichTextArea rta; + + /** For internal use only. May be removed or replaced in the future. */ + public VRichTextToolbar formatter; + + /** For internal use only. May be removed or replaced in the future. */ + public HTML html = new HTML(); + + private final FlowPanel fp = new FlowPanel(); + + private boolean enabled = true; + + /** For internal use only. May be removed or replaced in the future. */ + public int maxLength = -1; + + private int toolbarNaturalWidth = 500; + + /** For internal use only. May be removed or replaced in the future. */ + public HandlerRegistration keyPressHandler; + + private ShortcutActionHandlerOwner hasShortcutActionHandler; + + private boolean readOnly = false; + + private final Map<BlurHandler, HandlerRegistration> blurHandlers = new HashMap<BlurHandler, HandlerRegistration>(); + + private List<Command> inputHandlers = new ArrayList<>(); + + public VRichTextArea() { + createRTAComponents(); + fp.add(formatter); + fp.add(rta); + + initWidget(fp); + setStyleName(CLASSNAME); + + TouchScrollDelegate.enableTouchScrolling(html, html.getElement()); + } + + private void createRTAComponents() { + rta = new RichTextArea(); + rta.setWidth("100%"); + rta.addKeyDownHandler(this); + rta.addInitializeHandler(e -> { + // Must wait until iframe is attached to be able to access body + BodyElement rtaBody = IFrameElement.as(rta.getElement()) + .getContentDocument().getBody(); + addInputListener(rtaBody, event -> { + inputHandlers.forEach(handler -> handler.execute()); + }); + }); + + formatter = new VRichTextToolbar(rta); + + // Add blur handlers + for (Entry<BlurHandler, HandlerRegistration> handler : blurHandlers + .entrySet()) { + + // Remove old registration + handler.getValue().removeHandler(); + + // Add blur handlers + addBlurHandler(handler.getKey()); + } + } + + private native void addInputListener(Element element, + Consumer<NativeEvent> listener) + /*-{ + element.addEventListener("input", $entry(function(event) { + listener.@java.util.function.Consumer::accept(Ljava/lang/Object;)(event); + })); + }-*/; + + public void setMaxLength(int maxLength) { + if (maxLength >= 0) { + if (this.maxLength == -1) { + keyPressHandler = rta.addKeyPressHandler(this); + } + this.maxLength = maxLength; + } else if (this.maxLength != -1) { + this.maxLength = -1; + keyPressHandler.removeHandler(); + } + + } + + @Override + public void setEnabled(boolean enabled) { + if (this.enabled != enabled) { + // rta.setEnabled(enabled); + swapEditableArea(); + this.enabled = enabled; + } + } + + @Override + public boolean isEnabled() { + return enabled; + } + + /** + * Swaps html to rta and visa versa. + */ + private void swapEditableArea() { + String value = getValue(); + if (html.isAttached()) { + fp.remove(html); + if (BrowserInfo.get().isWebkit()) { + fp.remove(formatter); + createRTAComponents(); // recreate new RTA to bypass #5379 + fp.add(formatter); + } + fp.add(rta); + } else { + fp.remove(rta); + fp.add(html); + } + setValue(value); + } + + /** For internal use only. May be removed or replaced in the future. */ + public void selectAll() { + /* + * There is a timing issue if trying to select all immediately on first + * render. Simple deferred command is not enough. Using Timer with + * moderated timeout. If this appears to fail on many (most likely slow) + * environments, consider increasing the timeout. + * + * FF seems to require the most time to stabilize its RTA. On Vaadin + * tiergarden test machines, 200ms was not enough always (about 50% + * success rate) - 300 ms was 100% successful. This however was not + * enough on a sluggish old non-virtualized XP test machine. A bullet + * proof solution would be nice, GWT 2.1 might however solve these. At + * least setFocus has a workaround for this kind of issue. + */ + new Timer() { + @Override + public void run() { + rta.getFormatter().selectAll(); + } + }.schedule(320); + } + + public void setReadOnly(boolean b) { + if (isReadOnly() != b) { + swapEditableArea(); + readOnly = b; + } + // reset visibility in case enabled state changed and the formatter was + // recreated + formatter.setVisible(!readOnly); + } + + private boolean isReadOnly() { + return readOnly; + } + + @Override + public void setHeight(String height) { + super.setHeight(height); + if (height == null || height.equals("")) { + rta.setHeight(""); + } + } + + @Override + public void setWidth(String width) { + if (width.equals("")) { + /* + * IE cannot calculate the width of the 100% iframe correctly if + * there is no width specified for the parent. In this case we would + * use the toolbar but IE cannot calculate the width of that one + * correctly either in all cases. So we end up using a default width + * for a RichTextArea with no width definition in all browsers (for + * compatibility). + */ + + super.setWidth(toolbarNaturalWidth + "px"); + } else { + super.setWidth(width); + } + } + + @Override + public void onKeyPress(KeyPressEvent event) { + if (maxLength >= 0) { + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + if (rta.getHTML().length() > maxLength) { + rta.setHTML(rta.getHTML().substring(0, maxLength)); + } + } + }); + } + } + + @Override + public void onKeyDown(KeyDownEvent event) { + // delegate to closest shortcut action handler + // throw event from the iframe forward to the shortcuthandler + ShortcutActionHandler shortcutHandler = getShortcutHandlerOwner() + .getShortcutActionHandler(); + if (shortcutHandler != null) { + shortcutHandler.handleKeyboardEvent( + com.google.gwt.user.client.Event.as(event.getNativeEvent()), + ConnectorMap.get(client).getConnector(this)); + } + } + + private ShortcutActionHandlerOwner getShortcutHandlerOwner() { + if (hasShortcutActionHandler == null) { + Widget parent = getParent(); + while (parent != null) { + if (parent instanceof ShortcutActionHandlerOwner) { + break; + } + parent = parent.getParent(); + } + hasShortcutActionHandler = (ShortcutActionHandlerOwner) parent; + } + return hasShortcutActionHandler; + } + + @Override + public int getTabIndex() { + return rta.getTabIndex(); + } + + @Override + public void setAccessKey(char key) { + rta.setAccessKey(key); + } + + @Override + public void setFocus(boolean focused) { + /* + * Similar issue as with selectAll. Focusing must happen before possible + * selectall, so keep the timeout here lower. + */ + new Timer() { + + @Override + public void run() { + rta.setFocus(true); + } + }.schedule(300); + } + + @Override + public void setTabIndex(int index) { + rta.setTabIndex(index); + } + + /** + * Sets the value of the text area. + * + * @param value + * The text value, as HTML + */ + public void setValue(String value) { + if (rta.isAttached()) { + rta.setHTML(value); + } else { + html.setHTML(value); + } + } + + /** + * Gets the value of the text area. + * + * @return the value as HTML + */ + public String getValue() { + if (rta.isAttached()) { + return rta.getHTML(); + } else { + return html.getHTML(); + } + } + + /** + * Browsers differ in what they return as the content of a visually empty + * rich text area. This method is used to normalize these to an empty + * string. See #8004. + * + * @return cleaned html string + */ + public String getSanitizedValue() { + BrowserInfo browser = BrowserInfo.get(); + String result = getValue(); + if (browser.isFirefox()) { + if ("<br>".equals(result)) { + result = ""; + } + } else if (browser.isWebkit() || browser.isEdge()) { + if ("<br>".equals(result) || "<div><br></div>".equals(result)) { + result = ""; + } + } else if (browser.isIE()) { + if ("<P> </P>".equals(result)) { + result = ""; + } + } else if (browser.isOpera()) { + if ("<br>".equals(result) || "<p><br></p>".equals(result)) { + result = ""; + } + } + return result; + } + + /** + * Adds a blur handler to the component. + * + * @param blurHandler + * the blur handler to add + */ + public void addBlurHandler(BlurHandler blurHandler) { + blurHandlers.put(blurHandler, rta.addBlurHandler(blurHandler)); + } + + /** + * Removes a blur handler. + * + * @param blurHandler + * the handler to remove + */ + public void removeBlurHandler(BlurHandler blurHandler) { + HandlerRegistration registration = blurHandlers.remove(blurHandler); + if (registration != null) { + registration.removeHandler(); + } + } + + public HandlerRegistration addInputHandler(Command inputHandler) { + inputHandlers.add(inputHandler); + return () -> inputHandlers.remove(inputHandler); + } +} diff --git a/client/src/main/java/com/vaadin/client/ui/richtextarea/RichTextAreaConnector.java b/client/src/main/java/com/vaadin/client/ui/richtextarea/RichTextAreaConnector.java new file mode 100644 index 0000000000..87c5b938e6 --- /dev/null +++ b/client/src/main/java/com/vaadin/client/ui/richtextarea/RichTextAreaConnector.java @@ -0,0 +1,139 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * 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 com.vaadin.client.ui.richtextarea; + +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.vaadin.client.annotations.OnStateChange; +import com.vaadin.client.ui.AbstractFieldConnector; +import com.vaadin.client.ui.ConnectorFocusAndBlurHandler; +import com.vaadin.client.ui.SimpleManagedLayout; +import com.vaadin.client.ui.VRichTextArea; +import com.vaadin.client.ui.textfield.ValueChangeHandler; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.Connect.LoadStyle; +import com.vaadin.shared.ui.richtextarea.RichTextAreaClientRpc; +import com.vaadin.shared.ui.richtextarea.RichTextAreaServerRpc; +import com.vaadin.shared.ui.richtextarea.RichTextAreaState; +import com.vaadin.ui.RichTextArea; + +/** + * Connector for RichTextArea. + */ +@Connect(value = RichTextArea.class, loadStyle = LoadStyle.EAGER) +public class RichTextAreaConnector extends AbstractFieldConnector + implements SimpleManagedLayout, ValueChangeHandler.Owner { + + private class RichTextAreaClientRpcImpl implements RichTextAreaClientRpc { + @Override + public void selectAll() { + getWidget().selectAll(); + } + } + + private ValueChangeHandler valueChangeHandler; + + @Override + protected void init() { + getWidget().addBlurHandler(new BlurHandler() { + @Override + public void onBlur(BlurEvent event) { + flush(); + } + }); + getWidget().addInputHandler(() -> { + valueChangeHandler.scheduleValueChange(); + }); + + registerRpc(RichTextAreaClientRpc.class, + new RichTextAreaClientRpcImpl()); + ConnectorFocusAndBlurHandler.addHandlers(this); + + valueChangeHandler = new ValueChangeHandler(this); + + getLayoutManager().registerDependency(this, + getWidget().formatter.getElement()); + } + + @OnStateChange("valueChangeMode") + private void updateValueChangeMode() { + valueChangeHandler.setValueChangeMode(getState().valueChangeMode); + } + + @OnStateChange("valueChangeTimeout") + private void updateValueChangeTimeout() { + valueChangeHandler.setValueChangeTimeout(getState().valueChangeTimeout); + } + + @OnStateChange("readOnly") + private void updateReadOnly() { + getWidget().setReadOnly(getState().readOnly); + } + + @Override + public void onUnregister() { + super.onUnregister(); + getLayoutManager().unregisterDependency(this, + getWidget().formatter.getElement()); + } + + @Override + public VRichTextArea getWidget() { + return (VRichTextArea) super.getWidget(); + } + + @Override + public void layout() { + if (!isUndefinedHeight()) { + int rootElementInnerHeight = getLayoutManager() + .getInnerHeight(getWidget().getElement()); + int formatterHeight = getLayoutManager() + .getOuterHeight(getWidget().formatter.getElement()); + int editorHeight = rootElementInnerHeight - formatterHeight; + if (editorHeight < 0) { + editorHeight = 0; + } + getWidget().rta.setHeight(editorHeight + "px"); + } + } + + @Override + public RichTextAreaState getState() { + return (RichTextAreaState) super.getState(); + } + + private boolean hasStateChanged(String widgetValue) { + return !widgetValue.equals(getState().value); + } + + @Override + public void sendValueChange() { + String widgetValue = getWidget().getSanitizedValue(); + if (!hasStateChanged(widgetValue)) { + return; + } + + getRpcProxy(RichTextAreaServerRpc.class).setText(widgetValue); + getState().value = widgetValue; + + } + + @Override + public void flush() { + super.flush(); + sendValueChange(); + } +} diff --git a/client/src/main/java/com/vaadin/client/ui/richtextarea/VRichTextToolbar.java b/client/src/main/java/com/vaadin/client/ui/richtextarea/VRichTextToolbar.java new file mode 100644 index 0000000000..10c1de75e6 --- /dev/null +++ b/client/src/main/java/com/vaadin/client/ui/richtextarea/VRichTextToolbar.java @@ -0,0 +1,476 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * 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. + */ +/* + * Copyright 2007 Google Inc. + * + * 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 com.vaadin.client.ui.richtextarea; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.event.dom.client.ChangeEvent; +import com.google.gwt.event.dom.client.ChangeHandler; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.KeyUpEvent; +import com.google.gwt.event.dom.client.KeyUpHandler; +import com.google.gwt.i18n.client.Constants; +import com.google.gwt.resources.client.ClientBundle; +import com.google.gwt.resources.client.ImageResource; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.Image; +import com.google.gwt.user.client.ui.ListBox; +import com.google.gwt.user.client.ui.PushButton; +import com.google.gwt.user.client.ui.RichTextArea; +import com.google.gwt.user.client.ui.ToggleButton; + +/** + * A modified version of sample toolbar for use with {@link RichTextArea}. It + * provides a simple UI for all rich text formatting, dynamically displayed only + * for the available functionality. + */ +public class VRichTextToolbar extends Composite { + + /** + * This {@link ClientBundle} is used for all the button icons. Using a + * bundle allows all of these images to be packed into a single image, which + * saves a lot of HTTP requests, drastically improving startup time. + */ + public interface Images extends ClientBundle { + + ImageResource bold(); + + ImageResource createLink(); + + ImageResource hr(); + + ImageResource indent(); + + ImageResource insertImage(); + + ImageResource italic(); + + ImageResource justifyCenter(); + + ImageResource justifyLeft(); + + ImageResource justifyRight(); + + ImageResource ol(); + + ImageResource outdent(); + + ImageResource removeFormat(); + + ImageResource removeLink(); + + ImageResource strikeThrough(); + + ImageResource subscript(); + + ImageResource superscript(); + + ImageResource ul(); + + ImageResource underline(); + } + + /** + * This {@link Constants} interface is used to make the toolbar's strings + * internationalizable. + */ + public interface Strings extends Constants { + + String black(); + + String blue(); + + String bold(); + + String color(); + + String createLink(); + + String font(); + + String green(); + + String hr(); + + String indent(); + + String insertImage(); + + String italic(); + + String justifyCenter(); + + String justifyLeft(); + + String justifyRight(); + + String large(); + + String medium(); + + String normal(); + + String ol(); + + String outdent(); + + String red(); + + String removeFormat(); + + String removeLink(); + + String size(); + + String small(); + + String strikeThrough(); + + String subscript(); + + String superscript(); + + String ul(); + + String underline(); + + String white(); + + String xlarge(); + + String xsmall(); + + String xxlarge(); + + String xxsmall(); + + String yellow(); + } + + /** + * We use an inner EventHandler class to avoid exposing event methods on the + * RichTextToolbar itself. + */ + private class EventHandler + implements ClickHandler, ChangeHandler, KeyUpHandler { + + @Override + @SuppressWarnings("deprecation") + public void onChange(ChangeEvent event) { + Object sender = event.getSource(); + if (sender == backColors) { + basic.setBackColor( + backColors.getValue(backColors.getSelectedIndex())); + backColors.setSelectedIndex(0); + } else if (sender == foreColors) { + basic.setForeColor( + foreColors.getValue(foreColors.getSelectedIndex())); + foreColors.setSelectedIndex(0); + } else if (sender == fonts) { + basic.setFontName(fonts.getValue(fonts.getSelectedIndex())); + fonts.setSelectedIndex(0); + } else if (sender == fontSizes) { + basic.setFontSize( + fontSizesConstants[fontSizes.getSelectedIndex() - 1]); + fontSizes.setSelectedIndex(0); + } + } + + @Override + @SuppressWarnings("deprecation") + public void onClick(ClickEvent event) { + Object sender = event.getSource(); + if (sender == bold) { + basic.toggleBold(); + } else if (sender == italic) { + basic.toggleItalic(); + } else if (sender == underline) { + basic.toggleUnderline(); + } else if (sender == subscript) { + basic.toggleSubscript(); + } else if (sender == superscript) { + basic.toggleSuperscript(); + } else if (sender == strikethrough) { + extended.toggleStrikethrough(); + } else if (sender == indent) { + extended.rightIndent(); + } else if (sender == outdent) { + extended.leftIndent(); + } else if (sender == justifyLeft) { + basic.setJustification(RichTextArea.Justification.LEFT); + } else if (sender == justifyCenter) { + basic.setJustification(RichTextArea.Justification.CENTER); + } else if (sender == justifyRight) { + basic.setJustification(RichTextArea.Justification.RIGHT); + } else if (sender == insertImage) { + final String url = Window.prompt("Enter an image URL:", + "http://"); + if (url != null) { + extended.insertImage(url); + } + } else if (sender == createLink) { + final String url = Window.prompt("Enter a link URL:", + "http://"); + if (url != null) { + extended.createLink(url); + } + } else if (sender == removeLink) { + extended.removeLink(); + } else if (sender == hr) { + extended.insertHorizontalRule(); + } else if (sender == ol) { + extended.insertOrderedList(); + } else if (sender == ul) { + extended.insertUnorderedList(); + } else if (sender == removeFormat) { + extended.removeFormat(); + } else if (sender == richText) { + // We use the RichTextArea's onKeyUp event to update the toolbar + // status. This will catch any cases where the user moves the + // cursur using the keyboard, or uses one of the browser's + // built-in keyboard shortcuts. + updateStatus(); + } + } + + @Override + public void onKeyUp(KeyUpEvent event) { + if (event.getSource() == richText) { + // We use the RichTextArea's onKeyUp event to update the toolbar + // status. This will catch any cases where the user moves the + // cursor using the keyboard, or uses one of the browser's + // built-in keyboard shortcuts. + updateStatus(); + } + } + } + + private static final RichTextArea.FontSize[] fontSizesConstants = new RichTextArea.FontSize[] { + RichTextArea.FontSize.XX_SMALL, RichTextArea.FontSize.X_SMALL, + RichTextArea.FontSize.SMALL, RichTextArea.FontSize.MEDIUM, + RichTextArea.FontSize.LARGE, RichTextArea.FontSize.X_LARGE, + RichTextArea.FontSize.XX_LARGE }; + + private final Images images = (Images) GWT.create(Images.class); + private final Strings strings = (Strings) GWT.create(Strings.class); + private final EventHandler handler = new EventHandler(); + + private final RichTextArea richText; + @SuppressWarnings("deprecation") + private final RichTextArea.BasicFormatter basic; + @SuppressWarnings("deprecation") + private final RichTextArea.ExtendedFormatter extended; + + private final FlowPanel outer = new FlowPanel(); + private final FlowPanel topPanel = new FlowPanel(); + private final FlowPanel bottomPanel = new FlowPanel(); + private ToggleButton bold; + private ToggleButton italic; + private ToggleButton underline; + private ToggleButton subscript; + private ToggleButton superscript; + private ToggleButton strikethrough; + private PushButton indent; + private PushButton outdent; + private PushButton justifyLeft; + private PushButton justifyCenter; + private PushButton justifyRight; + private PushButton hr; + private PushButton ol; + private PushButton ul; + private PushButton insertImage; + private PushButton createLink; + private PushButton removeLink; + private PushButton removeFormat; + + private ListBox backColors; + private ListBox foreColors; + private ListBox fonts; + private ListBox fontSizes; + + /** + * Creates a new toolbar that drives the given rich text area. + * + * @param richText + * the rich text area to be controlled + */ + @SuppressWarnings("deprecation") + public VRichTextToolbar(RichTextArea richText) { + this.richText = richText; + basic = richText.getBasicFormatter(); + extended = richText.getExtendedFormatter(); + + outer.add(topPanel); + outer.add(bottomPanel); + topPanel.setStyleName("gwt-RichTextToolbar-top"); + bottomPanel.setStyleName("gwt-RichTextToolbar-bottom"); + + initWidget(outer); + setStyleName("gwt-RichTextToolbar"); + + if (basic != null) { + topPanel.add( + bold = createToggleButton(images.bold(), strings.bold())); + topPanel.add(italic = createToggleButton(images.italic(), + strings.italic())); + topPanel.add(underline = createToggleButton(images.underline(), + strings.underline())); + topPanel.add(subscript = createToggleButton(images.subscript(), + strings.subscript())); + topPanel.add(superscript = createToggleButton(images.superscript(), + strings.superscript())); + topPanel.add(justifyLeft = createPushButton(images.justifyLeft(), + strings.justifyLeft())); + topPanel.add(justifyCenter = createPushButton( + images.justifyCenter(), strings.justifyCenter())); + topPanel.add(justifyRight = createPushButton(images.justifyRight(), + strings.justifyRight())); + } + + if (extended != null) { + topPanel.add(strikethrough = createToggleButton( + images.strikeThrough(), strings.strikeThrough())); + topPanel.add(indent = createPushButton(images.indent(), + strings.indent())); + topPanel.add(outdent = createPushButton(images.outdent(), + strings.outdent())); + topPanel.add(hr = createPushButton(images.hr(), strings.hr())); + topPanel.add(ol = createPushButton(images.ol(), strings.ol())); + topPanel.add(ul = createPushButton(images.ul(), strings.ul())); + topPanel.add(insertImage = createPushButton(images.insertImage(), + strings.insertImage())); + topPanel.add(createLink = createPushButton(images.createLink(), + strings.createLink())); + topPanel.add(removeLink = createPushButton(images.removeLink(), + strings.removeLink())); + topPanel.add(removeFormat = createPushButton(images.removeFormat(), + strings.removeFormat())); + } + + if (basic != null) { + bottomPanel.add(backColors = createColorList("Background")); + bottomPanel.add(foreColors = createColorList("Foreground")); + bottomPanel.add(fonts = createFontList()); + bottomPanel.add(fontSizes = createFontSizes()); + + // We only use these handlers for updating status, so don't hook + // them up unless at least basic editing is supported. + richText.addKeyUpHandler(handler); + richText.addClickHandler(handler); + } + } + + private ListBox createColorList(String caption) { + final ListBox lb = new ListBox(); + lb.addChangeHandler(handler); + lb.setVisibleItemCount(1); + + lb.addItem(caption); + lb.addItem(strings.white(), "white"); + lb.addItem(strings.black(), "black"); + lb.addItem(strings.red(), "red"); + lb.addItem(strings.green(), "green"); + lb.addItem(strings.yellow(), "yellow"); + lb.addItem(strings.blue(), "blue"); + lb.setTabIndex(-1); + return lb; + } + + private ListBox createFontList() { + final ListBox lb = new ListBox(); + lb.addChangeHandler(handler); + lb.setVisibleItemCount(1); + + lb.addItem(strings.font(), ""); + lb.addItem(strings.normal(), "inherit"); + lb.addItem("Times New Roman", "Times New Roman"); + lb.addItem("Arial", "Arial"); + lb.addItem("Courier New", "Courier New"); + lb.addItem("Georgia", "Georgia"); + lb.addItem("Trebuchet", "Trebuchet"); + lb.addItem("Verdana", "Verdana"); + lb.setTabIndex(-1); + return lb; + } + + private ListBox createFontSizes() { + final ListBox lb = new ListBox(); + lb.addChangeHandler(handler); + lb.setVisibleItemCount(1); + + lb.addItem(strings.size()); + lb.addItem(strings.xxsmall()); + lb.addItem(strings.xsmall()); + lb.addItem(strings.small()); + lb.addItem(strings.medium()); + lb.addItem(strings.large()); + lb.addItem(strings.xlarge()); + lb.addItem(strings.xxlarge()); + lb.setTabIndex(-1); + return lb; + } + + private PushButton createPushButton(ImageResource img, String tip) { + final PushButton pb = new PushButton(new Image(img)); + pb.addClickHandler(handler); + pb.setTitle(tip); + pb.setTabIndex(-1); + return pb; + } + + private ToggleButton createToggleButton(ImageResource img, String tip) { + final ToggleButton tb = new ToggleButton(new Image(img)); + tb.addClickHandler(handler); + tb.setTitle(tip); + tb.setTabIndex(-1); + return tb; + } + + /** + * Updates the status of all the stateful buttons. + */ + @SuppressWarnings("deprecation") + private void updateStatus() { + if (basic != null) { + bold.setDown(basic.isBold()); + italic.setDown(basic.isItalic()); + underline.setDown(basic.isUnderlined()); + subscript.setDown(basic.isSubscript()); + superscript.setDown(basic.isSuperscript()); + } + + if (extended != null) { + strikethrough.setDown(extended.isStrikethrough()); + } + } +} diff --git a/client/src/main/java/com/vaadin/client/ui/textarea/TextAreaConnector.java b/client/src/main/java/com/vaadin/client/ui/textarea/TextAreaConnector.java index 1f98b04310..79f529ae5e 100644 --- a/client/src/main/java/com/vaadin/client/ui/textarea/TextAreaConnector.java +++ b/client/src/main/java/com/vaadin/client/ui/textarea/TextAreaConnector.java @@ -25,7 +25,6 @@ import com.vaadin.client.ui.textfield.AbstractTextFieldConnector; import com.vaadin.shared.ui.Connect; import com.vaadin.shared.ui.textarea.TextAreaServerRpc; import com.vaadin.shared.ui.textarea.TextAreaState; -import com.vaadin.shared.ui.textfield.ValueChangeMode; import com.vaadin.ui.TextArea; @Connect(TextArea.class) @@ -46,10 +45,9 @@ public class TextAreaConnector extends AbstractTextFieldConnector { super.init(); getWidget().addChangeHandler(event -> sendValueChange()); getWidget().addDomHandler(event -> { - if (getState().valueChangeMode != ValueChangeMode.BLUR) { - scheduleValueChange(); - } + getValueChangeHandler().scheduleValueChange(); }, InputEvent.getType()); + getWidget().addMouseUpHandler(new ResizeMouseUpHandler()); } diff --git a/client/src/main/java/com/vaadin/client/ui/textfield/AbstractTextFieldConnector.java b/client/src/main/java/com/vaadin/client/ui/textfield/AbstractTextFieldConnector.java index bc8c82f549..d774ba2262 100644 --- a/client/src/main/java/com/vaadin/client/ui/textfield/AbstractTextFieldConnector.java +++ b/client/src/main/java/com/vaadin/client/ui/textfield/AbstractTextFieldConnector.java @@ -15,8 +15,6 @@ */ package com.vaadin.client.ui.textfield; -import com.google.gwt.core.client.Scheduler; -import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.annotations.OnStateChange; import com.vaadin.client.ui.AbstractComponentConnector; @@ -25,14 +23,13 @@ import com.vaadin.client.ui.ConnectorFocusAndBlurHandler; import com.vaadin.shared.ui.textfield.AbstractTextFieldClientRpc; import com.vaadin.shared.ui.textfield.AbstractTextFieldServerRpc; import com.vaadin.shared.ui.textfield.AbstractTextFieldState; -import com.vaadin.shared.ui.textfield.ValueChangeMode; import com.vaadin.ui.AbstractTextField; /** * Connector class for AbstractTextField. */ public abstract class AbstractTextFieldConnector - extends AbstractComponentConnector { + extends AbstractComponentConnector implements ValueChangeHandler.Owner { private class AbstractTextFieldClientRpcImpl implements AbstractTextFieldClientRpc { @@ -62,19 +59,18 @@ public abstract class AbstractTextFieldConnector } private int lastSentCursorPosition = -1; - - private Timer valueChangeTrigger = new Timer() { - @Override - public void run() { - Scheduler.get().scheduleDeferred(() -> sendValueChange()); - } - }; + private ValueChangeHandler valueChangeHandler; @Override protected void init() { registerRpc(AbstractTextFieldClientRpc.class, new AbstractTextFieldClientRpcImpl()); ConnectorFocusAndBlurHandler.addHandlers(this); + valueChangeHandler = new ValueChangeHandler(this); + } + + protected ValueChangeHandler getValueChangeHandler() { + return valueChangeHandler; } /** @@ -88,50 +84,19 @@ public abstract class AbstractTextFieldConnector return (AbstractTextFieldWidget) getWidget(); } - /** - * Called whenever a change in the value has been detected. Schedules a - * value change to be sent to the server, depending on the current value - * change mode. - * <p> - * Note that this method does not consider the {@link ValueChangeMode#BLUR} - * mode but assumes that {@link #sendValueChange()} is called directly for - * this mode. - */ - protected void scheduleValueChange() { - switch (getState().valueChangeMode) { - case LAZY: - lazyTextChange(); - break; - case TIMEOUT: - timeoutTextChange(); - break; - case EAGER: - eagerTextChange(); - break; - case BLUR: - // Nothing to schedule for this mode - break; - } - } - - private void lazyTextChange() { - valueChangeTrigger.schedule(getState().valueChangeTimeout); - } - - private void timeoutTextChange() { - if (valueChangeTrigger.isRunning()) { - return; - } - valueChangeTrigger.schedule(getState().valueChangeTimeout); + @Override + public AbstractTextFieldState getState() { + return (AbstractTextFieldState) super.getState(); } - private void eagerTextChange() { - valueChangeTrigger.run(); + @OnStateChange("valueChangeMode") + private void updateValueChangeMode() { + valueChangeHandler.setValueChangeMode(getState().valueChangeMode); } - @Override - public AbstractTextFieldState getState() { - return (AbstractTextFieldState) super.getState(); + @OnStateChange("valueChangeTimeout") + private void updateValueChangeTimeout() { + valueChangeHandler.setValueChangeTimeout(getState().valueChangeTimeout); } @OnStateChange("readOnly") @@ -151,10 +116,12 @@ public abstract class AbstractTextFieldConnector * Sends the updated value and cursor position to the server, if either one * has changed. */ - protected void sendValueChange() { + @Override + public void sendValueChange() { if (!hasStateChanged()) { return; } + lastSentCursorPosition = getAbstractTextField().getCursorPos(); getRpcProxy(AbstractTextFieldServerRpc.class).setText( getAbstractTextField().getValue(), lastSentCursorPosition); diff --git a/client/src/main/java/com/vaadin/client/ui/textfield/TextFieldConnector.java b/client/src/main/java/com/vaadin/client/ui/textfield/TextFieldConnector.java index 0a937de349..84e8e7401b 100644 --- a/client/src/main/java/com/vaadin/client/ui/textfield/TextFieldConnector.java +++ b/client/src/main/java/com/vaadin/client/ui/textfield/TextFieldConnector.java @@ -20,7 +20,6 @@ import com.vaadin.client.ui.VTextField; import com.vaadin.shared.ui.Connect; import com.vaadin.shared.ui.Connect.LoadStyle; import com.vaadin.shared.ui.textfield.TextFieldState; -import com.vaadin.shared.ui.textfield.ValueChangeMode; import com.vaadin.ui.TextField; /** @@ -34,9 +33,7 @@ public class TextFieldConnector extends AbstractTextFieldConnector { super.init(); getWidget().addChangeHandler(event -> sendValueChange()); getWidget().addDomHandler(event -> { - if (getState().valueChangeMode != ValueChangeMode.BLUR) { - scheduleValueChange(); - } + getValueChangeHandler().scheduleValueChange(); }, InputEvent.getType()); } diff --git a/client/src/main/java/com/vaadin/client/ui/textfield/ValueChangeHandler.java b/client/src/main/java/com/vaadin/client/ui/textfield/ValueChangeHandler.java new file mode 100644 index 0000000000..89cca2394f --- /dev/null +++ b/client/src/main/java/com/vaadin/client/ui/textfield/ValueChangeHandler.java @@ -0,0 +1,129 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * 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 com.vaadin.client.ui.textfield; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.user.client.Timer; +import com.vaadin.client.ComponentConnector; +import com.vaadin.shared.ui.ValueChangeMode; + +/** + * Helper for dealing with scheduling value change events based on a given mode + * and possibly timeout. + */ +public class ValueChangeHandler { + + /** + * Must be implemented by any user of a ValueChangeHandler. + */ + public interface Owner extends ComponentConnector { + /** + * Sends the current value to the server, if it has changed. + */ + void sendValueChange(); + } + + private Owner owner; + + private Timer valueChangeTrigger = new Timer() { + @Override + public void run() { + Scheduler.get().scheduleDeferred(() -> owner.sendValueChange()); + } + }; + + private int valueChangeTimeout = -1; + + private ValueChangeMode valueChangeMode; + + /** + * Creates a value change handler for the given owner. + * + * @param owner + * the owner connector + */ + public ValueChangeHandler(Owner owner) { + this.owner = owner; + } + + /** + * Called whenever a change in the value has been detected. Schedules a + * value change to be sent to the server, depending on the current value + * change mode. + * <p> + * Note that this method does not consider the {@link ValueChangeMode#BLUR} + * mode but assumes that {@link #sendValueChange()} is called directly for + * this mode. + */ + public void scheduleValueChange() { + switch (valueChangeMode) { + case LAZY: + lazyTextChange(); + break; + case TIMEOUT: + timeoutTextChange(); + break; + case EAGER: + eagerTextChange(); + break; + case BLUR: + // Nothing to schedule for this mode + break; + default: + throw new IllegalStateException("Unknown mode: " + valueChangeMode); + } + } + + private void lazyTextChange() { + valueChangeTrigger.schedule(valueChangeTimeout); + } + + private void timeoutTextChange() { + if (valueChangeTrigger.isRunning()) { + return; + } + valueChangeTrigger.schedule(valueChangeTimeout); + } + + private void eagerTextChange() { + valueChangeTrigger.run(); + } + + /** + * Sets the value change mode to use. + * + * @see ValueChangeMode + * + * @param valueChangeMode + * the value change mode to use + */ + public void setValueChangeMode(ValueChangeMode valueChangeMode) { + this.valueChangeMode = valueChangeMode; + } + + /** + * Sets the value change timeout to use. + * + * @see ValueChangeMode + * + * @param valueChangeTimeout + * the value change timeout + */ + public void setValueChangeTimeout(int valueChangeTimeout) { + this.valueChangeTimeout = valueChangeTimeout; + } + +} diff --git a/client/src/main/resources/com/vaadin/client/ui/richtextarea/VRichTextToolbar$Strings.properties b/client/src/main/resources/com/vaadin/client/ui/richtextarea/VRichTextToolbar$Strings.properties new file mode 100644 index 0000000000..363b704584 --- /dev/null +++ b/client/src/main/resources/com/vaadin/client/ui/richtextarea/VRichTextToolbar$Strings.properties @@ -0,0 +1,35 @@ +bold = Toggle Bold +createLink = Create Link +hr = Insert Horizontal Rule +indent = Indent Right +insertImage = Insert Image +italic = Toggle Italic +justifyCenter = Center +justifyLeft = Left Justify +justifyRight = Right Justify +ol = Insert Ordered List +outdent = Indent Left +removeFormat = Remove Formatting +removeLink = Remove Link +strikeThrough = Toggle Strikethrough +subscript = Toggle Subscript +superscript = Toggle Superscript +ul = Insert Unordered List +underline = Toggle Underline +color = Color +black = Black +white = White +red = Red +green = Green +yellow = Yellow +blue = Blue +font = Font +normal = Normal +size = Size +xxsmall = XX-Small +xsmall = X-Small +small = Small +medium = Medium +large = Large +xlarge = X-Large +xxlarge = XX-Large
\ No newline at end of file diff --git a/client/src/main/resources/com/vaadin/client/ui/richtextarea/backColors.gif b/client/src/main/resources/com/vaadin/client/ui/richtextarea/backColors.gif Binary files differnew file mode 100644 index 0000000000..ddfc1cea2c --- /dev/null +++ b/client/src/main/resources/com/vaadin/client/ui/richtextarea/backColors.gif diff --git a/client/src/main/resources/com/vaadin/client/ui/richtextarea/bold.gif b/client/src/main/resources/com/vaadin/client/ui/richtextarea/bold.gif Binary files differnew file mode 100644 index 0000000000..7c22eaac68 --- /dev/null +++ b/client/src/main/resources/com/vaadin/client/ui/richtextarea/bold.gif diff --git a/client/src/main/resources/com/vaadin/client/ui/richtextarea/createLink.gif b/client/src/main/resources/com/vaadin/client/ui/richtextarea/createLink.gif Binary files differnew file mode 100644 index 0000000000..1a1412fe0e --- /dev/null +++ b/client/src/main/resources/com/vaadin/client/ui/richtextarea/createLink.gif diff --git a/client/src/main/resources/com/vaadin/client/ui/richtextarea/fontSizes.gif b/client/src/main/resources/com/vaadin/client/ui/richtextarea/fontSizes.gif Binary files differnew file mode 100644 index 0000000000..c2f4c8cb21 --- /dev/null +++ b/client/src/main/resources/com/vaadin/client/ui/richtextarea/fontSizes.gif diff --git a/client/src/main/resources/com/vaadin/client/ui/richtextarea/fonts.gif b/client/src/main/resources/com/vaadin/client/ui/richtextarea/fonts.gif Binary files differnew file mode 100644 index 0000000000..1629cabb78 --- /dev/null +++ b/client/src/main/resources/com/vaadin/client/ui/richtextarea/fonts.gif diff --git a/client/src/main/resources/com/vaadin/client/ui/richtextarea/foreColors.gif b/client/src/main/resources/com/vaadin/client/ui/richtextarea/foreColors.gif Binary files differnew file mode 100644 index 0000000000..2bb89ef189 --- /dev/null +++ b/client/src/main/resources/com/vaadin/client/ui/richtextarea/foreColors.gif diff --git a/client/src/main/resources/com/vaadin/client/ui/richtextarea/gwtLogo.png b/client/src/main/resources/com/vaadin/client/ui/richtextarea/gwtLogo.png Binary files differnew file mode 100644 index 0000000000..80728186d8 --- /dev/null +++ b/client/src/main/resources/com/vaadin/client/ui/richtextarea/gwtLogo.png diff --git a/client/src/main/resources/com/vaadin/client/ui/richtextarea/hr.gif b/client/src/main/resources/com/vaadin/client/ui/richtextarea/hr.gif Binary files differnew file mode 100644 index 0000000000..d507082cf1 --- /dev/null +++ b/client/src/main/resources/com/vaadin/client/ui/richtextarea/hr.gif diff --git a/client/src/main/resources/com/vaadin/client/ui/richtextarea/indent.gif b/client/src/main/resources/com/vaadin/client/ui/richtextarea/indent.gif Binary files differnew file mode 100644 index 0000000000..905421ed76 --- /dev/null +++ b/client/src/main/resources/com/vaadin/client/ui/richtextarea/indent.gif diff --git a/client/src/main/resources/com/vaadin/client/ui/richtextarea/insertImage.gif b/client/src/main/resources/com/vaadin/client/ui/richtextarea/insertImage.gif Binary files differnew file mode 100644 index 0000000000..394ec432a5 --- /dev/null +++ b/client/src/main/resources/com/vaadin/client/ui/richtextarea/insertImage.gif diff --git a/client/src/main/resources/com/vaadin/client/ui/richtextarea/italic.gif b/client/src/main/resources/com/vaadin/client/ui/richtextarea/italic.gif Binary files differnew file mode 100644 index 0000000000..ffe0e97284 --- /dev/null +++ b/client/src/main/resources/com/vaadin/client/ui/richtextarea/italic.gif diff --git a/client/src/main/resources/com/vaadin/client/ui/richtextarea/justifyCenter.gif b/client/src/main/resources/com/vaadin/client/ui/richtextarea/justifyCenter.gif Binary files differnew file mode 100644 index 0000000000..f7d4c4693d --- /dev/null +++ b/client/src/main/resources/com/vaadin/client/ui/richtextarea/justifyCenter.gif diff --git a/client/src/main/resources/com/vaadin/client/ui/richtextarea/justifyLeft.gif b/client/src/main/resources/com/vaadin/client/ui/richtextarea/justifyLeft.gif Binary files differnew file mode 100644 index 0000000000..bc37a3ed5a --- /dev/null +++ b/client/src/main/resources/com/vaadin/client/ui/richtextarea/justifyLeft.gif diff --git a/client/src/main/resources/com/vaadin/client/ui/richtextarea/justifyRight.gif b/client/src/main/resources/com/vaadin/client/ui/richtextarea/justifyRight.gif Binary files differnew file mode 100644 index 0000000000..892d569384 --- /dev/null +++ b/client/src/main/resources/com/vaadin/client/ui/richtextarea/justifyRight.gif diff --git a/client/src/main/resources/com/vaadin/client/ui/richtextarea/ol.gif b/client/src/main/resources/com/vaadin/client/ui/richtextarea/ol.gif Binary files differnew file mode 100644 index 0000000000..54f8e4f551 --- /dev/null +++ b/client/src/main/resources/com/vaadin/client/ui/richtextarea/ol.gif diff --git a/client/src/main/resources/com/vaadin/client/ui/richtextarea/outdent.gif b/client/src/main/resources/com/vaadin/client/ui/richtextarea/outdent.gif Binary files differnew file mode 100644 index 0000000000..78fd1b5722 --- /dev/null +++ b/client/src/main/resources/com/vaadin/client/ui/richtextarea/outdent.gif diff --git a/client/src/main/resources/com/vaadin/client/ui/richtextarea/removeFormat.gif b/client/src/main/resources/com/vaadin/client/ui/richtextarea/removeFormat.gif Binary files differnew file mode 100644 index 0000000000..cf92c9774f --- /dev/null +++ b/client/src/main/resources/com/vaadin/client/ui/richtextarea/removeFormat.gif diff --git a/client/src/main/resources/com/vaadin/client/ui/richtextarea/removeLink.gif b/client/src/main/resources/com/vaadin/client/ui/richtextarea/removeLink.gif Binary files differnew file mode 100644 index 0000000000..40721a7bca --- /dev/null +++ b/client/src/main/resources/com/vaadin/client/ui/richtextarea/removeLink.gif diff --git a/client/src/main/resources/com/vaadin/client/ui/richtextarea/strikeThrough.gif b/client/src/main/resources/com/vaadin/client/ui/richtextarea/strikeThrough.gif Binary files differnew file mode 100644 index 0000000000..a7a233c023 --- /dev/null +++ b/client/src/main/resources/com/vaadin/client/ui/richtextarea/strikeThrough.gif diff --git a/client/src/main/resources/com/vaadin/client/ui/richtextarea/subscript.gif b/client/src/main/resources/com/vaadin/client/ui/richtextarea/subscript.gif Binary files differnew file mode 100644 index 0000000000..58b6fbb816 --- /dev/null +++ b/client/src/main/resources/com/vaadin/client/ui/richtextarea/subscript.gif diff --git a/client/src/main/resources/com/vaadin/client/ui/richtextarea/superscript.gif b/client/src/main/resources/com/vaadin/client/ui/richtextarea/superscript.gif Binary files differnew file mode 100644 index 0000000000..a6270f6e21 --- /dev/null +++ b/client/src/main/resources/com/vaadin/client/ui/richtextarea/superscript.gif diff --git a/client/src/main/resources/com/vaadin/client/ui/richtextarea/ul.gif b/client/src/main/resources/com/vaadin/client/ui/richtextarea/ul.gif Binary files differnew file mode 100644 index 0000000000..83f1562bcb --- /dev/null +++ b/client/src/main/resources/com/vaadin/client/ui/richtextarea/ul.gif diff --git a/client/src/main/resources/com/vaadin/client/ui/richtextarea/underline.gif b/client/src/main/resources/com/vaadin/client/ui/richtextarea/underline.gif Binary files differnew file mode 100644 index 0000000000..06f0200fdd --- /dev/null +++ b/client/src/main/resources/com/vaadin/client/ui/richtextarea/underline.gif |