/* * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.xwiki.gwt.wysiwyg.client.plugin.format.exec; import java.util.Arrays; import java.util.List; import java.util.Stack; import org.xwiki.gwt.dom.client.Element; import org.xwiki.gwt.dom.client.Text; import org.xwiki.gwt.dom.client.TextFragment; import org.xwiki.gwt.user.client.StringUtils; import org.xwiki.gwt.user.client.ui.rta.RichTextArea; import org.xwiki.gwt.user.client.ui.rta.cmd.internal.ToggleInlineStyleExecutable; import com.google.gwt.dom.client.Node; /** * Removes the in-line style from the current selection. * * @version $Id: 77e0ffd4106bab73845be4b89c0c1b235592c42e $ */ public class RemoveFormatExecutable extends ToggleInlineStyleExecutable { /** * The property of the style object which holds the value of the style attribute. */ public static final String CSS_TEXT = "cssText"; /** * The list of HTML tags that shouldn't be split while removing the in-line style. Don't include the {@code span} * tag in this list! */ public static final List<String> DO_NOT_SPLIT = Arrays.asList("a"); /** * Creates a new executable that can be used to remove the in-line style from the current text selection. * * @param rta the execution target */ public RemoveFormatExecutable(RichTextArea rta) { // We remove all the in-line CSS properties and all the formatting tags so there's no need for a specific // property or a specific tag name. super(rta, null, null, null); } @Override protected TextFragment removeStyle(Text text, int firstCharIndex, int lastCharIndex) { // Make sure we remove the style only from the selected text. text.crop(firstCharIndex, lastCharIndex); Stack<Node> stack = getInlineAncestorsStack(text); // top is the first ancestor that was split; all the ancestors that can't be split are moved before the top. Node top = null; while (stack.size() > 1) { Node parent = stack.pop(); if (DO_NOT_SPLIT.contains(parent.getNodeName().toLowerCase())) { // If we can't split the parent then we move its in-line style down to its children. if (splitParenStyle(stack.peek())) { stack.push(stack.peek().getParentNode()); } if (top != null) { // Move parent to the top. reorder(top, stack.peek()); } } else if (top == null) { domUtils.isolate(stack.peek()); top = parent; } else { isolateUpTo(stack.peek(), top); } } // Remove the in-line style from the selected text. if (top != null) { top.getParentNode().replaceChild(text, top); } return new TextFragment(text, 0, text.getLength()); } /** * Computes the stack of in-line ancestors, starting with the given node and ending with its top most in-line * ancestor. * * @param node a DOM node * @return the stack of in-line ancestors of the given node */ protected Stack<Node> getInlineAncestorsStack(Node node) { Stack<Node> stack = new Stack<Node>(); Node ancestor = node; while (ancestor != null && domUtils.isInline(ancestor)) { stack.push(ancestor); ancestor = ancestor.getParentNode(); } return stack; } /** * Removes the in-line style from the parent of the given node and applies it to the given node's siblings. * * @param child a DOM node * @return {@code true} if the in-line style has been split, {@code false} otherwise */ protected boolean splitParenStyle(Node child) { Element parent = (Element) child.getParentNode(); if (parent == null || StringUtils.isEmpty(parent.getStyle().getProperty(CSS_TEXT))) { return false; } // Group the left siblings and apply the in-line style to the whole group. if (child.getPreviousSibling() != null) { Element left = child.getOwnerDocument().createSpanElement().cast(); left.appendChild(child.getPreviousSibling()); while (child.getPreviousSibling() != null) { left.insertBefore(child.getPreviousSibling(), left.getFirstChild()); } left.getStyle().setProperty(CSS_TEXT, parent.getStyle().getProperty(CSS_TEXT)); parent.insertBefore(left, child); } // Group the right siblings and apply the style to the whole group. if (child.getNextSibling() != null) { Element right = child.getOwnerDocument().createSpanElement().cast(); do { right.appendChild(child.getNextSibling()); } while (child.getNextSibling() != null); right.getStyle().setProperty(CSS_TEXT, parent.getStyle().getProperty(CSS_TEXT)); parent.appendChild(right); } // Wrap the child node and apply the in-line style to the wrapper. Element wrapper = child.getOwnerDocument().createSpanElement().cast(); wrapper.getStyle().setProperty(CSS_TEXT, parent.getStyle().getProperty(CSS_TEXT)); parent.replaceChild(wrapper, child); wrapper.appendChild(child); // Remove the in-line style from the parent. parent.removeAttribute("style"); return true; } /** * Moves the parent of the given child node before the specified top and replicates three times the ancestors up to * the top: once for the left siblings of the given child node, once the child node itself and once for the right * siblings of the given child node. * * @param top the ancestor before which the parent is moved * @param child the child node whose parent is moved before the top */ protected void reorder(Node top, Node child) { Node parent = child.getParentNode(); if (parent == null || parent == top) { return; } Node grandParent = parent.getParentNode(); if (grandParent == null) { return; } int index = domUtils.getNodeIndex(parent); grandParent.removeChild(parent); if (child.getPreviousSibling() != null) { Node left = top.cloneNode(true); Node leaf = domUtils.getLastLeaf(left); leaf.appendChild(child.getPreviousSibling()); while (child.getPreviousSibling() != null) { leaf.insertBefore(child.getPreviousSibling(), leaf.getFirstChild()); } parent.insertBefore(left, child); } if (child.getNextSibling() != null) { Node right = top.cloneNode(true); Node leaf = domUtils.getFirstLeaf(right); do { leaf.appendChild(child.getNextSibling()); } while (child.getNextSibling() != null); parent.appendChild(right); } top.getParentNode().replaceChild(parent, top); parent.replaceChild(top, child); domUtils.insertAt(grandParent, child, index); } /** * Isolates the ancestors of the given child node up to the specified top ancestor. * * @param child the child node whose ancestors will be isolated * @param top the top most ancestor that will be isolated */ protected void isolateUpTo(Node child, Node top) { Node ancestor = child; do { domUtils.isolate(ancestor); ancestor = ancestor.getParentNode(); } while (ancestor != null && ancestor != top); } @Override public boolean isExecuted() { // NOTE: This is just a trick that forces removeStyle to be called each time execute is called. Returning false // all the time is not better so we keep it like this for now. return true; } @Override public String getParameter() { // No parameter. return null; } }