//////////////////////////////////////////////////////////////////////////////// // // 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.

* * @param theFlow The TextFlow that is being inserted into. * @param startPos The index value of the first position of the replacement range in the TextFlow. * @param endPos The index value following the end position of the replacement range in the TextFlow. * @param newTextFlow The TextFlow to be merged into theFlow. * @param missingBeginElementsInFlow Array indicating all the elements within the TextFlow that have their beginning parts chopped off. * @param missingEndElementsInFlow Array indicating all the elements within the TextFlow that have their ending parts chopped off. */ public static function replaceRange(theFlow:TextFlow, startPos:int, endPos:int, textScrap:TextScrap = null):int { var nextInsertPosition:int = startPos; if (endPos > startPos) { deleteRange(theFlow, startPos, endPos); } if (textScrap != null) { textScrap = textScrap.clone(); // make a copy so the original isn't mutated nextInsertPosition = insertTextFlow(theFlow, startPos, textScrap.textFlow, textScrap.beginMissingArray, textScrap.endMissingArray); } return nextInsertPosition; } /** * Creates a copy of the TextFlow in between two positions and returns the TextFlow * within a TextScrap object. See TextScrap for more information. * @param theFlow The TextFlow that is being copied from. * @param startPos The index value of the first position of the TextFlow being copied from. * @param endPos The index value following the end position of the TextFlow being copied from. */ public static function createTextScrap(theFlow:TextFlow, startPos:int, endPos:int):TextScrap { if (!theFlow || startPos >= endPos) return null; var newTextFlow:TextFlow = theFlow.deepCopy(startPos, endPos) as TextFlow; newTextFlow.normalize(); var retTextScrap:TextScrap = new TextScrap(newTextFlow); if (newTextFlow.textLength > 0) { var fl:FlowElement = newTextFlow.getLastLeaf(); var srcElem:FlowElement = theFlow.findLeaf(startPos); var copyElem:FlowElement = newTextFlow.getFirstLeaf(); while (copyElem && srcElem) { if ((startPos - srcElem.getAbsoluteStart()) > 0) { retTextScrap.addToBeginMissing(copyElem); } copyElem = copyElem.parent; srcElem = srcElem.parent; } srcElem = theFlow.findLeaf(endPos - 1); copyElem = newTextFlow.getLastLeaf(); if ((copyElem is SpanElement) && (!(srcElem is SpanElement))) { copyElem = newTextFlow.findLeaf(newTextFlow.textLength - 2); } while (copyElem && srcElem) { if (endPos < (srcElem.getAbsoluteStart() + srcElem.textLength)) { retTextScrap.addToEndMissing(copyElem); } copyElem = copyElem.parent; srcElem = srcElem.parent; } return retTextScrap; } return null; } /** * Creates a TCY run out of the selected positions. * @param theFlow The TextFlow of interest. * @param startPos The index value of the first position of the TextFlow to be turned into a TCY run. * @param endPos The index value following the end position of the TextFlow to be turned into a TCY run. */ public static function makeTCY(theFlow:TextFlow, startPos:int, endPos:int):Boolean { var madeTCY:Boolean = true; var curPara:ParagraphElement = theFlow.findAbsoluteParagraph(startPos); if(!curPara) return false; while(curPara) { var paraEnd:int = curPara.getAbsoluteStart() + curPara.textLength; var curEndPos:int = Math.min(paraEnd, endPos); //we have an entire para selected and the para only contains a kParaTerminator char, which cannot be //made into TCY. if(canInsertSPBlock(theFlow, startPos, curEndPos, TCYElement) && curPara.textLength > 1) { var new_tcyElem:TCYElement = new TCYElement(); //don't hide an error! if(madeTCY) madeTCY = insertNewSPBlock(theFlow, startPos, curEndPos, new_tcyElem, TCYElement); else insertNewSPBlock(theFlow, startPos, curEndPos, new_tcyElem, TCYElement); } else madeTCY = false; if(paraEnd < endPos) { curPara = theFlow.findAbsoluteParagraph(curEndPos); startPos = curEndPos; } else curPara = null; } return madeTCY; } /** * Creates one or more LinkElements out of the selected positions. It will go through * every paragraph within the selected position and make links. * @param theFlow The TextFlow of interest. * @param startPos The index value of the first position of the TextFlow to be turned into a link. * @param endPos The index value following the end position of the TextFlow to be turned into a link. * @param urlString The url string to be associated with the link. */ public static function makeLink(theFlow:TextFlow, startPos:int, endPos:int, urlString:String, target:String):Boolean { var madeLink:Boolean = true; var curPara:ParagraphElement = theFlow.findAbsoluteParagraph(startPos); if(!curPara) return false; while(curPara) { var paraEnd:int = curPara.getAbsoluteStart() + curPara.textLength; var curEndPos:int = Math.min(paraEnd, endPos); var linkEndPos:int = (curEndPos == paraEnd) ? (curEndPos - 1) : curEndPos; if (linkEndPos > startPos) { //if the end of the paragraph is < endPos, we are going across bounds if(!canInsertSPBlock(theFlow, startPos, linkEndPos, LinkElement)) { return false; } var newLinkElement:LinkElement = new LinkElement(); newLinkElement.href = urlString; newLinkElement.target = target; //don't hide an error! if(madeLink) madeLink = insertNewSPBlock(theFlow, startPos, linkEndPos, newLinkElement, LinkElement); else insertNewSPBlock(theFlow, startPos, linkEndPos, newLinkElement, LinkElement); } if(paraEnd < endPos) { curPara = theFlow.findAbsoluteParagraph(curEndPos); startPos = curEndPos; } else curPara = null; } return madeLink; } /** * Removes the TCY block at the selected positions. * @param theFlow The TextFlow of interest. * @param startPos The index value of the first position of the TextFlow. * @param endPos The index value following the end position of the TextFlow. */ public static function removeTCY(theFlow:TextFlow, startPos:int, endPos:int):Boolean { if (endPos <= startPos) { return false; } return findAndRemoveFlowGroupElement(theFlow, startPos, endPos, TCYElement);; } /** * Removes all LinkElements under the selected positions. It will go through * every paragraph within the selected position and remove any link. * @param theFlow The TextFlow of interest. * @param startPos The index value of the first position of the TextFlow. * @param endPos The index value following the end position of the TextFlow. */ public static function removeLink(theFlow:TextFlow, startPos:int, endPos:int):Boolean { if (endPos <= startPos) { return false; } return findAndRemoveFlowGroupElement(theFlow, startPos, endPos, LinkElement); } /** * @private * insertNewSPBlock - add a SubParagraphGroupElement (spg) to 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) ghijklmnop * */ tlf_internal static function insertNewSPBlock(theFlow:TextFlow, startPos:int, endPos:int, newSPB:SubParagraphGroupElement, spgClass:Class):Boolean { var curPos:int = startPos; var curFBE:FlowGroupElement = theFlow.findAbsoluteFlowGroupElement(curPos); var elementIdx:int = 0; CONFIG::debug{ assert(curFBE != null, "No FBE at location curPos(" + curPos + ")!");} //if we are at the last "real" glyph of the paragraph, include the terminator. var paraEl:ParagraphElement = curFBE.getParagraph(); if(endPos == (paraEl.getAbsoluteStart() + paraEl.textLength - 1)) ++endPos; //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 made within the tcyElement as the link would not function. As a rule, a SubParagraphElement //cannot own a child of the same class. // //However, if the curFBE is parented by a spg and the objects have the same start and end, then we are doing //a replace and we're not splitting the parent. Check if the bounds are the same and if so, don't skip it... var parentStart:int = curFBE.parent.getAbsoluteStart(); var curFBEStart:int = curFBE.getAbsoluteStart(); if(curFBE.parent && curFBE.parent is spgClass && !(parentStart == curFBEStart && parentStart + curFBE.parent.textLength == curFBEStart + curFBE.textLength)) { return false; } //entire FBE is selected and is not a paragraph, get its parent. if(!(curFBE is ParagraphElement) && curPos == curFBEStart && curPos + curFBE.textLength <= endPos) { elementIdx = curFBE.parent.getChildIndex(curFBE); curFBE = curFBE.parent; } //first, if the curFBE is of the same class as the newSPB, then we need to split it to allow for insertion //of the new one IF the start position is > the start of the curFBE // //running example after this block: // 1) ghijklmnop // 2) ghijklmnop // 3) ghijklmnop - no change if(curPos >= curFBEStart) { if(!(curFBE is spgClass)) elementIdx = findAndSplitElement(curFBE, elementIdx, curPos, true); else { elementIdx = findAndSplitElement(curFBE.parent, curFBE.parent.getChildIndex(curFBE), curPos, false); curFBE = curFBE.parent; } } //now that the curFBE has been split, we want to insert the newSPB into the flow and then start absorbing the //contents... //running example after this block: // 1) ghijklmnop // 2) ghijklmnop // 3) ghijklmnop - no change // // we need another use case here where selection is entire sbp and selection runs from the head of a spg to // part way through it - so that a) does into parent and b) goes into spg // if this is case 2, then the new element must go into the parent... if(curFBE is spgClass) { curFBEStart = curFBE.getAbsoluteStart(); elementIdx = curFBE.parent.getChildIndex(curFBE); if(curPos > curFBEStart)//we're splitting the element, not replacing it... elementIdx += 1; //if the spg, curFBE is entirely selected then we want to use the parent, not the item itself. while(endPos >= curFBEStart + curFBE.textLength) { //we need access to the parent, which contains both the start and end not the FBE we just split curFBE = curFBE.parent; } curFBE.replaceChildren(elementIdx, elementIdx, newSPB); } else { //we're inserting into the curFBE curFBE.replaceChildren(elementIdx, elementIdx, newSPB); } //see subsumeElementsToSPBlock to see effects on running example subsumeElementsToSPBlock(curFBE, elementIdx + 1, curPos, endPos, newSPB, spgClass); return true; } /** * @private * splitElement - split elem 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 * * ABCDEF * * if splitPos indicated index between C and D, then if splitSubBlockContents equals true, * result is: * * ABCDEF * * if splitSubBlockContents equals false, result is: * * ABCDEF */ tlf_internal static function splitElement(elem:FlowElement, splitPos:int, splitSubBlockContents:Boolean):void { CONFIG::debug{ assert(splitPos < elem.textLength, "trying to splic FlowElement at illegal index!"); } if (elem is SpanElement) { SpanElement(elem).splitAtPosition(splitPos); } else if(elem is SubParagraphGroupElement && splitSubBlockContents) { var subBlock:SubParagraphGroupElement = SubParagraphGroupElement(elem); // Split the SpanElement of the block at splitPos. If the item at the splitPos is not a SpanElement, no action occurs. var tempElem:SpanElement = subBlock.findLeaf(splitPos) as SpanElement; if (tempElem) tempElem.splitAtPosition(splitPos - tempElem.getElementRelativeStart(subBlock)); } else if (elem is FlowGroupElement) { FlowGroupElement(elem).splitAtPosition(splitPos); } else { CONFIG::debug { assert(false, "Trying to split on an illegal FlowElement"); } } } /** * @private * findAndSplitElement - starting at the child 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: *

ZYXABCDEF123

* * if we are creating a new link from X to B, then we want the link to split and splitSubBlockContents should be false: * *

ZYXABCDEF123

* * @return int - the index of the last child of fbe 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: * ABCDEFGHI * 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) foobar * @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; } } }