devexpress-richedit
Version:
DevExpress Rich Text Editor is an advanced word-processing tool designed for working with rich text documents.
434 lines (433 loc) • 24.6 kB
JavaScript
import { Flag } from '@devexpress/utils/lib/class/flag';
import { Constants } from '@devexpress/utils/lib/constants';
import { Errors } from '@devexpress/utils/lib/errors';
import { Point } from '@devexpress/utils/lib/geometry/point';
import { FixedInterval } from '@devexpress/utils/lib/intervals/fixed';
import { EnumUtils } from '@devexpress/utils/lib/utils/enum';
import { ListUtils } from '@devexpress/utils/lib/utils/list';
import { NumberMapUtils } from '@devexpress/utils/lib/utils/map/number';
import { DocumentLayoutDetailsLevel } from '../../layout/document-layout-details-level';
import { LayoutPosition } from '../../layout/layout-position';
import { AnchoredObjectLevelType } from '../../layout/main-structures/layout-boxes/layout-anchored-object-box';
import { LayoutBoxType } from '../../layout/main-structures/layout-boxes/layout-box';
import { LayoutColumn } from '../../layout/main-structures/layout-column';
import { LayoutPageArea } from '../../layout/main-structures/layout-page-area';
import { LayoutRowStateFlags } from '../../layout/main-structures/layout-row';
import { SubDocument } from '../../model/sub-document';
import { Log } from '../../rich-utils/debug/logger/base-logger/log';
import { LogSource } from '../../rich-utils/debug/logger/base-logger/log-source';
import { LogObjToStrLayout } from '../../rich-utils/debug/logger/layout-logger/log-obj-to-str-layout';
import { ParagraphFrameCollector } from '../changes/engine/paragraph-frame-changes-collector';
import { LayoutRowBoundsCalculator } from '../floating/layout-row-bounds-manager';
import { RowFormatter } from '../row/formatter';
import { RowFormatterResultFlag } from '../row/result';
import { RowSpacingBeforeApplier, TableRowSpacingBeforeApplier } from '../row/utils/row-spacing-before-applier';
import { AddRowToTableResult, Formatter } from '../table/formatter';
import { LayoutFormatterState } from './enums';
import { LastRowInfo } from './utils/last-row-info';
import { RestartPreparer } from './utils/restart-preparer';
import { TableInfo } from '../table/info/table-info';
export class BaseFormatter {
stateMap;
manager;
rowFormatter;
layoutPosition;
lastRowInfo;
state;
pageAreaBounds;
columnBounds;
tableFormatter;
layoutRowBoundsCalculator = new LayoutRowBoundsCalculator();
pageBreakPositions = [];
constructor(manager, subDocId) {
this.manager = manager;
this.rowFormatter = new RowFormatter(manager, subDocId);
this.state = LayoutFormatterState.PageAreaStart;
this.stateMap = {};
this.stateMap[LayoutFormatterState.PageAreaStart] = this.processStatePageAreaStart;
this.stateMap[LayoutFormatterState.ColumnStart] = this.processStateColumnStart;
this.stateMap[LayoutFormatterState.RowFormatting] = this.processStateRowFormatting;
this.stateMap[LayoutFormatterState.ColumnEnd] = this.processStateColumnEnd;
}
initDocumentStart() {
this.rowFormatter.documentStart();
this.lastRowInfo = new LastRowInfo(this.subDocument.paragraphs);
this.lastRowInfo.reset(this.rowFormatter);
}
get subDocument() {
return this.rowFormatter.subDocument;
}
get currColumnHeight() {
return this.layoutPosition.rowIndex == 0 ? 0 : this.layoutPosition.column.rows[this.layoutPosition.rowIndex - 1].bottom;
}
formatPageArea(pageAreaBounds, columnBounds, page) {
Log.print(LogSource.LayoutFormatter, "formatPageArea", `pageIndex: ${page.index}`);
this.pageAreaBounds = pageAreaBounds;
this.columnBounds = columnBounds;
if (!this.layoutPosition) {
this.layoutPosition = new LayoutPosition(DocumentLayoutDetailsLevel.None);
this.layoutPosition.page = page;
this.layoutPosition.pageIndex = page.index;
}
while (this.stateMap[this.state].call(this) && this.state != LayoutFormatterState.PageAreaEnd)
;
}
processStatePageAreaStart() {
this.manager.activeFormatter = this;
Log.print(LogSource.LayoutFormatter, "processStatePageAreaStart", () => ` SubDocId: ${this.subDocument.id}, LayPos: ${LogObjToStrLayout.layoutPositionShort(this.layoutPosition)}`);
this.createNextPageArea();
if (this.subDocument.isMain())
this.layoutPosition.page.mainSubDocumentPageAreas.push(this.layoutPosition.pageArea);
else
this.layoutPosition.page.otherPageAreas[this.subDocument.id] = this.layoutPosition.pageArea;
return true;
}
processStateColumnStart() {
this.manager.activeFormatter = this;
Log.print(LogSource.LayoutFormatter, "processStateColumnStart", () => ` SubDocId: ${this.subDocument.id}, LayPos: ${LogObjToStrLayout.layoutPositionShort(this.layoutPosition)}`);
const columnBounds = this.columnBounds[this.layoutPosition.columnIndex];
this.createNextColumn(columnBounds);
this.layoutPosition.pageArea.columns.push(this.layoutPosition.column);
if (this.tableFormatter)
this.tableFormatter.columnStart(this.layoutPosition.column);
const bounds = new FixedInterval(this.layoutPosition.pageArea.x + columnBounds.x, columnBounds.width);
this.layoutRowBoundsCalculator.resetByColumn(this.layoutPosition.page.anchoredObjectHolder.objects, bounds, this.subDocument.isTextBox());
return true;
}
initializeTextBoxSizeForAutoFitTables() {
const tblPos = this.rowFormatter.getNextBoxWrapInfo().info.tablePosition[0];
if (tblPos.rowIndex == 0 && tblPos.cellIndex == 0) {
NumberMapUtils.forEach(this.manager.anchoredObjectsManager.textBoxContextSizeCalculators, (obj) => {
const currTblPoss = obj.wrap.info.tablePosition;
if (currTblPoss && currTblPoss[0].table.index == tblPos.table.index)
obj.calculateSize(this.manager.boundsCalculator);
});
}
}
processStateRowFormatting() {
this.manager.activeFormatter = this;
const currentPos = this.rowFormatter.getPosition();
if (this.tryBreakPageByPosition(currentPos))
return true;
const wrap = this.rowFormatter.getNextBoxWrapInfo();
const startRowOffset = wrap.box.rowOffset;
Log.print(LogSource.LayoutFormatter, "processStateRowFormatting", () => `pos:${startRowOffset}, SubDocId: ${this.subDocument.id}, LayPos: ${LogObjToStrLayout.layoutPositionShort(this.layoutPosition)}`);
if (!this.tableFormatter && this.applyPageBreakBefore())
return true;
if (!this.tableFormatter && wrap.info.tablePosition) {
const tableOffset = new Point(0, this.getTableStartYOffsetPosition());
const tableMaxWidth = LayoutColumn.findSectionColumnWithMinimumWidth(this.columnBounds);
if (TableInfo.checkIsTableCannotBePlacedOnCurrentPage(this.rowFormatter, wrap.info.tablePosition[0].table, tableOffset.y, tableMaxWidth)) {
if (this.layoutPosition.rowIndex > 0) {
wrap.info.pageIndexFromWhichTableWasMoved ??= this.layoutPosition.pageIndex;
this.state = LayoutFormatterState.ColumnEnd;
return true;
}
else if (this.tryExcludeAnchoredObjectFromCurrentPage())
return true;
}
this.initializeTextBoxSizeForAutoFitTables();
this.tableFormatter = new Formatter(this.rowFormatter, wrap.info.tablePosition, this.layoutPosition.column, tableMaxWidth, tableOffset, null, 0, null, null, wrap.info.pageIndexFromWhichTableWasMoved);
this.tableFormatter.tableInfo.currLayoutTableColumnInfo.logicInfo.isEditable = this.tableIsEditable(this.tableFormatter.tableInfo.table);
}
const rowResult = this.createRow();
if (rowResult.flags.get(RowFormatterResultFlag.NotEnoughChunks))
return false;
const addRowInTableResult = this.tableFormatter ?
this.tableFormatter.addLayoutRow(rowResult, wrap.info) :
new Flag(AddRowToTableResult.RowAdded);
if (addRowInTableResult.get(AddRowToTableResult.RowAdded)) {
const row = rowResult.row;
if (this.subDocument.isMain() && !this.tableFormatter && this.cantPlaceRowOnThisColumn(row)) {
if (this.layoutPosition.rowIndex === 0) {
if (this.tryExcludeAnchoredObjectFromCurrentPage())
return true;
}
else {
this.rowFormatter.setPosition(startRowOffset, false, !this.tableFormatter);
this.state = LayoutFormatterState.ColumnEnd;
return true;
}
}
this.pushRow(row, startRowOffset, rowResult.paragraphIndex);
if (this.placeAnchorObjects(rowResult)) {
if (this.tableFormatter) {
const bounds = new FixedInterval(this.layoutPosition.pageArea.x + this.layoutPosition.column.x +
this.layoutPosition.row.tableCellInfo.x, this.layoutPosition.row.tableCellInfo.width);
const obj = ListUtils.elementBy(rowResult.newAnchoredObjects, (obj) => obj.levelType == AnchoredObjectLevelType.InText);
this.layoutRowBoundsCalculator.addTableInTextObject(obj, bounds);
if (this.tableFormatter.resetCaseInTextAnchorObject(wrap.info, obj)) {
if (!this.layoutPosition.column.rows[0]) {
this.manager.floatingRestartInfoHolder.storeInfo(this.layoutPosition);
new RestartPreparer(this.manager).restartFromPage(this.layoutPosition.pageIndex, false, false);
}
else
this.state = LayoutFormatterState.ColumnEnd;
}
}
else {
this.manager.floatingRestartInfoHolder.storeInfo(this.layoutPosition);
new RestartPreparer(this.manager).restartByAnchoredObject(this.layoutPosition.page);
if (!this.subDocument.isMain())
this.state = LayoutFormatterState.PageAreaEnd;
}
this.isColumnOk();
return true;
}
if (this.tableFormatter) {
this.tableFormatter.actualFormatter.findNextCell(addRowInTableResult, wrap.info);
}
}
if (this.tableFormatter)
this.tableFormatter.applyResultOfTopLevelFormatters(addRowInTableResult, wrap.info);
if (addRowInTableResult.get(AddRowToTableResult.TableFinished))
this.tableFormatter = null;
if (this.shouldEndColumn(addRowInTableResult)) {
this.isColumnOk();
this.state = LayoutFormatterState.ColumnEnd;
}
return true;
}
tryBreakPageByPosition(pos) {
while (this.pageBreakPositions.length > 0 && pos >= this.pageBreakPositions[this.pageBreakPositions.length - 1]) {
const position = this.pageBreakPositions.pop();
if (position === pos) {
this.finishPage();
return true;
}
}
return false;
}
tryExcludeAnchoredObjectFromCurrentPage() {
const obj = this.layoutPosition.page.anchoredObjectHolder.getUnapprovedObj();
if (obj && obj.belongsToSubDocId === SubDocument.MAIN_SUBDOCUMENT_ID) {
const ancPos = this.manager.layout.anchorObjectsPositionInfo.getPosition(obj.objectId);
if (ancPos <= this.rowFormatter.getPosition())
return false;
this.pageBreakPositions.push(this.subDocument.getParagraphByPosition(ancPos).startLogPosition.value);
this.manager.layout.anchorObjectsPositionInfo.delete(obj.objectId);
this.layoutPosition.page.anchoredObjectHolder.removeObject(obj);
new RestartPreparer(this.manager).restartFromPage(this.layoutPosition.pageIndex, false, false);
return true;
}
return false;
}
shouldEndColumn(result) {
const columnEndFlags = [LayoutRowStateFlags.ColumnEnd, LayoutRowStateFlags.DocumentEnd, LayoutRowStateFlags.SectionEnd, LayoutRowStateFlags.PageEnd];
return result.get(AddRowToTableResult.GoToNextColumn) || result.get(AddRowToTableResult.RowAdded) && this.layoutPosition.row.flags.anyOf(...columnEndFlags);
}
processStateColumnEnd() {
this.manager.activeFormatter = this;
Log.print(LogSource.LayoutFormatter, "processStateColumnEnd", () => ` SubDocId: ${this.subDocument.id}, LayPos: ${LogObjToStrLayout.layoutPositionShort(this.layoutPosition)}`);
const createdColumn = this.layoutPosition.column;
createdColumn.rows.sort((a, b) => a.columnOffset - b.columnOffset);
BaseFormatter.correctRowOffsets(createdColumn);
if (this.tableFormatter)
this.tableFormatter.columnEnd();
createdColumn.tablesInfo.sort((a, b) => a.logicInfo.grid.table.index - b.logicInfo.grid.table.index);
ListUtils.forEach(createdColumn.rows, (row, index) => {
if (row.tableCellInfo)
row.indexInColumn = index;
});
const lastRow = createdColumn.getLastRow();
lastRow.flags.set(LayoutRowStateFlags.ColumnEnd, true);
if (this.manager.innerClientProperties.viewsSettings.isSimpleView) {
const lastAnchorBox = NumberMapUtils.max(this.layoutPosition.page.anchoredObjectHolder.objects, a => a.bottom);
const margins = this.manager.innerClientProperties.viewsSettings.paddings;
const pageInfo = this.manager.innerClientProperties.viewsSettings.pageVerticalInfo;
const lastRowBottomPos = lastRow.bottom + margins.top;
const bottomPosition = lastAnchorBox ? Math.max(lastAnchorBox.bottom, lastRowBottomPos) : lastRowBottomPos;
const minPageContentHeight = bottomPosition + margins.bottom;
const minVisibleAreaHeight = minPageContentHeight +
pageInfo.topMargin + pageInfo.topPageBorderWidth + pageInfo.bottomPageBorderWidth + pageInfo.bottomMargin;
const zoomLevel = this.manager.innerClientProperties.viewsSettings.zoomLevel;
const diff = Math.floor(Math.max(0, this.manager.controlHeightProvider.getVisibleAreaHeight(false) / zoomLevel - minVisibleAreaHeight));
const finalPageHeight = minPageContentHeight + diff;
const columnHeight = finalPageHeight - margins.vertical;
this.layoutPosition.column.height = columnHeight;
this.layoutPosition.pageArea.height = columnHeight;
this.layoutPosition.page.height = finalPageHeight;
this.layoutPosition.page.minContentHeight = minVisibleAreaHeight;
}
createdColumn.paragraphFrames = ParagraphFrameCollector.collect(this.manager.model.colorProvider, createdColumn, this.subDocument.isMain() ?
this.layoutPosition.page.getPosition() + this.layoutPosition.pageArea.pageOffset : 0, this.subDocument.paragraphs);
if (lastRow.flags.anyOf(LayoutRowStateFlags.DocumentEnd, LayoutRowStateFlags.PageEnd, LayoutRowStateFlags.SectionEnd)) {
this.layoutPosition.detailsLevel = DocumentLayoutDetailsLevel.PageArea;
this.state = LayoutFormatterState.PageAreaEnd;
}
else {
if (this.layoutPosition.columnIndex + 1 < this.columnBounds.length) {
this.state = LayoutFormatterState.ColumnStart;
this.layoutPosition.columnIndex++;
this.layoutPosition.column = null;
}
else
this.state = LayoutFormatterState.PageAreaEnd;
}
return true;
}
createNextPageArea() {
const newPageArea = new LayoutPageArea(this.subDocument);
newPageArea.setGeomerty(this.pageAreaBounds);
newPageArea.pageOffset = this.subDocument.isMain() ? 0 : this.rowFormatter.getPosition();
this.state = LayoutFormatterState.ColumnStart;
this.layoutPosition.pageArea = newPageArea;
this.layoutPosition.columnIndex = 0;
this.layoutPosition.column = null;
this.layoutPosition.detailsLevel = DocumentLayoutDetailsLevel.PageArea;
}
createNextColumn(columnBounds) {
const newColumn = new LayoutColumn();
newColumn.setGeomerty(columnBounds);
newColumn.pageAreaOffset = this.layoutPosition.columnIndex == 0 || !this.subDocument.isMain() ?
0 :
this.rowFormatter.getPosition() - this.layoutPosition.pageArea.pageOffset - this.layoutPosition.page.getPosition();
this.state = LayoutFormatterState.RowFormatting;
this.layoutPosition.column = newColumn;
this.layoutPosition.rowIndex = 0;
this.layoutPosition.detailsLevel = DocumentLayoutDetailsLevel.Column;
}
pushRow(row, rowAbsStartPos, parIndex) {
this.layoutPosition.row = row;
this.layoutPosition.column.rows.push(row);
this.layoutPosition.rowIndex++;
this.lastRowInfo.setFullRowInfo(row, rowAbsStartPos, parIndex);
BaseFormatter.correctBoxOffsets(row);
}
createRow() {
const rowSpacingBeforeApplier = this.tableFormatter ?
new TableRowSpacingBeforeApplier(this.lastRowInfo.row, this.subDocument.paragraphs, this.tableFormatter.isCurrLayoutRowIsFirstInCell, this.tableFormatter.isCurrTableCellFirstInRow, this.tableFormatter.isCurrTableRowIsFirstInTable) :
new RowSpacingBeforeApplier(this.subDocument.documentModel.compatibilitySettings, this.lastRowInfo, this.subDocument.paragraphs, this.layoutPosition.rowIndex == 0);
const offsetRelativeColumn = this.layoutPosition.getOffsetRelativeColumn();
const absOffset = this.tableFormatter ?
offsetRelativeColumn.clone().offsetByPoint(this.tableFormatter.currLayoutRowOffset) :
this.getCurrOffsetForRow(offsetRelativeColumn);
let minimumOfY = Constants.MIN_SAFE_INTEGER;
if (!this.tableFormatter && this.lastRowInfo.row && this.manager.innerClientProperties.viewsSettings.isSimpleView &&
ListUtils.unsafeAnyOf(this.lastRowInfo.row.boxes, b => EnumUtils.isAnyOf(b.getType(), LayoutBoxType.ColumnBreak, LayoutBoxType.PageBreak, LayoutBoxType.SectionMark))) {
const lastRowBottomPos = this.lastRowInfo.row.bottom;
let ancObj = null;
NumberMapUtils.forEach(this.layoutPosition.page.anchoredObjectHolder.objects, obj => {
if (!obj.isInText() && (obj.bottom > lastRowBottomPos) &&
(!ancObj || obj.bottom > ancObj.bottom))
ancObj = obj;
});
if (ancObj)
minimumOfY = ancObj.bottom + 1;
}
const maxLayoutRowWidth = this.tableFormatter ? this.tableFormatter.currLayoutRowContentWidth : this.layoutPosition.column.width;
this.rowFormatter.formatRow(Math.max(minimumOfY, absOffset.y), new FixedInterval(absOffset.x, maxLayoutRowWidth), rowSpacingBeforeApplier);
const rowResult = this.rowFormatter.result;
if (rowResult.row)
rowResult.row.moveRectangleByPoint(offsetRelativeColumn.multiply(-1, -1));
return rowResult;
}
getCurrOffsetForRow(offsetRelativeColumn) {
const prevRow = ListUtils.last(this.layoutPosition.column.rows);
if (!prevRow)
return offsetRelativeColumn;
const offset = offsetRelativeColumn.clone();
offset.y += prevRow.tableCellInfo ?
prevRow.tableCellInfo.parentRow.parentTable.getTopLevelColumn().bottom :
prevRow.bottom;
return offset;
}
applyPageBreakBefore() {
const wrapInfo = this.rowFormatter.getNextBoxWrapInfo();
if (wrapInfo && this.lastRowInfo.isNextRowFirstInParagraph() &&
this.subDocument.paragraphs[wrapInfo.info.paragraphIndex].getParagraphMergedProperties().pageBreakBefore &&
this.layoutPosition.rowIndex !== 0 && this.manager.innerClientProperties.viewsSettings.isPrintLayoutView) {
this.finishPage();
return true;
}
return false;
}
finishPage() {
this.lastRowInfo.row.flags.set(LayoutRowStateFlags.PageEnd, true);
this.state = LayoutFormatterState.ColumnEnd;
}
cantPlaceRowOnThisColumn(row) {
const lineBottom = row.y + row.lineHeight;
return lineBottom > this.layoutPosition.column.height && row.hasEffectToPageHeight;
}
tableIsEditable(table) {
const tableInterval = FixedInterval.fromPositions(table.getStartPosition(), table.getEndPosition());
return this.subDocument.isEditable([tableInterval]);
}
placeAnchorObjects(rowResult) {
if (!rowResult.newAnchoredObjects.length)
return false;
let needRestartFromPageStart = false;
ListUtils.forEach(rowResult.newAnchoredObjects, (obj) => {
this.manager.layout.anchorObjectsPositionInfo.add(obj, obj.rowOffset);
switch (obj.getType()) {
case LayoutBoxType.AnchorTextBox: {
const textBox = obj;
if (textBox.internalSubDocId >= 0) {
this.manager.otherPageAreaFormatter.setTextBoxContent(this.layoutPosition.page, textBox);
this.layoutPosition.page.anchoredObjectHolder.addObject(this.manager, textBox);
const pageArea = this.layoutPosition.page.otherPageAreas[textBox.internalSubDocId];
const contentBounds = textBox.getContentBounds();
pageArea.x = contentBounds.x;
pageArea.y = contentBounds.y;
}
break;
}
case LayoutBoxType.AnchorPicture: {
this.layoutPosition.page.anchoredObjectHolder.addObject(this.manager, obj);
break;
}
default: throw new Error(Errors.InternalException);
}
if (obj.levelType == AnchoredObjectLevelType.InText)
needRestartFromPageStart = true;
else
obj.isApproved = true;
});
return needRestartFromPageStart;
}
getTableStartYOffsetPosition() {
if (!this.lastRowInfo.row || !this.layoutPosition.column.rows.length)
return 0;
if (!this.lastRowInfo.row.tableCellInfo)
return this.lastRowInfo.row.bottom;
return this.lastRowInfo.row.tableCellInfo.parentRow.parentTable.getTopLevelColumn().bottom;
}
static correctColumnOffsets(pageArea) {
const columns = pageArea.columns;
if (!columns.length)
return;
const offsetFirstColumnFromPageArea = columns[0].pageAreaOffset;
if (offsetFirstColumnFromPageArea != 0) {
pageArea.pageOffset += offsetFirstColumnFromPageArea;
for (let column of columns)
column.pageAreaOffset -= offsetFirstColumnFromPageArea;
}
}
static correctRowOffsets(column) {
const rows = column.rows;
if (!rows.length)
return;
const offsetFirstRowFromColumn = rows[0].columnOffset;
if (offsetFirstRowFromColumn != 0) {
column.pageAreaOffset += offsetFirstRowFromColumn;
for (let row of rows)
row.columnOffset -= offsetFirstRowFromColumn;
}
}
static correctBoxOffsets(row) {
const boxes = row.boxes;
if (!boxes)
return;
const offsetFirstBoxFromRow = boxes[0].rowOffset;
if (offsetFirstBoxFromRow != 0) {
row.columnOffset += offsetFirstBoxFromRow;
for (let box of boxes)
box.rowOffset -= offsetFirstBoxFromRow;
}
}
isColumnOk() {
if (this.state == LayoutFormatterState.ColumnEnd && !this.layoutPosition.column.rows[0])
throw new Error(Errors.InternalException);
}
}