/** * Also affects the container nodelet. If the current container nodelet is the * same as the current impl nodelet, the new container will be the same as the new * impl nodelet. If it is null, it will stay null. Other scenarios are not supported * * @deprecated Use {@link #setImplNodelets(Element, Element)} instead of this method. */ @Override @Deprecated // Use #setImplNodelets(impl, container) instead public void setImplNodelet(Node nodelet) { Preconditions.checkNotNull(nodelet, "Null nodelet not supported with this deprecated method, use setImplNodelets instead"); Preconditions.checkState(containerNodelet == null || containerNodelet == getImplNodelet(), "Cannot set only the impl nodelet if the container nodelet is different"); Preconditions.checkArgument(!DomHelper.isTextNode(nodelet), "element cannot have text implnodelet"); Element element = nodelet.cast(); if (this.containerNodelet != null) { setContainerNodelet(element); } setImplNodeletInner(element); }
/** * Converts the given point to a parent-nodeAfter point, splitting a * text node if necessary. * * @param point * @return a point at the same location, between node boundaries */ public static Point.El<Node> forceElementPoint(Point<Node> point) { Point.El<Node> elementPoint = point.asElementPoint(); if (elementPoint != null) { return elementPoint; } Element parent; Node nodeAfter; Text text = point.getContainer().cast(); parent = text.getParentElement(); int offset = point.getTextOffset(); if (offset == 0) { nodeAfter = text; } else if (offset == text.getLength()) { nodeAfter = text.getNextSibling(); } else { nodeAfter = text.splitText(offset); } return Point.inElement(parent, nodeAfter); }
/** * Ensures the given container contains exactly one child, the given one. * Provides the important property that if the container is already the parent * of the given child, then the child is not removed and re-added, it is left * there; any siblings, if present, are removed. * * @param container * @param child */ public static void setOnlyChild(Element container, Node child) { if (child.getParentElement() != container) { // simple case emptyElement(container); container.appendChild(child); } else { // tricky case - avoid removing then re-appending the same child while (child.getNextSibling() != null) { child.getNextSibling().removeFromParent(); } while (child.getPreviousSibling() != null) { child.getPreviousSibling().removeFromParent(); } } }
/** {@inheritDoc} */ @Override public void setCaret(Point<ContentNode> caret) { if (caret == null) { throw new IllegalArgumentException("setCaret: caret may not be null"); } caret = findOrCreateValidSelectionPoint2(caret); Point<Node> nodeletCaret = // check if we have a place: (caret == null ? null : nodeManager.wrapperPointToNodeletPoint(caret)); // Ignore if there is no matching html location if (nodeletCaret != null) { NativeSelectionUtil.setCaret(nodeletCaret); } saveSelection(); }
private void deleteText(int amount) { Point<Node> sel = getSelectionStart(); Text txt = sel == null ? null : sel.getContainer().<Text>cast(); int startIndex = sel.getTextOffset(), len; while (amount > 0) { if (txt == null || !DomHelper.isTextNode(txt)) { throw new RuntimeException("Action ran off end of text node"); } String data = txt.getData(); int remainingInNode = data.length() - startIndex; if (remainingInNode >= amount) { len = amount; } else { len = remainingInNode; } txt.setData(data.substring(0, startIndex) + data.substring(startIndex + len)); amount -= len; startIndex = 0; txt = htmlView.getNextSibling(txt).cast(); } moveCaret(0); }
/** * Evalulates the LazyPoint to a Point. */ @Override public Point<Node> getPoint() { assert ref.getParentElement() != null : "Reference node must be attached when getting point"; switch (pointType) { case AFTER_NODE: return Point.inElement(ref.getParentElement(), ref.getNextSibling()); case BEFORE_NODE: return Point.inElement(ref.getParentElement(), ref); case AT_START: return Point.inElement(ref, ref.getFirstChild()); default: throw new RuntimeException("invalid case"); } }
@Override public void prepareForPaste() { super.prepareForPaste(); // N.B.(davidbyttow): In FF3, focus is not implicitly set by setting the // selection when appending a DOM element dynamically. So we must explicitly // set the focus. DomHelper.focus(iframe); NativeSelectionUtil.setCaret(Point.<Node>end(element)); }
/** * Search for node by pasting that element at a textRange and locating that * element directly using getElementById. This is a huge shortcut when there * are many nodes in parent. However, use with caution as it can fragment text * nodes. * * NOTE(user): The text node fragmentation is a real issue, it causes repairs * to happen. The constant splitting and repairing can also have performance * issues that needs to be investigated. We should repair the damage here, * when its clear how to fix the problem. * * @param target * @param parent * @return Point */ @SuppressWarnings("unused") // NOTE(user): Use later for nodes with many siblings. private Point<Node> searchForRangeUsingPaste(JsTextRangeIE target, Element parent) { Element elem = null; try { target.pasteHTML("<b id='__paste_target__'>X</b>"); elem = Document.get().getElementById("__paste_target__"); Node nextSibling = elem.getNextSibling(); if (DomHelper.isTextNode(nextSibling)) { return Point.inText(nextSibling, 0); } else { return Point.inElement(parent, nextSibling); } } finally { if (elem != null) { elem.removeFromParent(); } } }
void checkForWebkitEndOfLinkHack(SignalEvent signal) { // If it's inserting text if (DomHelper.isTextNode(signal.getTarget()) && (signal.getType().equals(JsEvents.DOM_CHARACTER_DATA_MODIFIED) || signal.getType().equals(JsEvents.DOM_NODE_INSERTED))) { Text textNode = signal.getTarget().cast(); if (textNode.getLength() > 0) { Node e = textNode.getPreviousSibling(); if (e != null && !DomHelper.isTextNode(e) && e.<Element>cast().getTagName().toLowerCase().equals("a")) { FocusedPointRange<Node> selection = editorInteractor.getHtmlSelection(); if (selection.isCollapsed() && selection.getFocus().getTextOffset() == 0) { editorInteractor.noteWebkitEndOfLinkHackOccurred(textNode); } } } } }
/** * Get the spacer for the given paragraph. * Lazily creates & registers one if not present. * If there's one that the browser created, registers it as our own. * If the browser put a different one in to the one that we were already * using, replace ours with the browser's. * @param paragraph * @return The spacer */ protected BRElement getSpacer(Element paragraph) { Node last = paragraph.getLastChild(); BRElement spacer = paragraph.getPropertyJSO(BR_REF).cast(); if (spacer == null) { // Register our spacer, using one the browser put in if present spacer = isSpacer(last) ? last.<BRElement>cast() : Document.get().createBRElement(); setupSpacer(paragraph, spacer); } else if (isSpacer(last) && last != spacer) { // The browser put a different one in by itself, so let's use that one if (spacer.hasParentElement()) { spacer.removeFromParent(); } spacer = last.<BRElement>cast(); setupSpacer(paragraph, spacer); } return spacer; }
private void deletify(Element element) { if (element == null) { // NOTE(danilatos): Not handling the case where the content element // is transparent w.r.t. the rendered view, but has visible children. return; } DiffManager.styleElement(element, DiffType.DELETE); DomHelper.makeUnselectable(element); for (Node n = element.getFirstChild(); n != null; n = n.getNextSibling()) { if (!DomHelper.isTextNode(n)) { deletify(n.<Element> cast()); } } }
/** * Continue tracking an existing typing sequence, after we have determined * that this selection is indeed part of an existing one * @param previousSelectionStart * @throws HtmlMissing * @throws HtmlInserted */ private void continueTypingSequence(Point<Node> previousSelectionStart) throws HtmlMissing, HtmlInserted { if (firstWrapper != null) { // minpost is only needed if we allow non-typing actions (such as moving // with arrow keys) to stay as part of the same typing sequence. otherwise, // minpost should always correspond to the last cursor position. // TODO(danilatos): Ensure this is the case updateMinPre(previousSelectionStart); // TODO(danilatos): Is it possible to ever need to check neighbouring // nodes if we're not in a text node now? If we're not, we are almost // certainly somewhere were there are no valid neighbouring text nodes, // otherwise the selection should have been reported as in one of // those nodes..... checkNeighbouringTextNodes(previousSelectionStart); } }
/** * Handles DOM mutation events. * @param event * @param contentRange last known selection */ public void handleDOMMutation(SignalEvent event, ContentRange contentRange) { // Early exit if non-safari or non-mac if (!(UserAgent.isSafari() && UserAgent.isMac())) { return; } // We don't care about DOMMutations that we generate while we are reverting. if (isReverting) { return; } previousContentRange = contentRange; Node n = event.getTarget(); if (n.getNodeType() == Node.ELEMENT_NODE) { Element e = Element.as(event.getTarget()); if (DOM_EVENTS_IGNORE.contains(event.getType())) { // ignore } else if (event.getType().equals(JsEvents.DOM_NODE_INSERTED) && handleDOMNodeInserted(e)) { } else if (event.getType().equals(JsEvents.DOM_NODE_REMOVED) && handleDOMNodeRemoved(e)) { } } }
private boolean isPartOfThisState(Point<Node> point) { checkRangeIsValid(); Text node = point.isInTextNode() ? point.getContainer().<Text>cast() : null; if (node == null) { // If we're not in a text node - i.e. we just started typing // either in an empty element, or between elements. if (htmlRange.getNodeAfter() == point.getNodeAfter() && htmlRange.getContainer() == point.getContainer()) { return true; } else if (point.getNodeAfter() == null) { return false; } else { return partOfMutatingRange(point.getNodeAfter()); } } // The first check is redundant but speeds up the general case return node == lastTextNode || partOfMutatingRange(node); }
@Override protected Skip getSkipLevel(Node node) { // TODO(danilatos): Detect and repair new elements. Currently we just ignore them. if (DomHelper.isTextNode(node) || NodeManager.hasBackReference(node.<Element>cast())) { return Skip.NONE; } else { Element element = node.<Element>cast(); Skip level = NodeManager.getTransparency(element); if (level == null) { if (!getDocumentElement().isOrHasChild(element)) { return Skip.INVALID; } register(element); } // For now, we treat unknown nodes as shallow as well. // TODO(danilatos): Either strip them or extract them return level == null ? Skip.SHALLOW : level; } }
private List<Style> collectAllChildrenStyles(NodeList<Node> childNodes) { List<Style> allStyles = Lists.newArrayList(); for (int x = 0; x < childNodes.getLength(); ++x) { Node item = childNodes.getItem(x); if (item.getNodeType() == Node.ELEMENT_NODE) { Style styles = cssHelper.getComputedStyle(item); allStyles.add(styles); allStyles.addAll(collectAllChildrenStyles(((com.google.gwt.dom.client.Element) item).getChildNodes())); } } return allStyles; }
public Audio reAttachAudio(Audio audio) { NodeList<Node> sourceList = getSourceNodes(audio); FlowPanel parentPanel = getParentPanelAndRemoveAudioElement(audio); Audio newAudio = createNewAudioAndAddToFlowPanel(parentPanel); appendChilds(sourceList, newAudio); return newAudio; }
/** * Mark implementation elements that aren't transparent as part of a * a complex implementation structure. * * @param element */ public static void walkImpl(Element element) { for (Node n = element.getFirstChild(); n != null;) { if (DomHelper.isTextNode(n)) { n = n.getNextSibling(); } else { Element e = n.cast(); if (!NodeManager.isTransparent(e)) { e.setPropertyBoolean(COMPLEX_IMPLEMENTATION_MARKER, true); } walkImpl(e); n = n.getNextSibling(); } } }
/** * Remove the given node (leaving its children in the dom) if * it does not correspond to a wrapper ContentNode * @param node * @return true if the node was removed, false if not. */ private boolean maybeStrip(Node node) { if (node == null || DomHelper.isTextNode(node)) return false; Element element = node.cast(); if (!NodeManager.hasBackReference(element)) { Node n; while ((n = element.getFirstChild()) != null) { element.getParentNode().insertBefore(n, element); } element.removeFromParent(); return true; } return false; }
/** * Takes an html selection and returns it, or null if it's not related to editor content. * * @param htmlSelection Selection range to filter. * @return htmlSelection or null if there's no related content. */ public static FocusedPointRange<Node> filterNonContentSelection( FocusedPointRange<Node> htmlSelection) { if (htmlSelection == null) { return null; // quick exit } // get just the focus point, finding the element it is inside. Point<Node> htmlFocus = htmlSelection.getFocus(); Element el; if (htmlFocus.isInTextNode()) { el = htmlFocus.getContainer().getParentElement(); } else { el = htmlFocus.getContainer().cast(); } // Assume given range is always in the editor, the htmlHelper should guarantee that. while (!NodeManager.hasBackReference(el)) { if (NodeManager.getTransparency(el) == Skip.DEEP || el.getPropertyBoolean(ContentElement.COMPLEX_IMPLEMENTATION_MARKER)) { // Exception: when we explicitly want the selection still to be reported if (!NodeManager.mayContainSelectionEvenWhenDeep(el)) { htmlSelection = null; break; } } el = el.getParentElement(); } return htmlSelection; }
/** {@inheritDoc} */ @Override public FocusedContentRange getSelectionPoints() { FocusedPointRange<Node> range = htmlHelper.getHtmlSelection(); try { needsCorrection = false; range = SelectionUtil.filterNonContentSelection(range); if (range == null) { return null; } Point<ContentNode> anchor = nodeletPointToFixedContentPoint(range.getAnchor()), focus = range.isCollapsed() ? anchor : nodeletPointToFixedContentPoint(range.getFocus()); if (anchor == null || focus == null) { return null; } // Uncomment for verbose debugging // if (Debug.isOn(LogSeverity.DEBUG)) { // logger.logXml("SELECTION: " + start + " - " + end); // } FocusedContentRange ret = range.isCollapsed() ? new FocusedContentRange(anchor) : new FocusedContentRange(anchor, focus); if (needsCorrection && ret != null) { setSelectionPoints(ret.getAnchor(), ret.getFocus()); } return ret; } finally { needsCorrection = false; } }
@Override protected Point<ContentNode> nodeletPointToWrapperPointAttempt2(Point<Node> nodelet) throws HtmlInserted, HtmlMissing { // Try again after a flush. // The normal case when this happens, is on the second character of a typing sequence // when node == a text node with no corresponding content text node. // This catch should remain here as a failsafe, but it would also be nice to improve // the code in nodeManager to deal with that scenario and return a Point.El. flushForUnextractedText(); needsCorrection = true; return nodeManager.nodeletPointToWrapperPoint(nodelet); }
private OffsetPosition getNearestElementPosition(Node focusNode, int focusOffset) { Node startContainer; if (focusNode == null) { return null; } Element e = DomHelper.isTextNode(focusNode) ? focusNode.getParentElement() : focusNode.<Element>cast(); return e == null ? null : new OffsetPosition(e.getOffsetLeft(), e.getOffsetTop(), e.getOffsetParent()); }
/** * @return length of character data in the html * @throws HtmlMissing */ public int getImplDataLength() throws HtmlMissing { Node next = checkNodeAndNeighbourReturnImpl(this); HtmlView filteredHtml = getFilteredHtmlView(); return sumTextNodesLength(getImplNodelet(), next, filteredHtml); }
/** * TODO(user): Handle multiple selections in the document. * * @return The current selection, or null if nothing is * currently selected. Note that the Elements in the range * are references to the actual elements in the DOM; not * clones. */ public static FocusedPointRange<Node> get() { if (caching) { if (cache == null) { cache = impl.get(); } return cache; } else { return impl.get(); } }
@Override public ContentNode renderSequence( ReadableDocumentView<ContentNode, ContentElement, ContentTextNode> view, ContentNode firstItem, ContentNode stopAt, Element dstParent, SelectionMatcher selectionMatcher) { Node implNodelet = firstItem.getImplNodelet(); Node clone = implNodelet != null ? implNodelet.cloneNode(false) : null; if (clone != null) { dstParent.appendChild(clone); selectionMatcher.maybeNoteHtml(firstItem, clone); } else { selectionMatcher.noteSelectionInNode(firstItem, dstParent, false); } if (firstItem instanceof ContentElement) { final Element container; if (clone != null && clone instanceof Element) { container = (Element) clone; } else { container = dstParent; } PasteFormatRenderer.renderChildren(view, container, firstItem, selectionMatcher); } return firstItem; }
/** * Compacts the multiple impl text nodelets into one * @throws HtmlMissing */ public void normaliseImplThrow() throws HtmlMissing { // TODO(danilatos): Some code in line container depends on the isImplAttached() check, // but sometimes it might not be attached but should, and so should throw an exception. if (!isContentAttached() || !isImplAttached()) { simpleNormaliseImpl(); } Text first = getImplNodelet(); if (first.getLength() == getLength()) { return; } ContentNode next = checkNodeAndNeighbour(this); HtmlView filteredHtml = getFilteredHtmlView(); //String sum = ""; Node nextImpl = (next == null) ? null : next.getImplNodelet(); for (Text nodelet = first; nodelet != nextImpl && nodelet != null; nodelet = filteredHtml.getNextSibling(first).cast()) { //sum += nodelet.getData(); if (nodelet != first) { getExtendedContext().editing().textNodeletAffected( nodelet, -1000, -1000, TextNodeChangeType.REMOVE); nodelet.removeFromParent(); } } getExtendedContext().editing().textNodeletAffected( first, -1000, -1000, TextNodeChangeType.REPLACE_DATA); first.setData(getData()); }
/** * @param point point of node * @return htmlpoint */ public static HtmlPoint nodeletPointToHtmlPoint(Point<Node> point) { if (point.isInTextNode()) { return new HtmlPoint(point.getContainer(), point.getTextOffset()); } else { return point.getNodeAfter() == null ? new HtmlPoint(point.getContainer(), point.getContainer().getChildCount()) : new HtmlPoint(point.getContainer(), DomHelper.findChildIndex(point.getNodeAfter())); } }
@Override public PointRange<Node> getOrderedHtmlSelection() { // TODO(danilatos): Optimise if (getHtmlSelection() == null) { return null; } return NativeSelectionUtil.getOrdered(); }
private Point<Node> toNodePoint(Point<ContentNode> content) { if (content == null) { return null; } else { if (content.isInTextNode()) { return Point.inText(content.getContainer().getImplNodelet(), content.getTextOffset()); } else { Node post = content.getNodeAfter() == null ? null : content.getNodeAfter().getImplNodelet(); return Point.inElement(content.getContainer().getImplNodelet(), post); } } }
private void clearContainer() { imeInput.setInnerHTML(""); if (!QuirksConstants.SUPPORTS_CARET_IN_EMPTY_SPAN) { ParagraphHelper.INSTANCE.onEmpty(imeInput); } inContainer = Point.<Node>inElement(imeInput, imeInput.getFirstChild()); }
private LazyPoint matchTextSelection( Point<ContentNode> selection, ContentNode source, Node clone) { if (selection.isInTextNode() && selection.getContainer() == source) { assert clone instanceof Text; return new EagerPoint(Point.<Node>inText(clone, selection.getTextOffset())); } else { return null; } }
/** * {@inheritDoc} * * Note(user): IE's selection type reports 'Text' for non-collapsed selections, * but 'None' for carets as well as for entirely missing selection. */ @Override FocusedPointRange<Node> get() { PointRange<Node> sel = getOrdered(); // TODO(danilatos): Proper difference between focus and anchor return sel == null ? null : new FocusedPointRange<Node>(sel.getFirst(), sel.getSecond()); }
@Override PointRange<Node> getOrdered() { // NOTE(user): try/catch here as JsTextRangeIE.duplicate throws an exception if the // selection is non-text, i.e. an image. Its much safer to wrap these IE native methods. // TODO(user): Decide whether returning null is the correct behaviour when exception is // thrown. If so, remove the logger.error(). try { // Get selection + corresponding text range JsSelectionIE selection = JsSelectionIE.get(); JsTextRangeIE start = selection.createRange(); // Best test we have found for empty selection if (checkNoSelection(selection, start)) { return null; } // create two collapsed ranges, for each end of the selection JsTextRangeIE end = start.duplicate(); start.collapse(true); end.collapse(false); // Translate to HtmlPoints Point<Node> startPoint = pointAtCollapsedRange(start); return JsTextRangeIE.equivalent(start, end) ? new PointRange<Node>(startPoint) : new PointRange<Node>(startPoint, pointAtCollapsedRange(end)); } catch (JavaScriptException e) { logger.error().log("Cannot get selection", e); return null; } }
private Point<Node> pointAtCollapsedRange(JsTextRangeIE target) { if (hint != null && isValid(hint) && pointMatchesRange(hint, target)) { return hint; } hint = pointAtCollapsedRangeInner(target); return hint; }
/** {@inheritDoc} */ @Override public boolean isSameNode(Node node, Node other) { // TODO(danilatos): Use .equals or isSameNode for nodelets in nodemanager, // typing extractor, etc. return node == other || (node != null && node.equals(other)); }
private boolean isInChildEditor(Point<Node> point) { // The editable doc is marked by an attribute EDITABLE_DOC_MARKER, if // an element is found with that attribute, and is not the element of this // editor's doc element, then it must be a child's doc element. Node n = point.getContainer(); Element e = DomHelper.isTextNode(n) ? n.getParentElement() : (Element) n; while (e != null && e != doc) { if (e.hasAttribute(EditorImpl.EDITABLE_DOC_MARKER)) { return true; } e = e.getParentElement(); } return false; }
/** * {@inheritDoc} */ @Override void set(Point<Node> startPoint, Point<Node> endPoint) { JsTextRangeIE start = collapsedRangeAtPoint(startPoint); JsTextRangeIE end = collapsedRangeAtPoint(endPoint); // TODO(danilatos): Should be possible to do this more efficiently, // by separately moving the end point and start point of the range, // instead of creating 2 collapsed ranges. JsTextRangeIE.create() .setEndPoint(StartToStart, start) .setEndPoint(EndToEnd, end) .select(); }
/** TODO(danilatos,user) Bring back early exit & clean up this interface */ public ContentNode findNodeWrapper(Node node, Element earlyExit) throws HtmlInserted, HtmlMissing { if (node == null) { return null; } if (DomHelper.isTextNode(node)) { return findTextWrapper(node.<Text>cast(), false); } else { return findElementWrapper(node.<Element>cast(), earlyExit); } }
/** * Returns the specified child element of the given element. * * @param parent parent element * @param childTypeString string representaton of the child element * @param toExclude element to be excluded from the search * @param startFrom element to start search with * @param step step to change the child index */ private static Element findChildElementExclusive(Element parent, String childTypeString, Element toExclude, Element startFrom, int step) { if (parent != null) { NodeList<Node> children = parent.getChildNodes(); int begin; int end; if (step == +1) { begin = 0; end = children.getLength(); } else { begin = children.getLength()-1; end = -1; } boolean startFound = false; for (int i = begin; i != end; i += step) { Node child = children.getItem(i); if (child instanceof Element) { Element e = (Element) child; if (doesElementHaveTypeString(e, childTypeString)) { if (e != toExclude) { if (startFrom != null && !startFound) { startFound = true; } else { return e; } } } } } } return null; }