////////////////////////////////////////////////////////////////////////////////
//
// 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.edit
{
import flashx.textLayout.debug.assert;
import flashx.textLayout.elements.ContainerFormattedElement;
import flashx.textLayout.elements.DivElement;
import flashx.textLayout.elements.FlowElement;
import flashx.textLayout.elements.FlowGroupElement;
import flashx.textLayout.elements.FlowLeafElement;
import flashx.textLayout.elements.LinkElement;
import flashx.textLayout.elements.ParagraphElement;
import flashx.textLayout.elements.SpanElement;
import flashx.textLayout.elements.SubParagraphGroupElement;
import flashx.textLayout.elements.TCYElement;
import flashx.textLayout.elements.TextFlow;
import flashx.textLayout.tlf_internal;
use namespace tlf_internal;
[ExcludeClass]
/**
* Encapsulates all methods necessary for dynamic editing of a text. The methods are all static member functions of this class.
* @private - because we can't make it tlf_internal. Used by the operations package
*/
public class TextFlowEdit
{
private static function deleteRange(theFlow:FlowGroupElement, startPos:int, endPos:int):int
{
var curFlowElementIdx:int = 0;
var curFlowElement:FlowElement;
var needToMergeWhenDone:Boolean = false;
var relStart:int = 0;
var relEnd:int = 0;
var s:SpanElement;
var processedAnElement:Boolean = false;
var totalItemsDeleted:int = 0;
var curNumDeleted:int = 0;
var tempFlowElement:FlowElement;
var numItems:int;
// do the middle all in one big block
var beginBlockDeleteIdx:int = -1;
var blockDeleteNumElements:int;
var blockDeleteLength:int;
while (curFlowElementIdx < theFlow.numChildren)
{
curFlowElement = theFlow.getChildAt(curFlowElementIdx);
relStart = startPos - curFlowElement.parentRelativeStart;
if (relStart < 0) relStart = 0;
relEnd = endPos - curFlowElement.parentRelativeStart;
if ((relStart < curFlowElement.textLength) && (relEnd > 0))
{
//at least partially selected
if ((relStart <= 0) && ((relEnd > curFlowElement.textLength) || ((relEnd >= curFlowElement.textLength) && (curFlowElement is ParagraphElement))))
{
//completely selected
// If the last character of a paragraph is part of the span, it won't get deleted. We will skip over it for now, and may delete it later as
// part of a paragraph merge.
curNumDeleted = curFlowElement.textLength;
var skippingTerminator:Boolean = curFlowElementIdx == theFlow.numChildren - 1 && (curFlowElement is SpanElement) && (theFlow is ParagraphElement);
if (skippingTerminator/* || !(theFlow is TextFlow)*/)
{
if (beginBlockDeleteIdx != -1)
{
theFlow.replaceChildren(beginBlockDeleteIdx,beginBlockDeleteIdx+blockDeleteNumElements);
curFlowElementIdx -= blockDeleteNumElements;
totalItemsDeleted += blockDeleteLength;
endPos -= blockDeleteLength;
beginBlockDeleteIdx = -1;
}
theFlow.replaceChildren(curFlowElementIdx, curFlowElementIdx + 1, null);
// if (skippingTerminator)
++curFlowElementIdx;
totalItemsDeleted += curNumDeleted;
endPos -= curNumDeleted;
}
else
{
// setup for a block delete
if (beginBlockDeleteIdx == -1)
{
beginBlockDeleteIdx = curFlowElementIdx;
blockDeleteNumElements = 0;
blockDeleteLength = 0;
}
blockDeleteNumElements++;
blockDeleteLength += curNumDeleted;
++curFlowElementIdx;
}
}
else
{ //not completely selected
if (beginBlockDeleteIdx != -1)
{
theFlow.replaceChildren(beginBlockDeleteIdx,beginBlockDeleteIdx+blockDeleteNumElements);
curFlowElementIdx -= blockDeleteNumElements;
totalItemsDeleted += blockDeleteLength;
endPos -= blockDeleteLength;
beginBlockDeleteIdx = -1;
}
if (curFlowElement is SpanElement)
{
s = curFlowElement as SpanElement;
if(relEnd > s.textLength)
relEnd = s.textLength;
s.replaceText(relStart, relEnd, "");
curNumDeleted = (relEnd - relStart);
totalItemsDeleted += curNumDeleted;
endPos -= curNumDeleted;
} else if (!(curFlowElement is FlowGroupElement))
{
curNumDeleted = curFlowElement.textLength;
totalItemsDeleted += curFlowElement.textLength;
endPos -= curNumDeleted;
theFlow.replaceChildren(curFlowElementIdx, curFlowElementIdx + 1, null);
} else { //it must be a FlowGroupElement of some kind
if ((!processedAnElement) && (relEnd >= curFlowElement.textLength))
{
if (curFlowElement is ParagraphElement)
needToMergeWhenDone = true;
else if (curFlowElement is FlowGroupElement)
{
numItems = (curFlowElement as FlowGroupElement).numChildren;
if (numItems > 0)
{
tempFlowElement = (curFlowElement as FlowGroupElement).getChildAt(numItems - 1);
if (tempFlowElement is ParagraphElement)
{
needToMergeWhenDone = true;
}
}
}
}
curNumDeleted = TextFlowEdit.deleteRange(curFlowElement as FlowGroupElement, relStart, relEnd);
totalItemsDeleted += curNumDeleted;
endPos -= curNumDeleted;
if (needToMergeWhenDone == true)
endPos++;
if (!(curFlowElement is ParagraphElement))
needToMergeWhenDone = false;
}
if (processedAnElement)
{
break;
}
curFlowElementIdx++;
}
processedAnElement = true;
} else if (processedAnElement)
{
break;
} else {
curFlowElementIdx++;
}
}
if (beginBlockDeleteIdx != -1)
{
theFlow.replaceChildren(beginBlockDeleteIdx,beginBlockDeleteIdx+blockDeleteNumElements);
curFlowElementIdx -= blockDeleteNumElements;
totalItemsDeleted += blockDeleteLength;
endPos -= blockDeleteLength;
}
if (needToMergeWhenDone)
{
joinNextParagraph(ParagraphElement(curFlowElement.getPreviousSibling()));
}
return totalItemsDeleted;
}
private static function isFlowElementInArray(arr:Array, fl:FlowElement):Boolean
{
if (arr != null)
{
var arrLen:int = arr.length;
var currPos:int = 0;
while (currPos < arrLen)
{
if (arr[currPos] == fl)
{
return true;
}
currPos++;
}
}
return false;
}
private static function getContainer(flEl:FlowElement):ContainerFormattedElement
{
while (!(flEl.parent is ContainerFormattedElement))
{
flEl = flEl.parent;
}
return flEl.parent as ContainerFormattedElement;
}
private static function isInsertableItem(flItem:FlowElement, missingBeginElements:Array, missingEndElements:Array):Boolean
{
return ((flItem is ParagraphElement) ||
(!TextFlowEdit.isFlowElementInArray(missingBeginElements, flItem) &&
!TextFlowEdit.isFlowElementInArray(missingEndElements, flItem)));
}
private static function putDivAtEndOfContainer(container:ContainerFormattedElement):DivElement
{
var tempDiv:DivElement = new DivElement();
var tempPar:ParagraphElement = new ParagraphElement();
tempPar.replaceChildren(0, 0, new SpanElement());
tempDiv.replaceChildren(0, 0, tempPar);
container.replaceChildren(container.numChildren, container.numChildren, tempDiv);
return tempDiv;
}
private static function putDivAtEndOfContainerAndInsertTextFlow(theFlow:TextFlow, pos:int, insertedTextFlow:FlowGroupElement, missingBeginElements:Array, missingEndElements:Array, separatorArray:Array):int
{
var nextInsertionPosition:int = pos;
var insertContainer:ContainerFormattedElement = TextFlowEdit.getContainer(theFlow.findAbsoluteParagraph(nextInsertionPosition));
var tempDiv:DivElement = TextFlowEdit.putDivAtEndOfContainer(insertContainer);
separatorArray.push(tempDiv);
var childArray:Array = insertedTextFlow.mxmlChildren;
insertedTextFlow.replaceChildren(0, insertedTextFlow.numChildren); // removing them from the old parent in a block is much faster
for each (var tempFlChild:FlowElement in childArray)
nextInsertionPosition = TextFlowEdit.insertTextFlow(theFlow, nextInsertionPosition, tempFlChild as FlowGroupElement, missingBeginElements, missingEndElements, separatorArray);
var elementIdx:int = tempDiv.parent.getChildIndex(tempDiv);
tempDiv.parent.replaceChildren(elementIdx, elementIdx + 1, null);
separatorArray.pop();
return nextInsertionPosition;
}
private static function isContainerSeparator(fl:FlowElement, separatorArray:Array):Boolean
{
var i:int = 0;
var numItemsInArray:int = separatorArray.length;
while (i < numItemsInArray)
{
if (separatorArray[i] == fl)
{
return true;
}
i++;
}
return false;
}
private static var processedFirstFlowElement:Boolean = false;
private static function insertTextFlow(theFlow:TextFlow, pos:int, insertedTextFlow:FlowGroupElement, missingBeginElementsInFlow:Array = null, missingEndElementsInFlow:Array = null, separatorArray:Array = null):int
{
var nextInsertionPosition:int = pos;
if (!TextFlowEdit.isInsertableItem(insertedTextFlow, missingBeginElementsInFlow, missingEndElementsInFlow) ||
(insertedTextFlow is TextFlow))
{
if (insertedTextFlow is TextFlow)
{
processedFirstFlowElement = false;
var tempDiv:DivElement = TextFlowEdit.putDivAtEndOfContainer(theFlow as ContainerFormattedElement);
separatorArray = new Array();
separatorArray.push(tempDiv);
}
var tempFlChild:FlowElement = insertedTextFlow.getChildAt(0);
if (TextFlowEdit.isInsertableItem(tempFlChild, missingBeginElementsInFlow, missingEndElementsInFlow))
{
nextInsertionPosition = TextFlowEdit.putDivAtEndOfContainerAndInsertTextFlow(theFlow, nextInsertionPosition, insertedTextFlow, missingBeginElementsInFlow, missingEndElementsInFlow, separatorArray);
} else {
while (insertedTextFlow.numChildren > 0)
{
tempFlChild = insertedTextFlow.getChildAt(0);
insertedTextFlow.replaceChildren(0, 1, null);
nextInsertionPosition = TextFlowEdit.insertTextFlow(theFlow, nextInsertionPosition, tempFlChild as FlowGroupElement, missingBeginElementsInFlow, missingEndElementsInFlow, separatorArray);
}
}
if (insertedTextFlow is TextFlow)
{
theFlow.replaceChildren(theFlow.numChildren - 1, theFlow.numChildren, null);
if (nextInsertionPosition >= theFlow.textLength)
{
nextInsertionPosition = theFlow.textLength - 1;
}
separatorArray.pop();
}
} else {
//if you are inserting at the very end of a paragraph, bump up the position
//by one. Otherwise, if you are not at the end of the paragraph, split at
//the position, and then move up by 1.
var leafEl:FlowLeafElement = null;
if (pos > 0) leafEl = theFlow.findLeaf(pos - 1);
var para:ParagraphElement = theFlow.findAbsoluteParagraph(pos);
var paraSplitIndex:int = pos - para.getAbsoluteStart();
var flowElIndex:int = para.parent.getChildIndex(para);
var okToMergeWithAfter:Boolean = true;
if (paraSplitIndex > 0)
{
if (paraSplitIndex < (para.textLength - 1))
{
para.splitAtPosition(paraSplitIndex);
} else if ((insertedTextFlow.textLength == 1) && !processedFirstFlowElement) {
if (TextFlowEdit.isFlowElementInArray(missingEndElementsInFlow, insertedTextFlow) ||
TextFlowEdit.isFlowElementInArray(missingBeginElementsInFlow, insertedTextFlow))
{
processedFirstFlowElement = true;
return nextInsertionPosition;
} else {
para.splitAtPosition(paraSplitIndex);
}
} else {
okToMergeWithAfter = false;
}
pos++;
} else { //no split done. So we want to insert after the previous paragraph.
flowElIndex = flowElIndex - 1;
}
//insert the insertedTextFlow after the paragraph at paragraphIndex
var paragraphContainer:FlowGroupElement = para.parent;
if (TextFlowEdit.isContainerSeparator(paragraphContainer, separatorArray))
{
flowElIndex = paragraphContainer.parent.getChildIndex(paragraphContainer);
paragraphContainer = paragraphContainer.parent;
flowElIndex--;
}
paragraphContainer.replaceChildren(flowElIndex + 1, flowElIndex + 1, insertedTextFlow);
nextInsertionPosition = pos + insertedTextFlow.textLength;
if (insertedTextFlow is ParagraphElement)
{
var missingEnd:Boolean = TextFlowEdit.isFlowElementInArray(missingEndElementsInFlow, insertedTextFlow);
if (okToMergeWithAfter && missingEnd)
{
// Merge the paragraph with what comes next. If the inserted paragraph is inserted to the middle or end of the paragraph,
// then merge the next paragraph into the inserted paragraph. If we're inserting to the start of the paragraph, merge
// the inserted paragraph into the next paragraph, so that the original host paragraph maintains its format settings.
if (paraSplitIndex == 0)
{
if (joinToNextParagraph(ParagraphElement(insertedTextFlow)))
nextInsertionPosition--;
}
else if (joinNextParagraph(ParagraphElement(insertedTextFlow)))
nextInsertionPosition--;
}
if (!processedFirstFlowElement)
{
if (paraSplitIndex > 0)
{
var prevSibling:ParagraphElement = insertedTextFlow.getPreviousSibling() as ParagraphElement;
if (prevSibling && joinNextParagraph(prevSibling))
nextInsertionPosition--;
}
}
if (missingEnd)
{
var absolutePar:ParagraphElement = paragraphContainer.getTextFlow().findAbsoluteParagraph(nextInsertionPosition);
var absoluteParIndex:int = absolutePar.getAbsoluteStart();
if ((nextInsertionPosition - absolutePar.getAbsoluteStart()) == 0)
{
nextInsertionPosition--;
}
}
}
processedFirstFlowElement = true;
}
return nextInsertionPosition;
}
/**
* Replaces the range of text positions that the startPos
and
* endPos
parameters specify with the newTextFlow
parameter in
* theFlow
.
*
To delete elements, pass null
for newTextFlow
.
To insert an element, pass the same value for startPos
and endPos
.
*
To insert a newline after the newTextFlow
is inserted, pass in
* true
for insertParAfter
The new element will be inserted before the specified index.
*To append the TextFlow, pass theFlow.length
for startPos
and endPos
.
theFlow
at the indicies specified by startPos
and
* endPos
. The newSPB
will take ownership of any FlowElements within the range and will split them
* as needed. If the parent of the FlowGroupElement indicated by startPos
is the same as spgClass
then
* the method fails and returns false because a spg cannot own children of the same class as itself. Any spg of type spgClass
* found within the indicies, however, is subsumed into newSPB
, effectively replacing it.
*
* @param theFlow:TextFlow - The TextFlow that is the destination for the newSPB
* @param startPos:int - The absolute index value of the first position of the range in the TextFlow to perform the insertion.
* @param endPos:int - The index value following the end position of the range in the TextFlow to perform the insertion.
* @param newSPB:SubParagraphGroupElement - The new SubParagraphElement which is to be added into theFlow.
* @param spgClass:Class - the class of the fbe we intend to add.
*
* Examples: Simple and complex where insertion is of spgClass
b. Selection is l~o
* 1) ghijklmnop
* 2) ghijklmnop
* 3) ghijkelem
at the relative index of splitPos
. If splitSubBlockContents
* is true, split the contents of elem
if it is a SubParagraphGroupElement, otherwise just split elem
*
* @param elem:FlowElement - the FlowElement to split
* @param splitPos:int - The elem relative index indicating where to split
* @param splitSubBlockContents:Boolean - boolean indicating whether a SubParagraphGroupElement is to be split OR that it's contents
* should be split. For example, are we splitting a link or are we splitting the child of the link
*
* splitPos
indicated index between C and D, then if splitSubBlockContents
equals true,
* result is:
*
* splitSubBlockContents
equals false, result is:
*
* elementIdx
of fbe
, iterate
* through the elements untill we find the one located at the aboslute index of startIdx
. Upon
* locating the child, split either the element itself OR its children based on the value of splitSubBlockContents
*
* @param fbe:FlowGroupElement - the FBE into which the newSPB is being inserted.
* @param elementIdx:int - The index into the fbe's
child list to start
* @param startIdx:int - The absolute index value into the TextFlow.
* @param splitSubBlockContents:Boolean - boolean indicating whether a subElement is to be split OR that it's contents
* should be split. For example, are we splitting a link or are we splitting the child of the link
*
* ZYXABCDEF123
* * if we are inserting a TCY into the link, splitSubBlockContents should be false. We want to split the span ABCDEF such that result is: *ZYXAB
ZYXABCDEF123
* * @return int - the index of the last child offbe
processed.
*/
tlf_internal static function findAndSplitElement(fbe:FlowGroupElement, elementIdx:int, startIdx:int, splitSubBlockContents:Boolean):int
{
var curFlowEl:FlowElement = null;
var curIndexInPar:int = startIdx - fbe.getAbsoluteStart();
while(elementIdx < fbe.numChildren)
{
curFlowEl = fbe.getChildAt(elementIdx);
if (curIndexInPar == curFlowEl.parentRelativeStart)
return elementIdx;
if ((curIndexInPar > curFlowEl.parentRelativeStart) && (curIndexInPar < curFlowEl.parentRelativeEnd))
{
splitElement(curFlowEl, curIndexInPar - curFlowEl.parentRelativeStart, splitSubBlockContents);
}
++elementIdx;
}
return elementIdx;
}
/**
* @private
* subsumeElementsToSPBlock - incorporates all elements of parentFBE
into
* the newSPB
between the curPos
and endPos
. If a child of
* parentFBE
is of type spgClass
then the child's contents are removed from the child,
* added to the newSPB
, the child is then removed from the parentFBE
*
* @param parentFBE:FlowGroupElement - the FBE into which the newSPB is being inserted.
* @param startPos:int - The index value of the first position of the replacement range in the TextFlow.
* @param endPos:int - The index value following the end position of the replacement range in the TextFlow.
* @param newSPB:SubParagraphGroupElement - the new SubParagraphGroupElement we intend to insert.
* @param spgClass:Class - the class of the fbe we intend to insert.
*
* @return int - the aboslute index in the text flow after insertion.
*
* Examples: Simple and complex where insertion is of spgClass b. Selection is l~o
* 1) ghijklmnop
* 2) ghijklmnop
*
* parentFBE =
* elementIdx = 1) 2, 2) 3
* curPos = 5
* endPos = 9
* newSPB is of type
*/
tlf_internal static function subsumeElementsToSPBlock(parentFBE:FlowGroupElement, elementIdx:int, curPos:int, endPos:int, newSPB:SubParagraphGroupElement, spgClass:Class):int
{
var curFlowEl:FlowElement = null;
//if we have an invalid index, then skip out. elementIdx will always point one
//element beyond the one we are inserting....
if(elementIdx >= parentFBE.numChildren)
return curPos;
while (curPos < endPos)
{
//running example: curFlowEl is the element immediately after the inserted newSPB:
// 1) ghijklmnop
// points to span-lmnop
// 2) ghijklmnop
// points to b-lm
curFlowEl = parentFBE.getChildAt(elementIdx);
//if the curFlowEl is of the Class we are adding (spgClass), and the entire thing is selected,
//then we are adding the entire block, but not spliting it - perform the split on the next block
//I think this can be safely removed from here as ownership of contents is handled below.
//leaving in commented out code in case we need to revert - gak 05.01.08
/* if(curFlowEl is spgClass && curPos == curFlowEl.getAbsoluteStart() && curFlowEl.getAbsoluteStart() + curFlowEl.textLength <= endPos)
{
curPos = parentFBE.getAbsoluteStart() + parentFBE.textLength;
continue;
}*/
//if the endPos is less than the length of the curFlowEl, then we need to split it.
//if the curFlowEl is NOT of class type spgClass, then we need to break it
//
//Use case: splitting a link in two (or three as will be the result with head and tail sharing
//attributes...
//running example 1 hits this, but 2 does not. Using variation of 2:
//
// example: 1) ghijklmnop
// 2a) foobar - selection: from o~a
//
// after this code:
// 1) ghijklmnop
// 2a) foobaor
if ((curPos + curFlowEl.textLength) > endPos)
{
splitElement(curFlowEl, endPos - curFlowEl.getAbsoluteStart(), !(curFlowEl is spgClass)); //changed to curFlowEl from newSPB as newSPB should be of type spgClass
}
//add the length before replacing the elements
curPos += curFlowEl.textLength;
//running example: after parentFBE.replaceChildren
//
// 1) curFlowEl = lmno
// ghijk{curFlowEl}p
//
// 2) curFlowEl = lm
// ghijk{curFlowEl}nop
parentFBE.replaceChildren(elementIdx, elementIdx + 1, null);
//if the curFlowEl is of type spgClass, then we need to take its children and
//add them to the newSPB because a spg cannot contain a child of the same class
//as itself
//
// exmaple: 2) curFlowEl = lm
if (curFlowEl is spgClass)
{
var subBlock:SubParagraphGroupElement = curFlowEl as SubParagraphGroupElement;
//elementCount == 1 - lm
while (subBlock.numChildren > 0)
{
//fe[0] = lm
var fe:FlowElement = subBlock.getChildAt(0);
//
subBlock.replaceChildren(0, 1, null);
//lm
newSPB.replaceChildren(newSPB.numChildren, newSPB.numChildren, fe);
}
//when compelete, example 2 is:
//2) ghijklmnop
}
else
{
//example 1, curFlowEl is lmno, so this is not hit
//
// extending element from foo~other
// foobarother
// curFlowEl = barother
//
// since is a spg, we need to walk it's contents and remove any elements
if(curFlowEl is SubParagraphGroupElement)
{
//we need to dive into this spgClass and remove any fbes of type spgClass
//pass in the curFlowEl as the newSPB, remove any spgs of type spgClass, then
//perform the replace on the newSPB passed in here
//
//ignore the return value of the recursive call as the length has already been
//accounted for above
flushSPBlock(curFlowEl as SubParagraphGroupElement, spgClass);
}
newSPB.replaceChildren(newSPB.numChildren, newSPB.numChildren, curFlowEl);
if(newSPB.numChildren == 1 && curFlowEl is SubParagraphGroupElement)
{
var childSPGE:SubParagraphGroupElement = curFlowEl as SubParagraphGroupElement;
//running example:
//a.precedence = 800, tcy.precedence = kMinSPGEPrecedence
//this = fooBar
//childSPGE = fooBar
if(childSPGE.textLength == newSPB.textLength && (curPos >= endPos))
{
CONFIG::debug { assert(childSPGE.precedence != newSPB.precedence, "normalizeRange found two equal SPGEs"); }
//if the child's precedence is higher than mine, I need to swap
if(childSPGE.precedence > newSPB.precedence)
{
//first, remove the child
//this =
newSPB.replaceChildren(0,1,null);
//we need to flop this object for the child
while(childSPGE.numChildren > 0)
{
//tempFE = fooBar
var tempFE:FlowElement = childSPGE.getChildAt(0);
//child =
childSPGE.replaceChildren(0,1,null);
//this = fooBar
newSPB.replaceChildren(newSPB.numChildren, newSPB.numChildren, tempFE);
}
var myIdx:int = newSPB.parent.getChildIndex(newSPB);
CONFIG::debug{ assert(myIdx >= 0, "Invalid index! How can a SubParagraphGroupElement normalizing not have a parent!"); }
//add childSPGE in my place
newSPB.parent.replaceChildren(myIdx, myIdx + 1, childSPGE)
//childSPGE = fooBar
childSPGE.replaceChildren(0,0,newSPB);
}
}
}
}
}
return curPos;
}
/**
* @private
* findAndRemoveFlowGroupElement
*
* @param theFlow The TextFlow that is containing the elements to remove.
* @param startPos The index value of the first position of the range in the TextFlow where we want to perform removal.
* @param endPos The index value following the end position of the range in the TextFlow where we want to perform removal.
* @param fbeClass Class the class of the fbe we intend to remove.
*
* Walks through the elements of theFlow
looking for any FlowGroupElement of type fbeClass
* On finding one, it removes the FBE's contents and adds them back into the FBE's parent. If the class of object is
* embedded within another spg and this removal would break the parent spg, then the method does nothing.
*
* Example:
* ABCDEF GHI
* Selection is on E and removal of link is attempted.
* Because E is a child of a spg (tcy), and removing the link from E would split the parent spg (link),
* the action is disallowed.
*
* Running example:
* 1) foo bar
* @return Boolean - true if items are removed or none are found. false if operation is illegal.
*/
tlf_internal static function findAndRemoveFlowGroupElement(theFlow:TextFlow, startPos:int, endPos:int, fbeClass:Class):Boolean
{
var curPos:int = startPos;
var curEl:FlowElement;
//walk through the elements
while (curPos < endPos)
{
var containerFBE:FlowGroupElement = theFlow.findAbsoluteFlowGroupElement(curPos);
//if the start of the parent is the same as the start of the current containerFBE, then
//we potentially have the wrong object. We need to walk up the parents until we get to
//the one which starts at our start AND is the topmost object at that index.
//example: foo bar - getting the object at "f" will yield the element, not
while(containerFBE.parent && containerFBE.parent.getAbsoluteStart() == containerFBE.getAbsoluteStart() &&
!(containerFBE.parent is ParagraphElement)) //don't go beyond paragraph
{
containerFBE = containerFBE.parent;
}
//if the absoluteFBE is the item we are trying to remove, we need to work with its parent, so
//reassign containerFBE. For example, if an entire link were selected, we'd need to get it's parent to
//perform the removal
if(containerFBE is fbeClass)
containerFBE = containerFBE.parent;
//before processing this any further, we need to make sure that we are not
//splitting a spg which is contained within the same type of spg as the curFBE's parent.
//for example, if we had a tcyElement inside a linkElement, then we cannot allow a link element
//to be broken within the tcyElement as the link would have to split the TCY.
var ancestorOfFBE:FlowGroupElement = containerFBE.parent;
while(ancestorOfFBE != null && !(ancestorOfFBE is fbeClass))
{
if(ancestorOfFBE.parent is fbeClass)
{
return false;
}
ancestorOfFBE = ancestorOfFBE.parent;
}
//if this is a sbe block contained in another sbe, and it is entire within the
//selection bounds, we need to use the parent sbe's container. If it is splitting
//the child sbe, we don't allow this and it is handled later...
var containerFBEStart:int = containerFBE.getAbsoluteStart();
if(ancestorOfFBE is fbeClass && (containerFBEStart >= curPos && containerFBEStart + containerFBE.textLength <= endPos))
containerFBE = ancestorOfFBE.parent;
var childIdx:int = containerFBE.findChildIndexAtPosition(curPos - containerFBEStart);
curEl = containerFBE.getChildAt(childIdx);
if(curEl is fbeClass)
{
CONFIG::debug{ assert(curEl is SubParagraphGroupElement, "Wrong FBE type! Trying to remove illeage FBE!") };
var curFBE:FlowGroupElement = curEl as FlowGroupElement;
//get it's parent and the index of the curFBE
var parentBlock:FlowGroupElement = curFBE.parent;
var idxInParent:int = parentBlock.getChildIndex(curFBE);
//if the curPos is not at the head of the SPB, then we need to split it here
//curFBE will point to the FBE starting at curPos
if(curPos > curFBE.getAbsoluteStart())
{
splitElement(curFBE, curPos - curFBE.getAbsoluteStart(), false);
curPos = curFBE.getAbsoluteStart() + curFBE.textLength;
continue;
}
//if curFBE goes beyond the endPos, then we need to split off the tail.
if (curFBE.getAbsoluteStart() + curFBE.textLength > endPos)
{
splitElement(curFBE, endPos - curFBE.getAbsoluteStart(), false);
}
//apply the length of the curFBE to the curPos tracker. Do this before
//removing the contents or it will be 0!
curPos = curFBE.getAbsoluteStart() + curFBE.textLength;
//walk all the contents of the FBE into it's parent container
while (curFBE.numChildren > 0)
{
var childFE:FlowElement = curFBE.getChildAt(0);
curFBE.replaceChildren(0, 1, null);
parentBlock.replaceChildren(idxInParent, idxInParent, childFE);
idxInParent++;
}
//remove the curFBE
parentBlock.replaceChildren(idxInParent, idxInParent + 1, null);
}
else if(curEl is SubParagraphGroupElement) //check all the parents...
{
var curSPB:SubParagraphGroupElement = SubParagraphGroupElement(curEl);
if(curSPB.numChildren == 1)
curPos = curSPB.getAbsoluteStart() + curSPB.textLength;
else
{
curEl = curSPB.getChildAt(curSPB.findChildIndexAtPosition(curPos - curSPB.getAbsoluteStart()));
curPos = curEl.getAbsoluteStart() + curEl.textLength;
}
}
else
{
//the current block isn't the type we're looking for, so just go to the end of the
//FlowElement and continue
curPos = curEl.getAbsoluteStart() + curEl.textLength;
}
}
return true;
}
/**
* @private
* canInsertSPBlock
*
* validate that we a valid selection to allow for insertion of a subBlock. The rules are as
* follows:
* endPos > start
* the new block will not span multiple paragraphs
* if the block is going into a SubParagraphGroupElement, it must not split the block:
* example: Text - ABCDEFG with a link on CDE
* legal new Block - D, CD, CDE, [n-chars]CDE[n1-chars]
* illegal new Block - [1 + n-chars]C[D], [D]E[1 + n-chars]
* exception - if the newBlock is the same class as the one we are trying to split
* then we can truncate the original and add its contents to the new one, or extend it
* as appropriate
*
* @param theFlow The TextFlow that is containing the elements to validate.
* @param startPos The index value of the first position of the range in the TextFlow to test.
* @param endPos The index value following the end position of the range in the TextFlow to test.
* @param blockClass Class the class of the fbe we intend to insert.
*/
tlf_internal static function canInsertSPBlock(theFlow:TextFlow, startPos:int, endPos:int, blockClass:Class):Boolean
{
if(endPos <= startPos)
return false;
var anchorFBE:FlowGroupElement = theFlow.findAbsoluteFlowGroupElement(startPos);
if(anchorFBE.getParentByType(blockClass))
anchorFBE = anchorFBE.getParentByType(blockClass) as FlowGroupElement;
var tailFBE:FlowGroupElement = theFlow.findAbsoluteFlowGroupElement(endPos - 1);
if(tailFBE.getParentByType(blockClass))
tailFBE = tailFBE.getParentByType(blockClass) as FlowGroupElement;
//if these are the same FBEs then we are safe to insert a SubParagraphGroupElement
if(anchorFBE == tailFBE)
return true;
//make sure that the two FBEs belong to the same paragraph!
else if(anchorFBE.getParagraph() != tailFBE.getParagraph())
return false;
else if(anchorFBE is blockClass && tailFBE is blockClass)//they're the same class, OK to merge, split, etc...
return true;
else if(anchorFBE is SubParagraphGroupElement && !(anchorFBE is blockClass))
{
var anchorStart:int = anchorFBE.getAbsoluteStart();
if(startPos > anchorStart && endPos > anchorStart + anchorFBE.textLength)
return false;
}
else if((anchorFBE.parent is SubParagraphGroupElement || tailFBE.parent is SubParagraphGroupElement)
&& anchorFBE.parent != tailFBE.parent)
{
//if either FBE parent is a SPGE and they are not the same, prevent the split.
return false;
}
//if we got here, then the anchorFBE is OK, check the tail. If endPos is pointing to the
//0th character of a FlowGroupElement, we don't need to worry about the tail.
if(tailFBE is SubParagraphGroupElement && !(tailFBE is blockClass) && endPos > tailFBE.getAbsoluteStart())
{
var tailStart:int = tailFBE.getAbsoluteStart();
if(startPos < tailStart && endPos < tailStart + tailFBE.textLength)
return false;
}
return true;
}
/**
* @private flushSPBlock recursively walk a spg looking for elements of type spgClass. On finding one,
* remove it's children and then remove the object itself. Since spg's cannot hold children of the same type
* as themselves, recursion is only needed for spg's of a class other than that of spgClass.
*
* example: subPB = barother extending an element to include all of "other"
*/
tlf_internal static function flushSPBlock(subPB:SubParagraphGroupElement, spgClass:Class):void
{
var subParaIter:int = 0;
//example, subPB has 2 elements, bar and other
while(subParaIter < subPB.numChildren)
{
//subParaIter == 0, subFE = bar skip the FE and move to next
//subParaIter == 1, subFE = other - is a spgClass
var subFE:FlowElement = subPB.getChildAt(subParaIter);
if(subFE is spgClass)
{
//subParaIter == 1, subFE = other
var subChildFBE:FlowGroupElement = subFE as FlowGroupElement;
while(subChildFBE.numChildren > 0)
{
//subFEChild = other
var subFEChild:FlowElement = subChildFBE.getChildAt(0);
//subFEChild =
subChildFBE.replaceChildren(0, 1, null);
//subPB = barother
subPB.replaceChildren(subParaIter, subParaIter, subFEChild);
}
//increment so that subParaIter points to the element we just emptied
++subParaIter;
//remove the empty child
//subPB = barother
subPB.replaceChildren(subParaIter, subParaIter + 1, null);
}
else if(subFE is SubParagraphGroupElement)
{
flushSPBlock(subFE as SubParagraphGroupElement, spgClass);
++subParaIter;
}
else
++subParaIter;//go to next child
}
}
/** Joins this paragraph's next sibling to this if it is a paragraph */
static public function joinNextParagraph(para:ParagraphElement):Boolean
{
if (para && para.parent)
{
var myidx:int = para.parent.getChildIndex(para);
if (myidx != para.parent.numChildren-1)
{
// right now, you can only merge with other paragraphs
var sibParagraph:ParagraphElement = para.parent.getChildAt(myidx+1) as ParagraphElement;
if (sibParagraph)
{
while (sibParagraph.numChildren > 0)
{
var curFlowElement:FlowElement = sibParagraph.getChildAt(0);
sibParagraph.replaceChildren(0, 1, null);
para.replaceChildren(para.numChildren, para.numChildren, curFlowElement);
}
para.parent.replaceChildren(myidx+1, myidx+2, null);
return true;
}
}
}
return false;
}
/** Joins this paragraph's next sibling to this if it is a paragraph */
static public function joinToNextParagraph(para:ParagraphElement):Boolean
{
if (para && para.parent)
{
var myidx:int = para.parent.getChildIndex(para);
if (myidx != para.parent.numChildren-1)
{
// right now, you can only merge with other paragraphs
var sibParagraph:ParagraphElement = para.parent.getChildAt(myidx+1) as ParagraphElement;
if (sibParagraph)
{
// Add the first paragraph's children to the front of the next paragraph's child list
var addAtIndex:int = 0;
while (para.numChildren > 0)
{
var curFlowElement:FlowElement = para.getChildAt(0);
para.replaceChildren(0, 1, null);
sibParagraph.replaceChildren(addAtIndex, addAtIndex, curFlowElement);
++addAtIndex;
}
para.parent.replaceChildren(myidx, myidx+1, null);
return true;
}
}
}
return false;
}
}
}