/*
* 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.user.client.ui.rta.cmd.internal;
import java.util.ArrayList;
import java.util.List;
import org.xwiki.gwt.dom.client.Document;
import org.xwiki.gwt.dom.client.Element;
import org.xwiki.gwt.dom.client.Property;
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 com.google.gwt.dom.client.Node;
/**
* Applies in-line style to the current selection.
*
* @version $Id: eec4a23313e323aa6c5f99d7f7d76fb9da4eab19 $
*/
public class InlineStyleExecutable extends AbstractSelectionExecutable
{
/**
* The style property used when applying style.
*/
private final Property property;
/**
* Creates a new instance that uses the given style property.
*
* @param rta the execution target
* @param property the style property used when applying style
*/
public InlineStyleExecutable(RichTextArea rta, Property property)
{
super(rta);
this.property = property;
}
/**
* @return the style property associated with the executable
*/
protected Property getProperty()
{
return property;
}
@Override
public boolean execute(String parameter)
{
Selection selection = rta.getDocument().getSelection();
List<Range> ranges = new ArrayList<Range>();
for (int i = 0; i < selection.getRangeCount(); i++) {
ranges.add(execute(selection.getRangeAt(i), parameter));
}
selection.removeAllRanges();
for (Range range : ranges) {
selection.addRange(range);
}
return true;
}
/**
* Applies the underlying style {@link #property} with the given value to the specified range.
*
* @param range the target range
* @param parameter the value to set for the style {@link #property}
* @return the given range after being processed
*/
protected Range execute(Range range, String parameter)
{
Range styledRange = range;
if (range.isCollapsed()) {
switch (range.getStartContainer().getNodeType()) {
case Node.TEXT_NODE:
Text text = (Text) range.getStartContainer();
text = execute(text, range.getStartOffset(), range.getEndOffset(), parameter).getText();
range.selectNodeContents(text);
break;
case Node.ELEMENT_NODE:
Text empty = (Text) range.getStartContainer().getOwnerDocument().createTextNode("");
domUtils.insertAt(range.getStartContainer(), empty, range.getStartOffset());
range.selectNodeContents(execute(empty, 0, 0, parameter).getText());
break;
default:
// Do nothing.
break;
}
} else {
// Iterate through all the text nodes within the given range and apply the underlying style.
TextFragment startContainer = null;
TextFragment endContainer = null;
List<Text> textNodes = getNonEmptyTextNodes(range);
for (int i = 0; i < textNodes.size(); i++) {
Text text = textNodes.get(i);
int startIndex = 0;
if (text == range.getStartContainer()) {
startIndex = range.getStartOffset();
}
int endIndex = text.getLength();
if (text == range.getEndContainer()) {
endIndex = range.getEndOffset();
}
endContainer = execute(text, startIndex, endIndex, parameter);
if (startContainer == null) {
startContainer = endContainer;
}
}
if (startContainer != null) {
// We cannot reuse the given range because it may have been invalidated by the DOM mutations.
styledRange = ((Document) startContainer.getText().getOwnerDocument()).createRange();
styledRange.setStart(startContainer.getText(), startContainer.getStartIndex());
styledRange.setEnd(endContainer.getText(), endContainer.getEndIndex());
}
}
return styledRange;
}
/**
* @param range a DOM range
* @return the list of non empty text nodes that are completely or partially (at least one character) included in
* the given range
*/
protected List<Text> getNonEmptyTextNodes(Range range)
{
Node leaf = domUtils.getFirstLeaf(range);
Node lastLeaf = domUtils.getLastLeaf(range);
List<Text> textNodes = new ArrayList<Text>();
// If the range starts at the end of a text node we have to ignore that node.
if (isNonEmptyTextNode(leaf)
&& (leaf != range.getStartContainer() || range.getStartOffset() < leaf.getNodeValue().length())) {
textNodes.add((Text) leaf);
}
while (leaf != lastLeaf) {
leaf = domUtils.getNextLeaf(leaf);
if (isNonEmptyTextNode(leaf)) {
textNodes.add((Text) leaf);
}
}
// If the range ends at the start of a text node then we have to ignore that node.
int lastIndex = textNodes.size() - 1;
if (lastIndex >= 0 && range.getEndOffset() == 0 && textNodes.get(lastIndex) == range.getEndContainer()) {
textNodes.remove(lastIndex);
}
return textNodes;
}
/**
* @param node a DOM node
* @return {@code true} if the given node is of type {@link Node#TEXT_NODE} and it's not empty, {@code false}
* otherwise
*/
private boolean isNonEmptyTextNode(Node node)
{
return node.getNodeType() == Node.TEXT_NODE && node.getNodeValue().length() > 0;
}
/**
* Applies the underlying style {@link #property} with the given value to the specified text fragment.
*
* @param text the target text node
* @param startIndex the first character to be processed
* @param endIndex the last character to be processed
* @param parameter the value to set for the style {@link #property}
* @return a text fragment indicating what has been processed
*/
protected TextFragment execute(Text text, int startIndex, int endIndex, String parameter)
{
// Make sure the style is applied only to the selected text.
text.crop(startIndex, endIndex);
// Look for the farthest in-line element ancestor without sibling nodes.
Element ancestor = null;
Node node = text.getParentNode();
while (node.getChildNodes().getLength() == 1 && domUtils.isInline(node)) {
ancestor = (Element) node;
node = node.getParentNode();
}
// If we haven't found the proper ancestor, we wrap the text in a span element.
if (ancestor == null) {
ancestor = text.getOwnerDocument().createSpanElement().cast();
text.getParentNode().replaceChild(ancestor, text);
ancestor.appendChild(text);
}
// Apply the style.
addStyle(ancestor, parameter);
return new TextFragment(text, 0, text.getLength());
}
/**
* Styles the given element.
*
* @param element the element to be styled
* @param parameter the value of the style property that is added
*/
protected void addStyle(Element element, String parameter)
{
element.getStyle().setProperty(property.getJSName(), parameter);
}
@Override
public String getParameter()
{
Selection selection = rta.getDocument().getSelection();
String selectionParameter = null;
for (int i = 0; i < selection.getRangeCount(); i++) {
String rangeParameter = getParameter(selection.getRangeAt(i));
if (rangeParameter == null || (selectionParameter != null && !selectionParameter.equals(rangeParameter))) {
return null;
}
selectionParameter = rangeParameter;
}
return selectionParameter;
}
/**
* @param range the range to be inspected
* @return the value of the style {@link #property} for the given range
*/
protected String getParameter(Range range)
{
if (range.isCollapsed()) {
return getParameter(range.getStartContainer());
} else {
List<Text> textNodes = getNonEmptyTextNodes(range);
String rangeParameter = null;
for (int i = 0; i < textNodes.size(); i++) {
String textParameter = getParameter(textNodes.get(i));
if (textParameter == null || (rangeParameter != null && !rangeParameter.equals(textParameter))) {
return null;
}
rangeParameter = textParameter;
}
return rangeParameter;
}
}
/**
* @param inputNode a DOM node
* @return the value of the style {@link #property} for the given node
*/
protected String getParameter(Node inputNode)
{
Node node = inputNode;
if (node.getNodeType() != Node.ELEMENT_NODE) {
node = node.getParentNode();
}
return node == null || node.getNodeType() != Node.ELEMENT_NODE ? null : getParameter(Element.as(node));
}
/**
* @param element a DOM element
* @return the {@link #property} value taken from the given element's computed style
*/
protected String getParameter(Element element)
{
if (getProperty().isInheritable()) {
return element.getComputedStyleProperty(property.getJSName());
} else {
Node node = element;
while (node != null && node.getNodeType() == Node.ELEMENT_NODE) {
String value = Element.as(node).getComputedStyleProperty(property.getJSName());
if (!StringUtils.areEqual(getProperty().getDefaultValue(), value)) {
return value;
}
node = node.getParentNode();
}
return getProperty().getDefaultValue();
}
}
}