/*
* 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.style.exec;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.xwiki.gwt.dom.client.Element;
import org.xwiki.gwt.dom.client.Range;
import org.xwiki.gwt.dom.client.Selection;
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.InlineStyleExecutable;
import com.google.gwt.dom.client.Node;
/**
* Applies a given style name to each of the text nodes from the current text selection.
*
* @version $Id: 159afe91725ab4af3ef8bb49359ef658bfbcfa54 $
*/
public class InlineStyleNameExecutable extends InlineStyleExecutable
{
/**
* Flag indicating if this executable has been executed on the current selection with the given parameter.
*/
private boolean executed;
/**
* Creates a new instance.
*
* @param rta the execution target
*/
public InlineStyleNameExecutable(RichTextArea rta)
{
super(rta, null);
}
@Override
public boolean execute(String parameter)
{
executed = getStyleNames(rta.getDocument().getSelection()).contains(parameter);
return super.execute(parameter);
}
@Override
protected TextFragment execute(Text text, int startIndex, int endIndex, String parameter)
{
if (executed) {
return removeStyleName(text, startIndex, endIndex, parameter);
} else if (!matchesStyleName(text, parameter)) {
return super.execute(text, startIndex, endIndex, parameter);
} else {
return new TextFragment(text, startIndex, endIndex);
}
}
@Override
protected void addStyle(Element element, String parameter)
{
element.addClassName(parameter);
}
/**
* @param text a text node
* @param parameter a style name
* @return {@code true} if any of the in-line ancestors of the given text node has the specified style name, {@code
* false} otherwise
*/
protected boolean matchesStyleName(Text text, String parameter)
{
Node ancestor = text.getParentNode();
while (ancestor != null && ancestor.getNodeType() == Node.ELEMENT_NODE && domUtils.isInline(ancestor)) {
if (Element.as(ancestor).hasClassName(parameter)) {
return true;
}
ancestor = ancestor.getParentNode();
}
return false;
}
/**
* Removes the give style name from all the in-line element ancestors of the given text node, making sure no other
* nodes are affected by this change (by isolating the nodes while iterating the ancestors).
*
* @param text the target text node
* @param startIndex the first character on which we remove the style name
* @param endIndex the last character on which we remove the style name
* @param styleName the style name to be removed
* @return the text fragment where the given style name doesn't apply anymore
*/
protected TextFragment removeStyleName(Text text, int startIndex, int endIndex, String styleName)
{
// Make sure we remove the style name only from the selected text.
text.crop(startIndex, endIndex);
// Remove the given style name from all the in-line ancestors.
Node child = text;
Node parent = child.getParentNode();
while (parent != null && parent.getNodeType() == Node.ELEMENT_NODE && domUtils.isInline(parent)) {
domUtils.isolate(child);
Element.as(parent).removeClassName(styleName);
child = child.getParentNode();
parent = child.getParentNode();
}
return new TextFragment(text, 0, text.getLength());
}
@Override
public String getParameter()
{
return StringUtils.join(getStyleNames(rta.getDocument().getSelection()), " ");
}
/**
* @param selection a text selection
* @return the set of style names that affect all the text nodes touched by the given text selection
*/
protected Set<String> getStyleNames(Selection selection)
{
Set<String> selectionStyleNames = null;
for (int i = 0; i < selection.getRangeCount(); i++) {
Set<String> rangeStyleNames = getStyleNames(selection.getRangeAt(i));
if (selectionStyleNames == null) {
selectionStyleNames = rangeStyleNames;
} else {
selectionStyleNames.retainAll(rangeStyleNames);
}
if (selectionStyleNames.isEmpty()) {
break;
}
}
if (selectionStyleNames == null) {
selectionStyleNames = Collections.emptySet();
}
return selectionStyleNames;
}
/**
* @param range a text range
* @return the set of style names that affect all the text nodes touched by the given text range
*/
protected Set<String> getStyleNames(Range range)
{
if (range.isCollapsed()) {
return getStyleNames(range.getStartContainer());
} else {
List<Text> textNodes = getNonEmptyTextNodes(range);
Set<String> rangeStyleNames = null;
for (int i = 0; i < textNodes.size(); i++) {
Set<String> textStyleNames = getStyleNames(textNodes.get(i));
if (rangeStyleNames == null) {
rangeStyleNames = textStyleNames;
} else {
rangeStyleNames.retainAll(textStyleNames);
}
if (rangeStyleNames.isEmpty()) {
break;
}
}
if (rangeStyleNames == null) {
rangeStyleNames = Collections.emptySet();
}
return rangeStyleNames;
}
}
/**
* @param node a DOM node
* @return the set of style names that affect the given DOM node
*/
protected Set<String> getStyleNames(Node node)
{
Set<String> styleNames = new HashSet<String>();
Node ancestor = node.getParentElement();
while (ancestor != null && ancestor.getNodeType() == Node.ELEMENT_NODE && domUtils.isInline(ancestor)) {
String className = Element.as(ancestor).getClassName();
if (!StringUtils.isEmpty(className)) {
styleNames.addAll(Arrays.asList(className.split("\\s+")));
}
ancestor = ancestor.getParentNode();
}
return styleNames;
}
@Override
public boolean isExecuted()
{
// Always return true because we cannot prove the contrary (and it's not important).
return true;
}
}