package org.geogebra.web.html5.gui.inputfield; import java.util.ArrayList; import org.geogebra.common.gui.inputfield.DynamicTextElement; import org.geogebra.common.gui.inputfield.DynamicTextElement.DynamicTextType; import org.geogebra.common.kernel.StringTemplate; import org.geogebra.common.kernel.geos.GeoElement; import org.geogebra.common.main.App; import org.geogebra.common.main.GWTKeycodes; import org.geogebra.common.util.StringUtil; import org.geogebra.common.util.debug.Log; import org.geogebra.web.html5.awt.GFontW; import org.geogebra.web.html5.main.AppW; import com.google.gwt.canvas.client.Canvas; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.dom.client.BodyElement; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.IFrameElement; import com.google.gwt.dom.client.Node; import com.google.gwt.dom.client.Style.BorderStyle; import com.google.gwt.dom.client.Style.Cursor; import com.google.gwt.dom.client.Style.Unit; 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.event.logical.shared.InitializeEvent; import com.google.gwt.event.logical.shared.InitializeHandler; import com.google.gwt.event.logical.shared.ValueChangeEvent; import com.google.gwt.event.logical.shared.ValueChangeHandler; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.ui.PopupPanel; import com.google.gwt.user.client.ui.RichTextArea; /** * Extension of RichTextArea for editing GeoText strings with dynamic references * to GeoElements. * * @author G. Sturr * */ public class GeoTextEditor extends RichTextArea { private static final String DYNAMIC_TEXT_CLASS = "dynamicText"; private AppW app; boolean initialized = false; protected ArrayList<DynamicTextElement> dynamicList = null; protected GFontW font; protected ITextEditPanel editPanel; protected Formatter formatter; protected PopupPanel textEditPopup; protected EditorTextField editBox; /************************************** * Constructor * * @param app * @param editPanel */ public GeoTextEditor(App app, ITextEditPanel editPanel) { this.app = (AppW) app; this.editPanel = editPanel; formatter = getFormatter(); // styles and handlers must be set after the editor has been initialized addInitializeHandler(new InitializeHandler() { @Override public void onInitialize(InitializeEvent event) { initialized = true; updateFonts(); // set style properties that cannot be done from stylesheet getBody().setAttribute("spellcheck", "false"); getBody().setAttribute("oncontextmenu", "return false"); getBody().setAttribute("word-wrap", "normal"); addCutHandler(getBody()); addPasteHandler(getBody()); if (dynamicList != null) { setDynamicText(); } } }); createEditPopup(); registerHandlers(); } private void registerHandlers() { addKeyUpHandler(new KeyUpHandler() { @Override public void onKeyUp(KeyUpEvent event) { editPanel.updatePreviewPanel(); } }); editBox.addKeyUpHandler(new KeyUpHandler() { @Override public void onKeyUp(KeyUpEvent event) { int keyCode = event.getNativeKeyCode(); switch (keyCode) { case GWTKeycodes.KEY_ESCAPE: showEditPopup(false); break; case GWTKeycodes.KEY_ENTER: showEditPopup(false); break; default: editPanel.updatePreviewPanel(); } } }); addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { showEditPopup(false); Element target = Element.as(event.getNativeEvent() .getEventTarget()); if (DYNAMIC_TEXT_CLASS .equalsIgnoreCase(target.getClassName())) { editBox.setText(target.getAttribute("value")); editBox.setTarget(target); showEditPopup(true); } } }); } public void updateFonts() { if (!initialized) { return; } font = (GFontW) app.getPlainFontCommon(); String fontSize = app.getFontSize() + ""; String fontFamily = font.getFontFamily(); // note: formatter cannot be used here because pixel font-size is not // supported getBody().setAttribute("style", "font-family:" + fontFamily + "; font-size:" + fontSize + "px"); } private Document getDocument() { if (!initialized) { return null; } if (IFrameElement.as(getElement()) == null) { return null; } return IFrameElement.as(getElement()).getContentDocument(); } protected BodyElement getBody() { if (getDocument() == null) { return null; } return getDocument().getBody(); } // workaround for ff bug that prevents disabling RichTextEditor @Override public void onBrowserEvent(final Event event) { if (isEnabled()) { super.onBrowserEvent(event); } } public native void addCutHandler(Element elem) /*-{ var temp = this; elem.oncut = function(e) { temp.@org.geogebra.web.html5.gui.inputfield.GeoTextEditor::handleCut()(); } }-*/; public native void addPasteHandler(Element elem) /*-{ var temp = this; elem.onpaste = function(e) { temp.@org.geogebra.web.html5.gui.inputfield.GeoTextEditor::handlePaste()(); } }-*/; /** * * @return */ public String getUnformattedContent() { return getUnformattedContent(getBody()); } private String getUnformattedContent(Node e) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < e.getChildCount(); i++) { Node c = e.getChild(i); if (c.getChildCount() > 0) { sb.append(getUnformattedContent(c)); } else { if (c instanceof Element) { Element el = (Element) c; if (((Element) c).getClassName() .contains(GeoTextEditor.DYNAMIC_TEXT_CLASS)) { sb.append(el.getString()); continue; } } String nodeValue = c.getNodeValue(); if (nodeValue != null) { if (c.getNodeType() == Node.TEXT_NODE) { sb.append(nodeValue); } else { sb.append("<div>"); sb.append(nodeValue); sb.append("</div>"); } } } } return sb.toString(); } public void handlePaste() { // setDynamicText(); Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { getBody().setInnerHTML(getUnformattedContent()); updateFonts(); } }); editPanel.updatePreviewPanel(); Log.debug("Paste! "); } public void handleCut() { editPanel.updatePreviewPanel(); // Log.debug("Cut!"); } /** * Inserts an HTML element at the current cursor position and updates the * editor. * * Note: The insertHTML method is not supported in IE11 so it can't be used * here. Instead, insertImage (browser safe) is used to insert a dummy image * element which is then replaced with the actual element to be inserted. * * @param elem */ public void insertElement(Element elem) { String dummyURL = dummyImageURL(); formatter.insertImage(dummyURL); Node node = findImageNodeRecursive(getBody(), dummyURL); if (node != null) { node.getParentElement().replaceChild(elem, node); editPanel.updatePreviewPanel(); } } private Node findImageNodeRecursive(Node node, String dummyURL) { for (int i = 0; i < node.getChildCount(); i++) { Node child = node.getChild(i); if (child.getNodeType() == Node.ELEMENT_NODE) { // image node? if (((Element) child).getPropertyString("src").equals(dummyURL)) { return child; } else if (node.hasChildNodes()) { // recursive search of child nodes Node targetNode = findImageNodeRecursive(child, dummyURL); if (targetNode != null) { return targetNode; } } } } return null; } private static String dummyImageURL() { Canvas canvas = Canvas.createIfSupported(); canvas.setWidth("1px"); canvas.setHeight("1px"); canvas.setCoordinateSpaceWidth(1); canvas.setCoordinateSpaceHeight(1); return canvas.toDataUrl(); } public Element createValueElement(String value) { Element elem = getDocument().createElement("input"); elem.setClassName(DYNAMIC_TEXT_CLASS); elem.setPropertyString("type", "button"); elem.setPropertyString("value", value); // set style // TODO: get this to work from the css file elem.getStyle().clearBackgroundImage(); elem.getStyle().clearBackgroundColor(); elem.getStyle().clearTextDecoration(); elem.getStyle().clearOpacity(); elem.getStyle().setBorderStyle(BorderStyle.SOLID); elem.getStyle().setBorderWidth(2, Unit.PX); elem.getStyle().setBorderColor("lightgray"); elem.getStyle().setCursor(Cursor.POINTER); elem.getStyle().setFontSize(font.getSize(), Unit.PX); elem.getStyle().setBackgroundColor("wheat"); elem.getStyle().setMarginLeft(1, Unit.PX); elem.getStyle().setMarginRight(1, Unit.PX); return elem; } public Element createTextElement(String text) { Element elem = getDocument().createTextNode(text).cast(); return elem; } public void insertGeoElement(GeoElement geo) { String text = ""; // gives empty box if geo is null if (geo != null) { text = geo.getLabel(StringTemplate.defaultTemplate); } insertElement(createValueElement(text)); } public void insertTextString(String str0, boolean isLatex) { boolean convertGreekLetters = !app.getLocalization().getLanguage() .equals("gr"); if (str0 != null) { String str = str0; if (isLatex) { str = StringUtil.toLaTeXString(str, convertGreekLetters); } insertElement(createTextElement(str)); } } protected void setDynamicText() { setHTML(""); Element lineElement = getBody(); for (DynamicTextElement dt : dynamicList) { if (dt.type == DynamicTextType.STATIC) { String[] lineSplit = dt.text.split("\n"); lineElement.appendChild(createTextElement(lineSplit[0])); for (int i = 1; i < lineSplit.length; i++) { lineElement = DOM.createDiv(); getBody().appendChild(lineElement); lineElement.appendChild(createTextElement(lineSplit[i])); } } else { lineElement.appendChild(createValueElement(dt.text)); } } } public void setText(ArrayList<DynamicTextElement> list) { dynamicList = list; if (initialized) { setDynamicText(); } } /** * Parses the RichTextArea HTML to create a list of DynamicTextElements * * @return list of DynamicTextElements represented by current editor text */ public ArrayList<DynamicTextElement> getDynamicTextList() { if (!initialized && dynamicList != null) { return dynamicList; } ArrayList<DynamicTextElement> list = new ArrayList<DynamicTextElement>(); getDynamicTextListRecursive(list, getBody()); return list; } /** * Parses a node into a list of DynamicTextElements * * @param list * @param node * @return */ public ArrayList<DynamicTextElement> getDynamicTextListRecursive( ArrayList<DynamicTextElement> list, Node node) { if (node == null) { return list; } for (int i = 0; i < node.getChildCount(); i++) { Node child = node.getChild(i); if (child.getNodeType() == Node.TEXT_NODE) { if (child.getNodeValue() != null) { list.add(new DynamicTextElement(child.getNodeValue(), DynamicTextType.STATIC)); } } else if (child.getNodeType() == Node.ELEMENT_NODE) { String tagName = ((Element) child).getTagName(); // convert input element to dynamic text string if (DYNAMIC_TEXT_CLASS .equals(((Element) child).getClassName())) { list.add(new DynamicTextElement(((Element) child) .getPropertyString("value"), DynamicTextType.VALUE)); // convert DIV or P (browser dependent) to newline } else if ("div".equalsIgnoreCase(tagName) || "p".equalsIgnoreCase(tagName)) { list.add(new DynamicTextElement("\n", DynamicTextType.STATIC)); // parse the inner HTML of this element getDynamicTextListRecursive(list, child); } } } return list; } // ====================================================== // Editor Popup // ====================================================== protected void showEditPopup(boolean isVisible) { if (isVisible) { textEditPopup .setPopupPositionAndShow(new PopupPanel.PositionCallback() { @Override public void setPosition(int offsetWidth, int offsetHeight) { int left = (getAbsoluteLeft() + getOffsetWidth() / 2 - offsetWidth / 2); int top = (getAbsoluteTop() + getOffsetHeight() / 2 - offsetHeight / 2); textEditPopup.setPopupPosition(left, top); Scheduler.get().scheduleDeferred( new Scheduler.ScheduledCommand() { @Override public void execute() { editBox.setFocus(true); } }); } }); textEditPopup.getElement().getStyle().setZIndex(1000); } else { textEditPopup.hide(); } } protected void createEditPopup() { if (textEditPopup == null) { textEditPopup = new PopupPanel(); editBox = new EditorTextField(); // TODO handle formatting with css style editBox.setWidth("9em"); editBox.setFont((GFontW) app.getPlainFontCommon()); textEditPopup.add(editBox); textEditPopup.setAutoHideEnabled(true); editBox.addValueChangeHandler(new ValueChangeHandler<String>() { @Override public void onValueChange(ValueChangeEvent<String> event) { textEditPopup.hide(); } }); } } }