blob: 9e37058d3c28a2b60a98eade26fb62609edd376c [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
package flashx.textLayout.elements
{
import flash.text.engine.GroupElement;
import flash.text.engine.TextElement;
import flash.utils.getQualifiedClassName;
import flashx.textLayout.container.ContainerController;
import flashx.textLayout.debug.Debugging;
import flashx.textLayout.debug.assert;
import flashx.textLayout.events.ModelChange;
import flashx.textLayout.formats.FormatValue;
import flashx.textLayout.formats.ITextLayoutFormat;
import flashx.textLayout.formats.TextLayoutFormat;
import flashx.textLayout.formats.WhiteSpaceCollapse;
import flashx.textLayout.property.Property;
import flashx.textLayout.tlf_internal;
import flashx.textLayout.utils.CharacterUtil;
use namespace tlf_internal;
[DefaultProperty("mxmlChildren")]
/**
* The SpanElement class represents a run of text that has a single set of formatting attributes applied. SpanElement
* objects contain the text in a paragraph. A simple paragraph (ParagraphElement) includes one or more SpanElement objects.
*
* <p>A ParagraphElement will have a single SpanElement object if all the text in the paragraph shares the same set of
* attributes. It has multiple SpanElement objects if the text in the paragraph has multiple formats.</p>
*
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*
* @includeExample examples/SpanElementExample.as -noswf
*
* @see FlowElement
* @see ParagraphElement
* @see TextFlow
*/
public class SpanElement extends FlowLeafElement
{
/** @private */
tlf_internal static const kParagraphTerminator:String = '\u2029';
/** Constructor - creates a SpanElement object to hold a run of text in a paragraph.
*
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*/
public function SpanElement()
{
super();
}
/** @private */
override tlf_internal function createContentElement():void
{
if (_blockElement)
return;
computedFormat; // BEFORE creating the element
_blockElement = new TextElement(_text,null);
CONFIG::debug { Debugging.traceFTECall(_blockElement,null,"new TextElement()"); }
CONFIG::debug { Debugging.traceFTEAssign(_blockElement, "text", _text); }
super.createContentElement();
}
/** @private */
public override function shallowCopy(startPos:int = 0, endPos:int = -1):FlowElement
{
if (endPos == -1)
endPos = textLength;
// Note to callers: If you are calling this function outside a try/catch, do ensure that the
// state of the model is coherent before the call.
var retFlow:SpanElement = super.shallowCopy(startPos, endPos) as SpanElement;
var startSpan:int = 0;
var endSpan:int = startSpan + textLength;
var leafElStartPos:int = startSpan >= startPos ? startSpan : startPos;
var leafElEndPos:int = endSpan < endPos ? endSpan : endPos;
if (leafElEndPos == textLength && hasParagraphTerminator)
--leafElEndPos;
if (leafElStartPos > leafElEndPos)
throw RangeError(GlobalSettings.resourceStringFunction("badShallowCopyRange"));
if (((leafElStartPos != endSpan) && CharacterUtil.isLowSurrogate(_text.charCodeAt(leafElStartPos))) ||
((leafElEndPos != 0) && CharacterUtil.isHighSurrogate(_text.charCodeAt(leafElEndPos-1))))
throw RangeError(GlobalSettings.resourceStringFunction("badSurrogatePairCopy"));
if (leafElStartPos != leafElEndPos)
retFlow.replaceText(0, retFlow.textLength, _text.substring(leafElStartPos, leafElEndPos));
return retFlow;
}
/** @private */
override protected function get abstract():Boolean
{ return false; }
/** @private */
tlf_internal override function get defaultTypeName():String
{ return "span"; }
/** @private */
public override function get text():String
{
// test textLength cause this is a property and the debugger may run this calculation in intermediate states
if (textLength == 0)
return "";
return hasParagraphTerminator ? _text.substr(0,textLength-1) : _text;
}
/**
* Receives the String of text that this SpanElement object holds.
*
* <p>The text of a span does not include the carriage return (CR) at the end of the paragraph
* but it is included in the value of <code>textLength</code>.</p>
*
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*/
public function set text(textValue:String):void
{
//original code stripped breaking and tab characters. new code moved to collapseWhitevar newLineTabPattern:RegExp = /[\n\r\t]/g;
replaceText(0,textLength, textValue);
}
/** @private */
public override function getText(relativeStart:int=0, relativeEnd:int=-1, paragraphSeparator:String="\n"):String
{
if (relativeEnd == -1)
relativeEnd = textLength;
if (textLength && relativeEnd == textLength && hasParagraphTerminator)
--relativeEnd; // don't include terminator
return _text ? _text.substring(relativeStart, relativeEnd) : "";
}
[RichTextContent]
/**
* Sets text based on content within span tags; always deletes existing children.
* This property is intended for use during MXML compiled import in Flex. Flash Professional ignores this property.
* When TLF markup elements have other
* TLF markup elements as children, the children are assigned to this property.
*
* @throws TypeError If array element is not a SpecialCharacterElement or a String.
* @param array - an array of elements within span tags. Each element of array must be a SpecialCharacterElement or a String.
*
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*/
public function get mxmlChildren():Array
{
return [ text ];
}
public function set mxmlChildren(array:Array):void
{
/* NOTE: all FlowElement implementers and overrides of mxmlChildren must specify [RichTextContent] metadata */
var str:String = new String();
for each (var elem:Object in array)
{
if (elem is String)
str += elem as String;
else if (elem is Number) // TODO: remove the Number else if when we can test with the most recent compilers. The [RichTextContent] metadata fixes the issue
str += elem.toString();
else if (elem is BreakElement)
str += String.fromCharCode(0x2028);
else if (elem is TabElement)
{
// Add a placeholder (from Unicode private use area) instead of '\t' because the latter is
// stripped during white space collapse. After white space collapse, we will change the placeholder
// to '\t'
str += String.fromCharCode(0xE000);
}
else if (elem != null)
throw new TypeError(GlobalSettings.resourceStringFunction("badMXMLChildrenArgument",[ getQualifiedClassName(elem) ])); // NO PMD
}
replaceText(0,textLength, str);
}
/**
* Specifies whether this SpanElement object terminates the paragraph. The SpanElement object that terminates a
* paragraph has an extra, hidden character at the end. This character is added automatically by the component and is
* included in the value of the <code>textLength</code> property.
*
* @private */
tlf_internal function get hasParagraphTerminator():Boolean
{
var p:ParagraphElement = getParagraph();
return (p && p.getLastLeaf() == this);
}
/** @private */
CONFIG::debug tlf_internal function verifyParagraphTerminator():void
{
assert(_text && _text.length && _text.charAt(_text.length-1) == SpanElement.kParagraphTerminator,
"attempting to remove para terminator when it doesn't exist");
}
/**
* Makes a shallow copy of this SpanElement between 2 character positions
* and returns it as a FlowElement. Unlike deepCopy, shallowCopy does
* not copy any of the children of this SpanElement.
*
*/
// If I have a sequence of different sorts of spaces (e.g., en quad, hair space), would I want them converted down to one space? Probably not.
// For now, u0020 is the only space character we consider for eliminating duplicates, though u00A0 (non-breaking space) is potentially eligible.
private static const _dblSpacePattern:RegExp = /[\u0020]{2,}/g;
// Tab, line feed, and carriage return
private static const _newLineTabPattern:RegExp = /[\u0009\u000a\u000d]/g;
private static const _tabPlaceholderPattern:RegExp = /\uE000/g;
// static private const anyPrintChar:RegExp = /[^\s]/g;
// Consider only tab, line feed, carriage return, and space as characters used for pretty-printing.
// While debatable, this is consistent with what CSS does.
static private const anyPrintChar:RegExp = /[^\u0009\u000a\u000d\u0020]/g;
/** @private */
tlf_internal override function applyWhiteSpaceCollapse(collapse:String):void
{
var ffc:ITextLayoutFormat = this.formatForCascade;
var wsc:* = ffc ? ffc.whiteSpaceCollapse : undefined;
if (wsc !== undefined && wsc != FormatValue.INHERIT)
collapse = wsc;
var origTxt:String = text;
var tempTxt:String = origTxt;
if (!collapse /* null == default value == COLLAPSE */ || collapse == WhiteSpaceCollapse.COLLAPSE)
{
// The span was added automatically when a String was passed to replaceChildren.
// If it contains only whitespace, we remove the text.
if (impliedElement && parent != null)
{
// var matchArray:Array = tempTxt.search(anyPrintChar);
if (tempTxt.search(anyPrintChar) == -1)
{
parent.removeChild(this);
return;
}
}
// For now, replace the newlines and tabs inside the element with a space.
// This is necessary for support of compiled mxml files that have newlines and tabs, because
// these are most likely not intended to be part of the text content, but only there so the
// text can be conveniently edited in the mxml file. Later on we need to add standalone elements
// for <br/> and <tab/>. Note that tab character is not supported in HTML.
tempTxt = tempTxt.replace(_newLineTabPattern, " ");
// Replace sequences of 2 or more whitespace characters with single space
tempTxt = tempTxt.replace(_dblSpacePattern, " ");
}
// Replace tab placeholders (used for tabs that are expected to survive whitespace collapse) with '\t'
tempTxt = tempTxt.replace(_tabPlaceholderPattern, '\t')
if (tempTxt != origTxt)
replaceText(0, textLength, tempTxt);
super.applyWhiteSpaceCollapse(collapse);
}
/**
* Updates the text in text span based on the specified start and end positions. To insert text, set the end position
* equal to the start position. To append text to the existing text in the span, set the start position and the
* end position equal to the length of the existing text.
*
* <p>The replaced text includes the start character and up to but not including the end character.</p>
*
* @param relativeStartPosition The index position of the beginning of the text to be replaced,
* relative to the start of the span. The first character in the span is at position 0.
* @param relativeEndPosition The index one position after the last character of the text to be replaced,
* relative to the start of the span. Set this value equal to <code>relativeStartPos</code>
* for an insert.
* @param textValue The replacement text or the text to add, as the case may be.
*
* @throws RangeError The <code>relativeStartPosition</code> or <code>relativeEndPosition</code> specified is out of
* range or a surrogate pair is being split as a result of the replace.
*
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*
* @includeExample examples/SpanElement_replaceTextExample.as -noswf
*/
public function replaceText(relativeStartPosition:int, relativeEndPosition:int, textValue:String):void
{
// Note to callers: If you are calling this function outside a try/catch, do ensure that the
// state of the model is coherent before the call.
if (relativeStartPosition < 0 || relativeEndPosition > textLength || relativeEndPosition < relativeStartPosition)
throw RangeError(GlobalSettings.resourceStringFunction("invalidReplaceTextPositions"));
if ((relativeStartPosition != 0 && relativeStartPosition != textLength && CharacterUtil.isLowSurrogate(_text.charCodeAt(relativeStartPosition))) ||
(relativeEndPosition != 0 && relativeEndPosition != textLength && CharacterUtil.isHighSurrogate(_text.charCodeAt(relativeEndPosition-1))))
throw RangeError (GlobalSettings.resourceStringFunction("invalidSurrogatePairSplit"));
if (hasParagraphTerminator)
{
CONFIG::debug { assert(textLength > 0,"invalid span"); }
if (relativeStartPosition == textLength)
relativeStartPosition--;
if (relativeEndPosition == textLength)
relativeEndPosition--;
}
if (relativeEndPosition != relativeStartPosition)
modelChanged(ModelChange.TEXT_DELETED,this,relativeStartPosition,relativeEndPosition-relativeStartPosition);
replaceTextInternal(relativeStartPosition,relativeEndPosition,textValue);
if (textValue && textValue.length)
modelChanged(ModelChange.TEXT_INSERTED,this,relativeStartPosition,textValue.length);
}
private function replaceTextInternal(startPos:int, endPos:int, textValue:String):void
{
var textValueLength:int = textValue == null ? 0 : textValue.length;
var deleteTotal:int = endPos-startPos;
var deltaChars:int = textValueLength - deleteTotal;
if (_blockElement)
{
(_blockElement as TextElement).replaceText(startPos,endPos,textValue);
_text = _blockElement.rawText;
CONFIG::debug { Debugging.traceFTECall(null,_blockElement as TextElement,"replaceText",startPos,endPos,textValue); }
}
else if (_text)
{
if (textValue)
_text = _text.slice(0, startPos) + textValue + _text.slice(endPos, _text.length);
else
_text = _text.slice(0, startPos) + _text.slice(endPos, _text.length);
}
else
_text = textValue;
if (deltaChars != 0)
{
updateLengths(getAbsoluteStart() + startPos, deltaChars, true);
deleteContainerText(endPos,deleteTotal);
if (textValueLength != 0)
{
var enclosingContainer:ContainerController = getEnclosingController(startPos);
if (enclosingContainer)
ContainerController(enclosingContainer).setTextLength(enclosingContainer.textLength + textValueLength);
}
}
CONFIG::debug {
assert(textLength == (_text ? _text.length : 0),"span textLength doesn't match the length of the text property, text property length is " + _text.length.toString() + " textLength property is " + textLength.toString());
assert(_blockElement == null || _blockElement.rawText == _text,"mismatched text");
}
}
/** @private */
tlf_internal function addParaTerminator():void
{
CONFIG::debug
{
// TODO: Is this assert valid? Do we prevent users from adding para terminators themselves?
if (_blockElement && _blockElement.rawText && _blockElement.rawText.length)
assert(_blockElement.rawText.charAt(_blockElement.rawText.length-1) != SpanElement.kParagraphTerminator,"adding para terminator twice");
}
if(_text && _text.substr(-1) == SpanElement.kParagraphTerminator)// terminator exists. Bail out.
return;
replaceTextInternal(textLength,textLength,SpanElement.kParagraphTerminator);
CONFIG::debug
{
// TODO: Is this assert valid? Do we prevent users from adding para terminators themselves?
if (_blockElement)
assert(_blockElement.rawText.charAt(_blockElement.rawText.length-1) == SpanElement.kParagraphTerminator,"adding para terminator failed");
}
modelChanged(ModelChange.TEXT_INSERTED,this,textLength-1,1);
}
/** @private */
tlf_internal function removeParaTerminator():void
{
CONFIG::debug
{
assert(_text && _text.length && _text.charAt(_text.length-1) == SpanElement.kParagraphTerminator,
"attempting to remove para terminator when it doesn't exist");
}
if(!_text || _text.substr(-1) != SpanElement.kParagraphTerminator)// no terminator exists. Bail out.
return;
replaceTextInternal(textLength-1,textLength,"");
modelChanged(ModelChange.TEXT_DELETED,this,textLength > 0 ? textLength-1 : 0,1);
}
// ****************************************
// Begin tree modification support code
// ****************************************
/**
* Splits this SpanElement object at the specified position and returns a new SpanElement object for the content
* that follows the specified position.
*
* <p>This method throws an error if you attempt to split a surrogate pair. In Unicode UTF-16, a surrogate pair is a pair of
* 16-bit code units (a high code unit and a low code unit) that represent one of the abstract Unicode characters
* that cannot be represented in a single 16-bit word. The 16-bit high code unit is in the range of D800 to DBFF. The
* 16-bit low code unit is in the range of DC00 to DFFF.</p>
*
* @param relativePosition - relative position in the span to create the split
* @return - the newly created span.
* @throws RangeError <code>relativePosition</code> is less than 0 or greater than textLength or a surrogate pair is being split.
*
* @playerversion Flash 10
* @playerversion AIR 1.5
* @langversion 3.0
*
* @private
*/
public override function splitAtPosition(relativePosition:int):FlowElement
{
// Note to callers: If you are calling this function outside a try/catch, do ensure that the
// state of the model is coherent before the call.
if (relativePosition < 0 || relativePosition > textLength)
throw RangeError(GlobalSettings.resourceStringFunction("invalidSplitAtPosition"));
if ((relativePosition < textLength) && CharacterUtil.isLowSurrogate(String(text).charCodeAt(relativePosition)))
throw RangeError (GlobalSettings.resourceStringFunction("invalidSurrogatePairSplit"));
var newSpan:SpanElement = new SpanElement();
// clone styling information
newSpan.id = this.id;
newSpan.typeName = this.typeName;
if (parent)
{
var newBlockElement:TextElement;
var newSpanLength:int = textLength - relativePosition;
if (_blockElement)
{
// optimized version leverages player APIs
// TODO: Jeff to add split on TextElement so we don't have to go find a group every time
var group:GroupElement = parent.createContentAsGroup(getElementRelativeStart(parent));
var elementIndex:int = group.getElementIndex(_blockElement);
CONFIG::debug { assert(elementIndex == parent.getChildIndex(this),"bad group index"); }
CONFIG::debug { assert(elementIndex != -1 && elementIndex < group.elementCount,"bad span split"); }
//trace("GROUP BEFORE: " + group.rawText);
//trace("BLOCK BEFORE: " + group.block.content.rawText);
//trace("calling group.splitTextElement("+elementIndex.toString()+","+relativePosition.toString()+")");
group.splitTextElement(elementIndex, relativePosition);
CONFIG::debug { Debugging.traceFTECall(null,group,"splitTextElement",elementIndex,relativePosition); }
//trace("GROUP AFTER: " + group.rawText);
//trace("BLOCK AFTER: " + group.block.content.rawText);
// no guarantee on how the split works
_blockElement = group.getElementAt(elementIndex);
_text = _blockElement.rawText;
CONFIG::debug { Debugging.traceFTECall(_blockElement,group,"getElementAt",elementIndex); }
newBlockElement = group.getElementAt(elementIndex+1) as TextElement;
CONFIG::debug { Debugging.traceFTECall(newBlockElement,group,"getElementAt",elementIndex+1); }
}
else if (relativePosition < textLength)
{
newSpan.text = _text.substr(relativePosition);
_text = _text.substring(0, relativePosition);
}
// Split this span at the offset, into two equivalent spans
modelChanged(ModelChange.TEXT_DELETED,this,relativePosition,newSpanLength);
newSpan.quickInitializeForSplit(this, newSpanLength, newBlockElement);
setTextLength(relativePosition);
// slices it in, sets the parent and the start
parent.addChildAfterInternal(this,newSpan);
var p:ParagraphElement = this.getParagraph();
p.updateTerminatorSpan(this,newSpan);
parent.modelChanged(ModelChange.ELEMENT_ADDED,newSpan,newSpan.parentRelativeStart,newSpan.textLength);
}
else
{
// this version also works if para is non-null but may not be as efficient.
newSpan.format = format;
// could be we are splitting
if (relativePosition < textLength)
{
newSpan.text = String(this.text).substr(relativePosition);
replaceText(relativePosition,textLength,null);
}
}
return newSpan;
}
/** @private */
tlf_internal override function normalizeRange(normalizeStart:uint,normalizeEnd:uint):void
{
if (this.textLength == 1 && !bindableElement)
{
var p:ParagraphElement = getParagraph();
if (p && p.getLastLeaf() == this)
{
var prevLeaf:FlowLeafElement = getPreviousLeaf(p);
if (prevLeaf)
{
if (!TextLayoutFormat.isEqual(this.format, prevLeaf.format))
this.format = prevLeaf.format;
}
}
}
super.normalizeRange(normalizeStart,normalizeEnd);
}
/** @private */
tlf_internal override function mergeToPreviousIfPossible():Boolean
{
if (parent && !bindableElement)
{
var myidx:int = parent.getChildIndex(this);
if (myidx != 0)
{
var sib:SpanElement = parent.getChildAt(myidx-1) as SpanElement;
// If the element we're checking for merge has only the terminator, and the previous element
// is not a Span, then we always merge with the previous span (NOT the previous sib).
// We just remove this span, and add the terminator to the previous span.
if (!sib && this.textLength == 1 && this.hasParagraphTerminator)
{
var p:ParagraphElement = getParagraph();
if (p)
{
var prevLeaf:FlowLeafElement = getPreviousLeaf(p) as SpanElement;
if (prevLeaf)
{
parent.removeChildAt(myidx);
return true;
}
}
}
if (sib == null)
return false;
// If this has an active event mirror do not merge
if (this.hasActiveEventMirror())
return false;
var thisIsSimpleTerminator:Boolean = textLength == 1 && hasParagraphTerminator;
// if sib has an active event mirror still merge if this is a simple terminator span
if (sib.hasActiveEventMirror() && !thisIsSimpleTerminator)
return false;
// always merge if this is just a terminator
if (thisIsSimpleTerminator || equalStylesForMerge(sib))
{
CONFIG::debug { assert(this.parent == sib.parent, "Should never merge two spans with different parents!"); }
CONFIG::debug { assert(TextLayoutFormat.isEqual(this.formatForCascade,sib.formatForCascade) || (this.textLength == 1 && this.hasParagraphTerminator), "Bad merge!"); }
// Merge the spans
var siblingInsertPosition:int = sib.textLength;
sib.replaceText(siblingInsertPosition, siblingInsertPosition, this.text);
parent.removeChildAt(myidx);
return true;
}
}
}
return false;
}
// ****************************************
// Begin debug support code
// ****************************************
/** @private */
CONFIG::debug public override function debugCheckFlowElement(depth:int = 0, extraData:String = ""):int
{
// debugging function that asserts if the flow element tree is in an invalid state
var rslt:int = super.debugCheckFlowElement(depth,"text:"+String(text).substr(0,32)+" "+extraData);
assert(_blockElement == null || _blockElement.rawText == _text,"debugCheckFlowElement: mismatched text");
var textLen:int = textLength;
if (_text)
rslt += assert(textLen == _text.length,"span is different than its textElement, span text length is " + _text.length.toString() + " expecting " + textLen.toString());
else
rslt += assert(textLen == 0,"span is different than its textElement, span text length is null expecting " + textLen.toString());
rslt += assert(this != getParagraph().getLastLeaf() || (_text.length >= 1 && _text.substr(_text.length-1,1) == SpanElement.kParagraphTerminator),"last span in paragraph must end with terminator");
return rslt;
}
}
}