Return-Path: X-Original-To: apmail-corinthia-commits-archive@minotaur.apache.org Delivered-To: apmail-corinthia-commits-archive@minotaur.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id F2BB41817B for ; Mon, 17 Aug 2015 08:50:03 +0000 (UTC) Received: (qmail 65011 invoked by uid 500); 17 Aug 2015 08:50:03 -0000 Delivered-To: apmail-corinthia-commits-archive@corinthia.apache.org Received: (qmail 64993 invoked by uid 500); 17 Aug 2015 08:50:03 -0000 Mailing-List: contact commits-help@corinthia.incubator.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@corinthia.incubator.apache.org Delivered-To: mailing list commits@corinthia.incubator.apache.org Received: (qmail 64984 invoked by uid 99); 17 Aug 2015 08:50:03 -0000 Received: from Unknown (HELO spamd1-us-west.apache.org) (209.188.14.142) by apache.org (qpsmtpd/0.29) with ESMTP; Mon, 17 Aug 2015 08:50:03 +0000 Received: from localhost (localhost [127.0.0.1]) by spamd1-us-west.apache.org (ASF Mail Server at spamd1-us-west.apache.org) with ESMTP id 4C8DADE2F8 for ; Mon, 17 Aug 2015 08:50:03 +0000 (UTC) X-Virus-Scanned: Debian amavisd-new at spamd1-us-west.apache.org X-Spam-Flag: NO X-Spam-Score: 1.224 X-Spam-Level: * X-Spam-Status: No, score=1.224 tagged_above=-999 required=6.31 tests=[KAM_ASCII_DIVIDERS=0.8, KAM_LAZY_DOMAIN_SECURITY=1, RP_MATCHES_RCVD=-0.577, URIBL_BLOCKED=0.001] autolearn=disabled Received: from mx1-us-east.apache.org ([10.40.0.8]) by localhost (spamd1-us-west.apache.org [10.40.0.7]) (amavisd-new, port 10024) with ESMTP id Ews3xidi7Mpc for ; Mon, 17 Aug 2015 08:49:56 +0000 (UTC) Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by mx1-us-east.apache.org (ASF Mail Server at mx1-us-east.apache.org) with SMTP id C69F142BA7 for ; Mon, 17 Aug 2015 08:49:54 +0000 (UTC) Received: (qmail 64791 invoked by uid 99); 17 Aug 2015 08:49:54 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Mon, 17 Aug 2015 08:49:54 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id 429E5DFE7C; Mon, 17 Aug 2015 08:49:54 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: jani@apache.org To: commits@corinthia.incubator.apache.org Date: Mon, 17 Aug 2015 08:50:11 -0000 Message-Id: <82aaf41476b041dda31fc136473300f5@git.apache.org> In-Reply-To: References: X-Mailer: ASF-Git Admin Mailer Subject: [19/28] incubator-corinthia git commit: included MOC compiler for Qt implementation http://git-wip-us.apache.org/repos/asf/incubator-corinthia/blob/9bf02bb2/experiments/editorFramework/src/Javascript_Layer_0/Tables.js ---------------------------------------------------------------------- diff --git a/experiments/editorFramework/src/Javascript_Layer_0/Tables.js b/experiments/editorFramework/src/Javascript_Layer_0/Tables.js new file mode 100644 index 0000000..f1f27be --- /dev/null +++ b/experiments/editorFramework/src/Javascript_Layer_0/Tables.js @@ -0,0 +1,1362 @@ +// 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. + +var Tables_insertTable; +var Tables_addAdjacentRow; +var Tables_addAdjacentColumn; +var Tables_removeAdjacentRow; +var Tables_removeAdjacentColumn; +var Tables_deleteRegion; +var Tables_clearCells; +var Tables_mergeCells; +var Tables_splitSelection; +var Tables_cloneRegion; +var Tables_analyseStructure; +var Tables_findContainingCell; +var Tables_findContainingTable; +var Tables_regionFromRange; +var Tables_getSelectedTableId; +var Tables_getProperties; +var Tables_setProperties; +var Tables_getColWidths; +var Tables_setColWidths; +var Tables_getGeometry; + +var Table_get; +var Table_set; +var Table_setRegion; +var Table_fix; +var Table_fixColumnWidths; +var TableRegion_splitCells; + +(function() { + + function Cell(element,row,col) + { + this.element = element; + this.row = row; + this.col = col; + + if (element.hasAttribute("colspan")) + this.colspan = parseInt(element.getAttribute("colspan")); + else + this.colspan = 1; + if (element.hasAttribute("rowspan")) + this.rowspan = parseInt(element.getAttribute("rowspan")); + else + this.rowspan = 1; + + if (this.colspan < 1) + this.colspan = 1; + if (this.rowspan < 1) + this.rowspan = 1; + + this.top = this.row; + this.bottom = this.top + this.rowspan - 1; + this.left = this.col; + this.right = this.left + this.colspan - 1; + } + + function Cell_setRowspan(cell,rowspan) + { + if (rowspan < 1) + rowspan = 1; + cell.rowspan = rowspan; + cell.bottom = cell.top + cell.rowspan - 1; + if (rowspan == 1) + DOM_removeAttribute(cell.element,"rowspan"); + else + DOM_setAttribute(cell.element,"rowspan",rowspan); + } + + function Cell_setColspan(cell,colspan) + { + if (colspan < 1) + colspan = 1; + cell.colspan = colspan; + cell.right = cell.left + cell.colspan - 1; + if (colspan == 1) + DOM_removeAttribute(cell.element,"colspan"); + else + DOM_setAttribute(cell.element,"colspan",colspan); + } + + function Table(element) + { + this.element = element; + this.row = 0; + this.col = 0; + this.cells = new Array(); + this.numRows = 0; + this.numCols = 0; + this.translated = false; + this.cellsByElement = new NodeMap(); + Table_processTable(this,element); + } + + // public + Table_get = function(table,row,col) + { + if (table.cells[row] == null) + return null; + return table.cells[row][col]; + } + + // public + Table_set = function(table,row,col,cell) + { + if (table.numRows < row+1) + table.numRows = row+1; + if (table.numCols < col+1) + table.numCols = col+1; + if (table.cells[row] == null) + table.cells[row] = new Array(); + table.cells[row][col] = cell; + } + + // public + Table_setRegion = function(table,top,left,bottom,right,cell) + { + for (var row = top; row <= bottom; row++) { + for (var col = left; col <= right; col++) { + var destCell = Table_get(table,row,col); + DOM_deleteNode(destCell.element); + Table_set(table,row,col,cell); + } + } + } + + function Table_processTable(table,node) + { + var type = node._type; + switch (node._type) { + case HTML_TD: + case HTML_TH: { + while (Table_get(table,table.row,table.col) != null) + table.col++; + + var cell = new Cell(node,table.row,table.col); + table.cellsByElement.put(node,cell); + + for (var r = 0; r < cell.rowspan; r++) { + for (var c = 0; c < cell.colspan; c++) { + Table_set(table,table.row+r,table.col+c,cell); + } + } + table.col += cell.colspan; + break; + } + case HTML_TR: + for (var child = node.firstChild; child != null; child = child.nextSibling) + Table_processTable(table,child); + table.row++; + table.col = 0; + break; + default: + for (var child = node.firstChild; child != null; child = child.nextSibling) + Table_processTable(table,child); + break; + } + } + + // public + Tables_insertTable = function(rows,cols,width,numbered,caption,className) + { + UndoManager_newGroup("Insert table"); + + if (rows < 1) + rows = 1; + if (cols < 1) + cols = 1; + + var haveCaption = (caption != null) && (caption != ""); + var table = DOM_createElement(document,"TABLE"); + + if (width != null) + DOM_setStyleProperties(table,{"width": width}); + + if (className != null) + DOM_setAttribute(table,"class",className); + + // Caption comes first + if (haveCaption) { + var tableCaption = DOM_createElement(document,"CAPTION"); + DOM_appendChild(tableCaption,DOM_createTextNode(document,caption)); + DOM_appendChild(table,tableCaption); + } + + // Set equal column widths + var colWidth = Math.round(100/cols)+"%"; + for (var c = 0; c < cols; c++) { + var col = DOM_createElement(document,"COL"); + DOM_setAttribute(col,"width",colWidth); + DOM_appendChild(table,col); + } + + var firstTD = null; + + // Then the rows and columns + var tbody = DOM_createElement(document,"TBODY"); + DOM_appendChild(table,tbody); + for (var r = 0; r < rows; r++) { + var tr = DOM_createElement(document,"TR"); + DOM_appendChild(tbody,tr); + for (var c = 0; c < cols; c++) { + var td = DOM_createElement(document,"TD"); + var p = DOM_createElement(document,"P"); + var br = DOM_createElement(document,"BR"); + DOM_appendChild(tr,td); + DOM_appendChild(td,p); + DOM_appendChild(p,br); + + if (firstTD == null) + firstTD = td; + } + } + + Clipboard_pasteNodes([table]); + + // Now that the table has been inserted into the DOM tree, the outline code will + // have noticed it and added an id attribute, as well as a caption giving the + // table number. + Outline_setNumbered(table.getAttribute("id"),numbered); + + // Place the cursor at the start of the first cell on the first row + var pos = new Position(firstTD,0); + pos = Position_closestMatchForwards(pos,Position_okForMovement); + Selection_set(pos.node,pos.offset,pos.node,pos.offset); + + PostponedActions_add(UndoManager_newGroup); + } + + // private + function createEmptyTableCell(elementName) + { + var br = DOM_createElement(document,"BR"); + var p = DOM_createElement(document,"P"); + var td = DOM_createElement(document,elementName); + DOM_appendChild(p,br); + DOM_appendChild(td,p); + return td; + } + + // private + function addEmptyTableCell(newTR,elementName) + { + var td = createEmptyTableCell(elementName); + DOM_appendChild(newTR,td); + return td; + } + + // private + function populateNewRow(structure,newTR,newRow,oldRow) + { + var col = 0; + while (col < structure.numCols) { + var existingCell = Table_get(structure,oldRow,col); + if (((newRow > oldRow) && (newRow < existingCell.row + existingCell.rowspan)) || + ((newRow < oldRow) && (newRow >= existingCell.row))) { + Cell_setRowspan(existingCell,existingCell.rowspan+1); + } + else { + var td = addEmptyTableCell(newTR,existingCell.element.nodeName); // check-ok + if (existingCell.colspan != 1) + DOM_setAttribute(td,"colspan",existingCell.colspan); + } + col += existingCell.colspan; + } + } + + function tableAtRightOfRange(range) + { + if (!Range_isEmpty(range)) + return null; + + var pos = Position_preferElementPosition(range.start); + if ((pos.node.nodeType == Node.ELEMENT_NODE) && + (pos.offset < pos.node.childNodes.length) && + (pos.node.childNodes[pos.offset]._type == HTML_TABLE)) { + var element = pos.node.childNodes[pos.offset]; + var table = Tables_analyseStructure(element); + return table; + } + return null; + } + + function tableAtLeftOfRange(range) + { + if (!Range_isEmpty(range)) + return null; + + var pos = Position_preferElementPosition(range.start); + if ((pos.node.nodeType == Node.ELEMENT_NODE) && + (pos.offset > 0) && + (pos.node.childNodes[pos.offset-1]._type == HTML_TABLE)) { + var element = pos.node.childNodes[pos.offset-1]; + var table = Tables_analyseStructure(element); + return table; + } + return null; + } + + function insertRowAbove(table,row) + { + var cell = Table_get(table,row,0); + var oldTR = cell.element.parentNode; + var newTR = DOM_createElement(document,"TR"); + DOM_insertBefore(oldTR.parentNode,newTR,oldTR); + populateNewRow(table,newTR,row-1,row); + } + + function insertRowBelow(table,row) + { + var cell = Table_get(table,row,0); + var oldTR = cell.element.parentNode; + var newTR = DOM_createElement(document,"TR"); + DOM_insertBefore(oldTR.parentNode,newTR,oldTR.nextSibling); + populateNewRow(table,newTR,row+1,row); + } + + function insertRowAdjacentToRange(range) + { + var table; + + table = tableAtLeftOfRange(range); + if (table != null) { + insertRowBelow(table,table.numRows-1); + return; + } + + table = tableAtRightOfRange(range); + if (table != null) { + insertRowAbove(table,0); + return; + } + } + + // public + Tables_addAdjacentRow = function() + { + UndoManager_newGroup("Insert row below"); + Selection_preserveWhileExecuting(function() { + var range = Selection_get(); + var region = Tables_regionFromRange(range,true); + if (region != null) + insertRowBelow(region.structure,region.bottom); + else + insertRowAdjacentToRange(range); + }); + UndoManager_newGroup(); + } + + // private + function getColElements(table) + { + var cols = new Array(); + for (child = table.firstChild; child != null; child = child.nextSibling) { + switch (child._type) { + case HTML_COLGROUP: + for (var gc = child.firstChild; gc != null; gc = gc.nextSibling) { + if (gc._type == HTML_COL) + cols.push(gc); + } + break; + case HTML_COL: + cols.push(child); + break; + } + } + return cols; + } + + // private + function getColWidths(colElements,expectedCount) + { + // FIXME: also handle the case where the width has been set as a CSS property in the + // style attribute. There's probably not much we can do if the width comes from a style + // rule elsewhere in the document though. + var colWidths = new Array(); + for (var i = 0; i < colElements.length; i++) { + if (colElements[i].hasAttribute("width")) + colWidths.push(colElements[i].getAttribute("width")); + else + colWidths.push(""); + } + return colWidths; + } + + // private + function addMissingColElements(structure,colElements) + { + // If there are fewer COL elements than there are colums, add extra ones, copying the + // width value from the last one + // FIXME: handle col elements with colspan > 1, as well as colgroups with width set + // FIXME: What if there are 0 col elements? + while (colElements.length < structure.numCols) { + var newColElement = DOM_createElement(document,"COL"); + var lastColElement = colElements[colElements.length-1]; + DOM_insertBefore(lastColElement.parentNode,newColElement,lastColElement.nextSibling); + colElements.push(newColElement); + DOM_setAttribute(newColElement,"width",lastColElement.getAttribute("width")); + } + } + + // private + function fixColPercentages(structure,colElements) + { + var colWidths = getColWidths(colElements,structure.numCols); + + var percentages = colWidths.map(getPercentage); + if (percentages.every(notNull)) { + var colWidthTotal = 0; + for (var i = 0; i < percentages.length; i++) + colWidthTotal += percentages[i]; + + for (var i = 0; i < colElements.length; i++) { + var pct = 100*percentages[i]/colWidthTotal; + // Store value using at most two decimal places + pct = Math.round(100*pct)/100; + DOM_setAttribute(colElements[i],"width",pct+"%"); + } + } + + function notNull(arg) + { + return (arg != null); + } + + function getPercentage(str) + { + if (str.match(/^\s*\d+(\.\d+)?\s*%\s*$/)) + return parseInt(str.replace(/\s*%\s*$/,"")); + else + return null; + } + } + + // private + function addColElement(structure,oldIndex,right) + { + var table = structure.element; + + var colElements = getColElements(table); + if (colElements.length == 0) { + // The table doesn't have any COL elements; don't add any + return; + } + + addMissingColElements(structure,colElements); + + var prevColElement = colElements[oldIndex]; + var newColElement = DOM_createElement(document,"COL"); + DOM_setAttribute(newColElement,"width",prevColElement.getAttribute("width")); + if (right) + DOM_insertBefore(prevColElement.parentNode,newColElement,prevColElement.nextSibling); + else + DOM_insertBefore(prevColElement.parentNode,newColElement,prevColElement); + + if (right) { + colElements.splice(oldIndex+1,0,newColElement); + } + else { + colElements.splice(oldIndex+1,0,newColElement); + } + + fixColPercentages(structure,colElements); + } + + // private + function deleteColElements(structure,left,right) + { + var table = structure.element; + + var colElements = getColElements(table); + if (colElements.length == 0) { + // The table doesn't have any COL elements + return; + } + + addMissingColElements(structure,colElements); + + for (var col = left; col <= right; col++) + DOM_deleteNode(colElements[col]); + colElements.splice(left,right-left+1); + + fixColPercentages(structure,colElements); + } + + // private + function addColumnCells(structure,oldIndex,right) + { + for (var row = 0; row < structure.numRows; row++) { + var cell = Table_get(structure,row,oldIndex); + var oldTD = cell.element; + if (cell.row == row) { + + if (((right && (oldIndex+1 < cell.col + cell.colspan)) || + (!right && (oldIndex-1 >= cell.col))) && + (cell.colspan > 1)) { + Cell_setColspan(cell,cell.colspan+1); + } + else { + var newTD = createEmptyTableCell(oldTD.nodeName); // check-ok + if (right) + DOM_insertBefore(cell.element.parentNode,newTD,oldTD.nextSibling); + else + DOM_insertBefore(cell.element.parentNode,newTD,oldTD); + if (cell.rowspan != 1) + DOM_setAttribute(newTD,"rowspan",cell.rowspan); + } + } + } + } + + function insertColumnAdjacentToRange(range) + { + var table; + + table = tableAtLeftOfRange(range); + if (table != null) { + var right = table.numCols-1; + addColElement(table,right,right+1); + addColumnCells(table,right,true); + return; + } + + table = tableAtRightOfRange(range); + if (table != null) { + var left = 0; + addColElement(table,left,left-1); + addColumnCells(table,left,false); + return; + } + } + + // public + Tables_addAdjacentColumn = function() + { + UndoManager_newGroup("Insert column at right"); + Selection_preserveWhileExecuting(function() { + var range = Selection_get(); + var region = Tables_regionFromRange(range,true); + if (region != null) { + addColElement(region.structure,region.right,region.right+1); + addColumnCells(region.structure,region.right,true); + } + else { + insertColumnAdjacentToRange(range); + } + }); + UndoManager_newGroup(); + } + + function columnHasContent(table,col) + { + for (var row = 0; row < table.numRows; row++) { + var cell = Table_get(table,row,col); + if ((cell != null) && (cell.col == col) && nodeHasContent(cell.element)) + return true; + } + return false; + } + + function rowHasContent(table,row) + { + for (var col = 0; col < table.numCols; col++) { + var cell = Table_get(table,row,col); + if ((cell != null) && (cell.row == row) && nodeHasContent(cell.element)) + return true; + } + return false; + } + + function selectRegion(table,top,bottom,left,right) + { + left = clampCol(table,left); + right = clampCol(table,right); + top = clampRow(table,top); + bottom = clampRow(table,bottom); + + var tlCell = Table_get(table,top,left); + var brCell = Table_get(table,bottom,right); + if ((tlCell != null) && (brCell != null)) { + var tlPos = new Position(tlCell.element,0); + tlPos = Position_closestMatchForwards(tlPos,Position_okForMovement); + + var brPos = new Position(brCell.element,brCell.element.childNodes.length); + brPos = Position_closestMatchBackwards(brPos,Position_okForMovement); + + Selection_set(tlPos.node,tlPos.offset,brPos.node,brPos.offset); + } + } + + function clampCol(table,col) + { + if (col > table.numCols-1) + col = table.numCols-1; + if (col < 0) + col = 0; + return col; + } + + function clampRow(table,row) + { + if (row > table.numRows-1) + row = table.numRows-1; + if (row < 0) + row = 0; + return row; + } + + function removeRowAdjacentToRange(range) + { + var table; + + table = tableAtLeftOfRange(range); + if ((table != null) && (table.numRows >= 2)) { + UndoManager_newGroup("Delete one row"); + var row = table.numRows-1; + Tables_deleteRegion(new TableRegion(table,row,row,0,table.numCols-1)); + UndoManager_newGroup(); + return; + } + + table = tableAtRightOfRange(range); + if ((table != null) && (table.numRows >= 2)) { + UndoManager_newGroup("Delete one row"); + Tables_deleteRegion(new TableRegion(table,0,0,0,table.numCols-1)); + UndoManager_newGroup(); + return; + } + } + + Tables_removeAdjacentRow = function() + { + var range = Selection_get(); + var region = Tables_regionFromRange(range,true); + + if (region == null) { + removeRowAdjacentToRange(range); + return; + } + + if (region.structure.numRows <= 1) + return; + + UndoManager_newGroup("Delete one row"); + + var table = region.structure; + var left = region.left; + var right = region.right; + var top = region.top; + var bottom = region.bottom; + + // Is there an empty row below the selection? If so, delete it + if ((bottom+1 < table.numRows) && !rowHasContent(table,bottom+1)) { + Selection_preserveWhileExecuting(function() { + Tables_deleteRegion(new TableRegion(table,bottom+1,bottom+1,0,table.numCols-1)); + }); + } + + // Is there an empty row above the selection? If so, delete it + else if ((top-1 >= 0) && !rowHasContent(table,top-1)) { + Selection_preserveWhileExecuting(function() { + Tables_deleteRegion(new TableRegion(table,top-1,top-1,0,table.numCols-1)); + }); + } + + + // There are no empty rows adjacent to the selection. Delete the right-most row + // of the selection (which may be the only one) + else { + Selection_preserveWhileExecuting(function() { + Tables_deleteRegion(new TableRegion(table,bottom,bottom,0,table.numCols-1)); + }); + + table = Tables_analyseStructure(table.element); + var multiple = (top != bottom); + + if (multiple) { + selectRegion(table,top,bottom-1,left,right); + } + else { + var newRow = clampRow(table,bottom); + var newCell = Table_get(table,newRow,left); + if (newCell != null) { + var pos = new Position(newCell.element,0); + pos = Position_closestMatchForwards(pos,Position_okForMovement); + Selection_set(pos.node,pos.offset,pos.node,pos.offset); + } + } + } + + UndoManager_newGroup(); + } + + function removeColumnAdjacentToRange(range) + { + var table; + + table = tableAtLeftOfRange(range); + if ((table != null) && (table.numCols >= 2)) { + UndoManager_newGroup("Delete one column"); + var col = table.numCols-1; + Tables_deleteRegion(new TableRegion(table,0,table.numRows-1,col,col)); + UndoManager_newGroup(); + return; + } + + table = tableAtRightOfRange(range); + if ((table != null) && (table.numCols >= 2)) { + UndoManager_newGroup("Delete one column"); + Tables_deleteRegion(new TableRegion(table,0,table.numRows-1,0,0)); + UndoManager_newGroup(); + return; + } + } + + Tables_removeAdjacentColumn = function() + { + var range = Selection_get(); + var region = Tables_regionFromRange(range,true); + + if (region == null) { + removeColumnAdjacentToRange(range); + return; + } + + if (region.structure.numCols <= 1) + return; + + UndoManager_newGroup("Delete one column"); + + var table = region.structure; + var left = region.left; + var right = region.right; + var top = region.top; + var bottom = region.bottom; + + // Is there an empty column to the right of the selection? If so, delete it + if ((right+1 < table.numCols) && !columnHasContent(table,right+1)) { + Selection_preserveWhileExecuting(function() { + Tables_deleteRegion(new TableRegion(table,0,table.numRows-1,right+1,right+1)); + }); + } + + // Is there an empty column to the left of the selection? If so, delete it + else if ((left-1 >= 0) && !columnHasContent(table,left-1)) { + Selection_preserveWhileExecuting(function() { + Tables_deleteRegion(new TableRegion(table,0,table.numRows-1,left-1,left-1)); + }); + } + + // There are no empty columns adjacent to the selection. Delete the right-most column + // of the selection (which may be the only one) + else { + Selection_preserveWhileExecuting(function() { + Tables_deleteRegion(new TableRegion(table,0,table.numRows-1,right,right)); + }); + + table = Tables_analyseStructure(table.element); + var multiple = (left != right); + + if (multiple) { + selectRegion(table,top,bottom,left,right-1); + } + else { + var newCol = clampCol(table,right); + var newCell = Table_get(table,top,newCol); + if (newCell != null) { + var pos = new Position(newCell.element,0); + pos = Position_closestMatchForwards(pos,Position_okForMovement); + Selection_set(pos.node,pos.offset,pos.node,pos.offset); + } + } + } + + UndoManager_newGroup(); + } + + // private + function deleteTable(structure) + { + DOM_deleteNode(structure.element); + } + + // private + function deleteRows(structure,top,bottom) + { + var trElements = new Array(); + getTRs(structure.element,trElements); + + for (var row = top; row <= bottom; row++) + DOM_deleteNode(trElements[row]); + } + + // private + function getTRs(node,result) + { + if (node._type == HTML_TR) { + result.push(node); + } + else { + for (var child = node.firstChild; child != null; child = child.nextSibling) + getTRs(child,result); + } + } + + // private + function deleteColumns(structure,left,right) + { + var nodesToDelete = new NodeSet(); + for (var row = 0; row < structure.numRows; row++) { + for (var col = left; col <= right; col++) { + var cell = Table_get(structure,row,col); + nodesToDelete.add(cell.element); + } + } + nodesToDelete.forEach(DOM_deleteNode); + deleteColElements(structure,left,right); + } + + // private + function deleteCellContents(region) + { + var structure = region.structure; + for (var row = region.top; row <= region.bottom; row++) { + for (var col = region.left; col <= region.right; col++) { + var cell = Table_get(structure,row,col); + DOM_deleteAllChildren(cell.element); + } + } + } + + // public + Tables_deleteRegion = function(region) + { + var structure = region.structure; + + var coversEntireWidth = (region.left == 0) && (region.right == structure.numCols-1); + var coversEntireHeight = (region.top == 0) && (region.bottom == structure.numRows-1); + + if (coversEntireWidth && coversEntireHeight) + deleteTable(region.structure); + else if (coversEntireWidth) + deleteRows(structure,region.top,region.bottom); + else if (coversEntireHeight) + deleteColumns(structure,region.left,region.right); + else + deleteCellContents(region); + } + + // public + Tables_clearCells = function() + { + } + + // public + Tables_mergeCells = function() + { + Selection_preserveWhileExecuting(function() { + var region = Tables_regionFromRange(Selection_get()); + if (region == null) + return; + + var structure = region.structure; + + // FIXME: handle the case of missing cells + // (or even better, add cells where there are some missing) + + for (var row = region.top; row <= region.bottom; row++) { + for (var col = region.left; col <= region.right; col++) { + var cell = Table_get(structure,row,col); + var cellFirstRow = cell.row; + var cellLastRow = cell.row + cell.rowspan - 1; + var cellFirstCol = cell.col; + var cellLastCol = cell.col + cell.colspan - 1; + + if ((cellFirstRow < region.top) || (cellLastRow > region.bottom) || + (cellFirstCol < region.left) || (cellLastCol > region.right)) { + debug("Can't merge this table: cell at "+row+","+col+ + " goes outside bounds of selection"); + return; + } + } + } + + var mergedCell = Table_get(structure,region.top,region.left); + + for (var row = region.top; row <= region.bottom; row++) { + for (var col = region.left; col <= region.right; col++) { + var cell = Table_get(structure,row,col); + // parentNode will be null if we've already done this cell + if ((cell != mergedCell) && (cell.element.parentNode != null)) { + while (cell.element.firstChild != null) + DOM_appendChild(mergedCell.element,cell.element.firstChild); + DOM_deleteNode(cell.element); + } + } + } + + var totalRows = region.bottom - region.top + 1; + var totalCols = region.right - region.left + 1; + if (totalRows == 1) + DOM_removeAttribute(mergedCell.element,"rowspan"); + else + DOM_setAttribute(mergedCell.element,"rowspan",totalRows); + if (totalCols == 1) + DOM_removeAttribute(mergedCell.element,"colspan"); + else + DOM_setAttribute(mergedCell.element,"colspan",totalCols); + }); + } + + // public + Tables_splitSelection = function() + { + Selection_preserveWhileExecuting(function() { + var range = Selection_get(); + Range_trackWhileExecuting(range,function() { + var region = Tables_regionFromRange(range,true); + if (region != null) + TableRegion_splitCells(region); + }); + }); + } + + // public + TableRegion_splitCells = function(region) + { + var structure = region.structure; + var trElements = new Array(); + getTRs(structure.element,trElements); + + for (var row = region.top; row <= region.bottom; row++) { + for (var col = region.left; col <= region.right; col++) { + var cell = Table_get(structure,row,col); + if ((cell.rowspan > 1) || (cell.colspan > 1)) { + + var original = cell.element; + + for (var r = cell.top; r <= cell.bottom; r++) { + for (var c = cell.left; c <= cell.right; c++) { + if ((r == cell.top) && (c == cell.left)) + continue; + var newTD = createEmptyTableCell(original.nodeName); // check-ok + var nextElement = null; + + var nextCol = cell.right+1; + while (nextCol < structure.numCols) { + var nextCell = Table_get(structure,r,nextCol); + if ((nextCell != null) && (nextCell.row == r)) { + nextElement = nextCell.element; + break; + } + nextCol++; + } + + DOM_insertBefore(trElements[r],newTD,nextElement); + Table_set(structure,r,c,new Cell(newTD,r,c)); + } + } + DOM_removeAttribute(original,"rowspan"); + DOM_removeAttribute(original,"colspan"); + } + } + } + } + + // public + Tables_cloneRegion = function(region) + { + var cellNodesDone = new NodeSet(); + var table = DOM_shallowCopyElement(region.structure.element); + for (var row = region.top; row <= region.bottom; row++) { + var tr = DOM_createElement(document,"TR"); + DOM_appendChild(table,tr); + for (var col = region.left; col <= region.right; col++) { + var cell = Table_get(region.structure,row,col); + if (!cellNodesDone.contains(cell.element)) { + DOM_appendChild(tr,DOM_cloneNode(cell.element,true)); + cellNodesDone.add(cell.element); + } + } + } + return table; + } + + // private + function pasteCells(fromTableElement,toRegion) + { + // FIXME + var fromStructure = Tables_analyseStructure(fromTableElement); + } + + // public + Table_fix = function(table) + { + var changed = false; + + var tbody = null; + for (var child = table.element.firstChild; child != null; child = child.nextSibling) { + if (child._type == HTML_TBODY) + tbody = child; + } + + if (tbody == null) + return table; // FIXME: handle presence of THEAD and TFOOT, and also a missing TBODY + + var trs = new Array(); + for (var child = tbody.firstChild; child != null; child = child.nextSibling) { + if (child._type == HTML_TR) + trs.push(child); + } + + while (trs.length < table.numRows) { + var tr = DOM_createElement(document,"TR"); + DOM_appendChild(tbody,tr); + trs.push(tr); + } + + for (var row = 0; row < table.numRows; row++) { + for (var col = 0; col < table.numCols; col++) { + var cell = Table_get(table,row,col); + if (cell == null) { + var td = createEmptyTableCell("TD"); + DOM_appendChild(trs[row],td); + changed = true; + } + } + } + + if (changed) + return new Table(table.element); + else + return table; + } + + // public + Table_fixColumnWidths = function(structure) + { + var colElements = getColElements(structure.element); + if (colElements.length == 0) + return; + addMissingColElements(structure,colElements); + + var widths = Tables_getColWidths(structure); + fixWidths(widths,structure.numCols); + colElements = getColElements(structure.element); + for (var i = 0; i < widths.length; i++) + DOM_setAttribute(colElements[i],"width",widths[i]+"%"); + } + + // public + Tables_analyseStructure = function(element) + { + // FIXME: we should probably be preserving the selection here, since we are modifying + // the DOM (though I think it's unlikely it would cause problems, becausing the fixup + // logic only adds elements). However this method is called (indirectly) from within + // Selection_update(), which causes unbounded recursion due to the subsequent Selecton_set() + // that occurs. + var initial = new Table(element); + var fixed = Table_fix(initial); + return fixed; + } + + // public + Tables_findContainingCell = function(node) + { + for (var ancestor = node; ancestor != null; ancestor = ancestor.parentNode) { + if (isTableCell(ancestor)) + return ancestor; + } + return null; + } + + // public + Tables_findContainingTable = function(node) + { + for (var ancestor = node; ancestor != null; ancestor = ancestor.parentNode) { + if (ancestor._type == HTML_TABLE) + return ancestor; + } + return null; + } + + function TableRegion(structure,top,bottom,left,right) + { + this.structure = structure; + this.top = top; + this.bottom = bottom; + this.left = left; + this.right = right; + } + + TableRegion.prototype.toString = function() + { + return "("+this.top+","+this.left+") - ("+this.bottom+","+this.right+")"; + } + + // public + Tables_regionFromRange = function(range,allowSameCell) + { + var region = null; + + if (range == null) + return null; + + var start = Position_closestActualNode(range.start,true); + var end = Position_closestActualNode(range.end,true); + + var startTD = Tables_findContainingCell(start); + var endTD = Tables_findContainingCell(end); + + if (!isTableCell(start) || !isTableCell(end)) { + if (!allowSameCell) { + if (startTD == endTD) // not in cell, or both in same cell + return null; + } + } + + if ((startTD == null) || (endTD == null)) + return null; + + var startTable = Tables_findContainingTable(startTD); + var endTable = Tables_findContainingTable(endTD); + + if (startTable != endTable) + return null; + + var structure = Tables_analyseStructure(startTable); + + var startInfo = structure.cellsByElement.get(startTD); + var endInfo = structure.cellsByElement.get(endTD); + + var startTopRow = startInfo.row; + var startBottomRow = startInfo.row + startInfo.rowspan - 1; + var startLeftCol = startInfo.col; + var startRightCol = startInfo.col + startInfo.colspan - 1; + + var endTopRow = endInfo.row; + var endBottomRow = endInfo.row + endInfo.rowspan - 1; + var endLeftCol = endInfo.col; + var endRightCol = endInfo.col + endInfo.colspan - 1; + + var top = (startTopRow < endTopRow) ? startTopRow : endTopRow; + var bottom = (startBottomRow > endBottomRow) ? startBottomRow : endBottomRow; + var left = (startLeftCol < endLeftCol) ? startLeftCol : endLeftCol; + var right = (startRightCol > endRightCol) ? startRightCol : endRightCol; + + var region = new TableRegion(structure,top,bottom,left,right); + adjustRegionForSpannedCells(region); + return region; + } + + // private + function adjustRegionForSpannedCells(region) + { + var structure = region.structure; + var boundariesOk; + var columnsOk; + do { + boundariesOk = true; + for (var row = region.top; row <= region.bottom; row++) { + var cell = Table_get(structure,row,region.left); + if (region.left > cell.left) { + region.left = cell.left; + boundariesOk = false; + } + cell = Table_get(structure,row,region.right); + if (region.right < cell.right) { + region.right = cell.right; + boundariesOk = false; + } + } + + for (var col = region.left; col <= region.right; col++) { + var cell = Table_get(structure,region.top,col); + if (region.top > cell.top) { + region.top = cell.top; + boundariesOk = false; + } + cell = Table_get(structure,region.bottom,col); + if (region.bottom < cell.bottom) { + region.bottom = cell.bottom; + boundariesOk = false; + } + } + } while (!boundariesOk); + } + + Tables_getSelectedTableId = function() + { + var element = Cursor_getAdjacentNodeWithType(HTML_TABLE); + return element ? element.getAttribute("id") : null; + } + + Tables_getProperties = function(itemId) + { + var element = document.getElementById(itemId); + if ((element == null) || (element._type != HTML_TABLE)) + return null; + var structure = Tables_analyseStructure(element); + var width = element.style.width; + return { width: width, rows: structure.numRows, cols: structure.numCols }; + } + + Tables_setProperties = function(itemId,width) + { + var table = document.getElementById(itemId); + if (table == null) + return null; + DOM_setStyleProperties(table,{ width: width }); + Selection_update(); // ensure cursor/selection drawn in correct pos + } + + // Returns an array of numbers representing the percentage widths (0 - 100) of each + // column. This works on the assumption that all tables are supposed to have all of + // their column widths specified, and in all cases as percentages. Any which do not + // are considered invalid, and have any non-percentage values filled in based on the + // average values of all valid percentage-based columns. + Tables_getColWidths = function(structure) + { + var colElements = getColElements(structure.element); + var colWidths = new Array(); + + for (var i = 0; i < structure.numCols; i++) { + var value = null; + + if (i < colElements.length) { + var widthStr = DOM_getAttribute(colElements[i],"width"); + if (widthStr != null) { + value = parsePercentage(widthStr); + } + } + + if ((value != null) && (value >= 1.0)) { + colWidths[i] = value; + } + else { + colWidths[i] = null; + } + } + + fixWidths(colWidths,structure.numCols); + + return colWidths; + + function parsePercentage(str) + { + if (str.match(/^\s*\d+(\.\d+)?\s*%\s*$/)) + return parseFloat(str.replace(/\s*%\s*$/,"")); + else + return null; + } + } + + function fixWidths(colWidths,numCols) + { + var totalWidth = 0; + var numValidCols = 0; + for (var i = 0; i < numCols; i++) { + if (colWidths[i] != null) { + totalWidth += colWidths[i]; + numValidCols++; + } + } + + var averageWidth = (numValidCols > 0) ? totalWidth/numValidCols : 1.0; + for (var i = 0; i < numCols; i++) { + if (colWidths[i] == null) { + colWidths[i] = averageWidth; + totalWidth += averageWidth; + } + } + + // To cater for the case where the column widths do not all add up to 100%, + // recalculate all of them based on their value relative to the total width + // of all columns. For example, if there are three columns of 33%, 33%, and 33%, + // these will get rounded up to 33.33333.....%. + // If there are no column widths defined, each will have 100/numCols%. + if (totalWidth > 0) { + for (var i = 0; i < numCols; i++) { + colWidths[i] = 100.0*colWidths[i]/totalWidth; + } + } + } + + // public + Tables_setColWidths = function(itemId,widths) + { + var element = document.getElementById(itemId); + if (element == null) + return null; + + var structure = Tables_analyseStructure(element); + + fixWidths(widths,structure.numCols); + + var colElements = getColElements(element); + for (var i = 0; i < widths.length; i++) + DOM_setAttribute(colElements[i],"width",widths[i]+"%"); + + Selection_update(); + } + + // public + Tables_getGeometry = function(itemId) + { + var element = document.getElementById(itemId); + if ((element == null) || (element.parentNode == null)) + return null; + + var structure = Tables_analyseStructure(element); + + var result = new Object(); + + // Calculate the rect based on the cells, not the whole table element; + // we want to ignore the caption + var topLeftCell = Table_get(structure,0,0); + var bottomRightCell = Table_get(structure,structure.numRows-1,structure.numCols-1); + + if (topLeftCell == null) + throw new Error("No top left cell"); + if (bottomRightCell == null) + throw new Error("No bottom right cell"); + + var topLeftRect = topLeftCell.element.getBoundingClientRect(); + var bottomRightRect = bottomRightCell.element.getBoundingClientRect(); + + var left = topLeftRect.left + window.scrollX; + var right = bottomRightRect.right + window.scrollX; + var top = topLeftRect.top + window.scrollY; + var bottom = bottomRightRect.bottom + window.scrollY; + + result.contentRect = { x: left, y: top, width: right - left, height: bottom - top }; + result.fullRect = xywhAbsElementRect(element); + result.parentRect = xywhAbsElementRect(element.parentNode); + + result.columnWidths = Tables_getColWidths(structure); + + var caption = firstChildOfType(element,HTML_CAPTION); + result.hasCaption = (caption != null); + + return result; + + } + +})(); http://git-wip-us.apache.org/repos/asf/incubator-corinthia/blob/9bf02bb2/experiments/editorFramework/src/Javascript_Layer_0/Text.js ---------------------------------------------------------------------- diff --git a/experiments/editorFramework/src/Javascript_Layer_0/Text.js b/experiments/editorFramework/src/Javascript_Layer_0/Text.js new file mode 100644 index 0000000..5a69feb --- /dev/null +++ b/experiments/editorFramework/src/Javascript_Layer_0/Text.js @@ -0,0 +1,543 @@ +// 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. + +var Text_findParagraphBoundaries; +var Text_analyseParagraph; +var Text_posAbove; +var Text_posBelow; +var Text_closestPosBackwards; +var Text_closestPosForwards; +var Text_closestPosInDirection; + +var Paragraph_runFromOffset; +var Paragraph_runFromNode; +var Paragraph_positionAtOffset; +var Paragraph_offsetAtPosition; +var Paragraph_getRunRects; +var Paragraph_getRunOrFallbackRects; + +var Text_toStartOfBoundary; +var Text_toEndOfBoundary; + +(function() { + + function Paragraph(node,startOffset,endOffset,runs,text) + { + this.node = node; + this.startOffset = startOffset; + this.endOffset = endOffset; + this.runs = runs; + this.text = text; + + Object.defineProperty(this,"first",{ + get: function() { throw new Error("Attempt to access first property of Position") }, + set: function() {}, + enumerable: true }); + Object.defineProperty(this,"last",{ + get: function() { throw new Error("Attempt to access last property of Position") }, + set: function() {}, + enumerable: true }); + } + + function Run(node,start,end) + { + this.node = node; + this.start = start; + this.end = end; + } + + // In this code, we represent a paragraph by its first and last node. Normally, this will be + // the first and last child of a paragraph-level element (e.g. p or h1), but this scheme also + // represent a sequence of inline nodes between two paragraph or container nodes, e.g. + // + //

...

Some inline nodes

...

+ + Text_findParagraphBoundaries = function(pos) + { + Position_assertValid(pos); + var startOffset = pos.offset; + var endOffset = pos.offset; + var node = pos.node; + + while (isInlineNode(node)) { + startOffset = DOM_nodeOffset(node); + endOffset = DOM_nodeOffset(node)+1; + node = node.parentNode; + } + + if (node.nodeType != Node.ELEMENT_NODE) + throw new Error("Not an element node: "+nodeString(node)); + + while ((startOffset > 0) && isInlineNode(node.childNodes[startOffset-1])) + startOffset--; + while ((endOffset < node.childNodes.length) && isInlineNode(node.childNodes[endOffset])) + endOffset++; + + return { node: node, startOffset: startOffset, endOffset: endOffset }; + } + + Text_analyseParagraph = function(pos) + { + var initial = pos.node; + var strings = new Array(); + var runs = new Array(); + var offset = 0; + + var boundaries = Text_findParagraphBoundaries(pos); + if (boundaries == null) + return null; + + for (var off = boundaries.startOffset; off < boundaries.endOffset; off++) + recurse(boundaries.node.childNodes[off]); + + var text = strings.join(""); + + return new Paragraph(boundaries.node,boundaries.startOffset,boundaries.endOffset,runs,text); + + function recurse(node) + { + if (node.nodeType == Node.TEXT_NODE) { + strings.push(node.nodeValue); + var start = offset; + var end = offset + node.nodeValue.length; + runs.push(new Run(node,start,end)); + offset += node.nodeValue.length; + } + for (var child = node.firstChild; child != null; child = child.nextSibling) + recurse(child); + } + } + + Text_posAbove = function(pos,cursorRect,cursorX) + { + if (cursorX == null) + cursorX = pos.targetX; + pos = Position_closestMatchBackwards(pos,Position_okForMovement); + if (cursorRect == null) { + cursorRect = Position_rectAtPos(pos); + if (cursorRect == null) + return null; + } + + if (cursorX == null) { + cursorX = cursorRect.left; + } + + while (true) { + pos = Position_closestMatchBackwards(pos,Position_okForMovement); + if (pos == null) + return null; + + var paragraph = Text_analyseParagraph(pos); + if (paragraph == null) + return null; + + var rects = Paragraph_getRunOrFallbackRects(paragraph,pos); + + rects = rects.filter(function (rect) { + return (rect.bottom <= cursorRect.top); + }); + + + + var bottom = findLowestBottom(rects); + + rects = rects.filter(function (rect) { return (rect.bottom == bottom); }); + + // Scroll previous line into view, if necessary + var top = findHighestTop(rects); + if (top < 0) { + var offset = -top; + window.scrollBy(0,-offset); + rects = offsetRects(rects,0,offset); + } + + for (var i = 0; i < rects.length; i++) { + if ((cursorX >= rects[i].left) && (cursorX <= rects[i].right)) { + var newPos = Position_atPoint(cursorX,rects[i].top + rects[i].height/2); + if (newPos != null) { + newPos = Position_closestMatchBackwards(newPos,Position_okForInsertion); + newPos.targetX = cursorX; + return newPos; + } + } + } + + var rightMost = findRightMostRect(rects); + if (rightMost != null) { + var newPos = Position_atPoint(rightMost.right,rightMost.top + rightMost.height/2); + if (newPos != null) { + newPos = Position_closestMatchBackwards(newPos,Position_okForInsertion); + newPos.targetX = cursorX; + return newPos; + } + } + + + pos = new Position(paragraph.node,paragraph.startOffset); + pos = Position_prevMatch(pos,Position_okForMovement); + } + } + + var findHighestTop = function(rects) + { + var top = null; + for (var i = 0; i < rects.length; i++) { + if ((top == null) || (top > rects[i].top)) + top = rects[i].top; + } + return top; + } + + var findLowestBottom = function(rects) + { + var bottom = null; + for (var i = 0; i < rects.length; i++) { + if ((bottom == null) || (bottom < rects[i].bottom)) + bottom = rects[i].bottom; + } + return bottom; + } + + var findRightMostRect = function(rects) + { + var rightMost = null; + for (var i = 0; i < rects.length; i++) { + if ((rightMost == null) || (rightMost.right < rects[i].right)) + rightMost = rects[i]; + } + return rightMost; + } + + var offsetRects = function(rects,offsetX,offsetY) + { + var result = new Array(); + for (var i = 0; i < rects.length; i++) { + result.push({ top: rects[i].top + offsetY, + bottom: rects[i].bottom + offsetY, + left: rects[i].left + offsetX, + right: rects[i].right + offsetX, + width: rects[i].width, + height: rects[i].height }); + } + return result; + } + + Text_posBelow = function(pos,cursorRect,cursorX) + { + if (cursorX == null) + cursorX = pos.targetX; + pos = Position_closestMatchForwards(pos,Position_okForMovement); + if (cursorRect == null) { + cursorRect = Position_rectAtPos(pos); + if (cursorRect == null) + return null; + } + + if (cursorX == null) { + cursorX = cursorRect.left; + } + + + while (true) { + pos = Position_closestMatchForwards(pos,Position_okForMovement); + if (pos == null) + return null; + + var paragraph = Text_analyseParagraph(pos); + if (paragraph == null) + return null; + + var rects = Paragraph_getRunOrFallbackRects(paragraph,pos); + + rects = rects.filter(function (rect) { + return (rect.top >= cursorRect.bottom); + }); + + var top = findHighestTop(rects); + + rects = rects.filter(function (rect) { return (rect.top == top); }); + + // Scroll next line into view, if necessary + var bottom = findLowestBottom(rects); + if (bottom > window.innerHeight) { + var offset = window.innerHeight - bottom; + window.scrollBy(0,-offset); + rects = offsetRects(rects,0,offset); + } + + for (var i = 0; i < rects.length; i++) { + if ((cursorX >= rects[i].left) && (cursorX <= rects[i].right)) { + var newPos = Position_atPoint(cursorX,rects[i].top + rects[i].height/2); + if (newPos != null) { + newPos = Position_closestMatchForwards(newPos,Position_okForInsertion); + newPos.targetX = cursorX; + return newPos; + } + } + } + + var rightMost = findRightMostRect(rects); + if (rightMost != null) { + var newPos = Position_atPoint(rightMost.right,rightMost.top + rightMost.height/2); + if (newPos != null) { + newPos = Position_closestMatchForwards(newPos,Position_okForInsertion); + newPos.targetX = cursorX; + return newPos; + } + } + + pos = new Position(paragraph.node,paragraph.endOffset); + pos = Position_nextMatch(pos,Position_okForMovement); + } + } + + Text_closestPosBackwards = function(pos) + { + if (isNonWhitespaceTextNode(pos.node)) + return pos; + var node; + if ((pos.node.nodeType == Node.ELEMENT_NODE) && (pos.offset > 0)) { + node = pos.node.childNodes[pos.offset-1]; + while (node.lastChild != null) + node = node.lastChild; + } + else { + node = pos.node; + } + while ((node != null) && (node != document.body) && !isNonWhitespaceTextNode(node)) + node = prevNode(node); + + if ((node == null) || (node == document.body)) + return null; + else + return new Position(node,node.nodeValue.length); + } + + Text_closestPosForwards = function(pos) + { + if (isNonWhitespaceTextNode(pos.node)) + return pos; + var node; + if ((pos.node.nodeType == Node.ELEMENT_NODE) && (pos.offset < pos.node.childNodes.length)) { + node = pos.node.childNodes[pos.offset]; + while (node.firstChild != null) + node = node.firstChild; + } + else { + node = nextNodeAfter(pos.node); + } + while ((node != null) && !isNonWhitespaceTextNode(node)) { + var old = nodeString(node); + node = nextNode(node); + } + + if (node == null) + return null; + else + return new Position(node,0); + } + + Text_closestPosInDirection = function(pos,direction) + { + if ((direction == "forward") || + (direction == "right") || + (direction == "down")) { + return Text_closestPosForwards(pos); + } + else { + return Text_closestPosBackwards(pos); + } + } + + Paragraph_runFromOffset = function(paragraph,offset,end) + { + if (paragraph.runs.length == 0) + throw new Error("Paragraph has no runs"); + if (!end) { + + for (var i = 0; i < paragraph.runs.length; i++) { + var run = paragraph.runs[i]; + if ((offset >= run.start) && (offset < run.end)) + return run; + if ((i == paragraph.runs.length-1) && (offset == run.end)) + return run; + } + + } + else { + + for (var i = 0; i < paragraph.runs.length; i++) { + var run = paragraph.runs[i]; + if ((offset > run.start) && (offset <= run.end)) + return run; + if ((i == 0) && (offset == 0)) + return run; + } + + } + } + + Paragraph_runFromNode = function(paragraph,node) + { + for (var i = 0; i < paragraph.runs.length; i++) { + if (paragraph.runs[i].node == node) + return paragraph.runs[i]; + } + throw new Error("Run for text node not found"); + } + + Paragraph_positionAtOffset = function(paragraph,offset,end) + { + var run = Paragraph_runFromOffset(paragraph,offset,end); + if (run == null) + throw new Error("Run at offset "+offset+" not found"); + return new Position(run.node,offset-run.start); + } + + Paragraph_offsetAtPosition = function(paragraph,pos) + { + var run = Paragraph_runFromNode(paragraph,pos.node); + return run.start + pos.offset; + } + + Paragraph_getRunRects = function(paragraph) + { + var rects = new Array(); + for (var i = 0; i < paragraph.runs.length; i++) { + var run = paragraph.runs[i]; + var runRange = new Range(run.node,0,run.node,run.node.nodeValue.length); + var runRects = Range_getClientRects(runRange); + Array.prototype.push.apply(rects,runRects); + } + return rects; + } + + Paragraph_getRunOrFallbackRects = function(paragraph,pos) + { + var rects = Paragraph_getRunRects(paragraph); + if ((rects.length == 0) && (paragraph.node.nodeType == Node.ELEMENT_NODE)) { + if (isBlockNode(paragraph.node) && + (paragraph.startOffset == 0) && + (paragraph.endOffset == paragraph.node.childNodes.length)) { + rects = [paragraph.node.getBoundingClientRect()]; + } + else { + var beforeNode = paragraph.node.childNodes[paragraph.startOffset-1]; + var afterNode = paragraph.node.childNodes[paragraph.endOffset]; + if ((afterNode != null) && isBlockNode(afterNode)) { + rects = [afterNode.getBoundingClientRect()]; + } + else if ((beforeNode != null) && isBlockNode(beforeNode)) { + rects = [beforeNode.getBoundingClientRect()]; + } + } + } + return rects; + } + + function toStartOfParagraph(pos) + { + pos = Position_closestMatchBackwards(pos,Position_okForMovement); + if (pos == null) + return null; + var paragraph = Text_analyseParagraph(pos); + if (paragraph == null) + return null; + + var newPos = new Position(paragraph.node,paragraph.startOffset); + return Position_closestMatchForwards(newPos,Position_okForMovement); + } + + function toEndOfParagraph(pos) + { + pos = Position_closestMatchForwards(pos,Position_okForMovement); + if (pos == null) + return null; + var paragraph = Text_analyseParagraph(pos); + if (paragraph == null) + return null; + + var newPos = new Position(paragraph.node,paragraph.endOffset); + return Position_closestMatchBackwards(newPos,Position_okForMovement); + } + + function toStartOfLine(pos) + { + var posRect = Position_rectAtPos(pos); + if (posRect == null) { + pos = Text_closestPosBackwards(pos); + posRect = Position_rectAtPos(pos); + if (posRect == null) { + return null; + } + } + + while (true) { + var check = Position_prevMatch(pos,Position_okForMovement); + var checkRect = Position_rectAtPos(check); // handles check == null case + if (checkRect == null) + return pos; + if ((checkRect.bottom <= posRect.top) || (checkRect.top >= posRect.bottom)) + return pos; + pos = check; + } + } + + function toEndOfLine(pos) + { + var posRect = Position_rectAtPos(pos); + if (posRect == null) { + pos = Text_closestPosForwards(pos); + posRect = Position_rectAtPos(pos); + if (posRect == null) { + return null; + } + } + + while (true) { + var check = Position_nextMatch(pos,Position_okForMovement); + var checkRect = Position_rectAtPos(check); // handles check == null case + if (checkRect == null) + return pos; + if ((checkRect.bottom <= posRect.top) || (checkRect.top >= posRect.bottom)) + return pos; + pos = check; + } + } + + Text_toStartOfBoundary = function(pos,boundary) + { + if (boundary == "paragraph") + return toStartOfParagraph(pos); + else if (boundary == "line") + return toStartOfLine(pos); + else + throw new Error("Unsupported boundary: "+boundary); + } + + Text_toEndOfBoundary = function(pos,boundary) + { + if (boundary == "paragraph") + return toEndOfParagraph(pos); + else if (boundary == "line") + return toEndOfLine(pos); + else + throw new Error("Unsupported boundary: "+boundary); + } + +})(); http://git-wip-us.apache.org/repos/asf/incubator-corinthia/blob/9bf02bb2/experiments/editorFramework/src/Javascript_Layer_0/UndoManager.js ---------------------------------------------------------------------- diff --git a/experiments/editorFramework/src/Javascript_Layer_0/UndoManager.js b/experiments/editorFramework/src/Javascript_Layer_0/UndoManager.js new file mode 100644 index 0000000..9f63c43 --- /dev/null +++ b/experiments/editorFramework/src/Javascript_Layer_0/UndoManager.js @@ -0,0 +1,270 @@ +// 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. + +// FIXME: place a limit on the number of undo steps recorded - say, 30-50? + +var UndoManager_getLength; +var UndoManager_getIndex; +var UndoManager_setIndex; +var UndoManager_print; +var UndoManager_undo; +var UndoManager_redo; +var UndoManager_addAction; +var UndoManager_newGroup; +var UndoManager_groupType; +var UndoManager_disableWhileExecuting; +var UndoManager_isActive; +var UndoManager_isDisabled; +var UndoManager_clear; +var UndoManager_setProperty; +var UndoManager_deleteProperty; + +(function() { + + var UNDO_LIMIT = 50; + + function UndoGroup(type,onClose) + { + this.type = type; + this.onClose = onClose; + this.actions = new Array(); + } + + function UndoAction(fun,args) + { + this.fun = fun; + this.args = args; + } + + UndoAction.prototype.toString = function() + { + var name; + if (this.fun.wrappedName != null) + name = this.fun.wrappedName; + else + name = this.fun.name; + + var argStrings = new Array(); + for (var i = 0; i < this.args.length; i++) { + if (this.args[i] instanceof Node) + argStrings.push(nodeString(this.args[i])); + else if (this.args[i] == null) + argStrings.push("null"); + else + argStrings.push(this.args[i].toString()); + } + + return name + "(" + argStrings.join(",") + ")"; + } + + var undoStack = new Array(); + var redoStack = new Array(); + var inUndo = false; + var inRedo = false; + var currentGroup = null; + var disabled = 0; + + // public + UndoManager_getLength = function() + { + return undoStack.length + redoStack.length; + } + + // public + UndoManager_getIndex = function() + { + return undoStack.length; + } + + // public + UndoManager_setIndex = function(index) + { + while (undoStack.length > index) + UndoManager_undo(); + while (undoStack.length < index) + UndoManager_redo(); + } + + // public + UndoManager_print = function() + { + debug(""); + debug("--------------------------------------------------------------------"); + debug("Undo stack:"); + for (var groupIndex = 0; groupIndex < undoStack.length; groupIndex++) { + var group = undoStack[groupIndex]; + debug(" "+group.type); + for (var actionIndex = 0; actionIndex < group.actions.length; actionIndex++) { + var action = group.actions[actionIndex]; + debug(" "+action); + } + } + debug("Redo stack:"); + for (var groupIndex = 0; groupIndex < redoStack.length; groupIndex++) { + var group = redoStack[groupIndex]; + debug(" "+group.type); + for (var actionIndex = 0; actionIndex < group.actions.length; actionIndex++) { + var action = group.actions[actionIndex]; + debug(" "+action); + } + } + debug("Current group = "+currentGroup); + debug("--------------------------------------------------------------------"); + debug(""); + } + + function closeCurrentGroup() + { + if ((currentGroup != null) && (currentGroup.onClose != null)) + currentGroup.onClose(); + currentGroup = null; + } + + // public + UndoManager_undo = function() + { + closeCurrentGroup(); + if (undoStack.length > 0) { + var group = undoStack.pop(); + inUndo = true; + for (var i = group.actions.length-1; i >= 0; i--) + group.actions[i].fun.apply(null,group.actions[i].args); + inUndo = false; + } + closeCurrentGroup(); + } + + // public + UndoManager_redo = function() + { + closeCurrentGroup(); + if (redoStack.length > 0) { + var group = redoStack.pop(); + inRedo = true; + for (var i = group.actions.length-1; i >= 0; i--) + group.actions[i].fun.apply(null,group.actions[i].args); + inRedo = false; + } + closeCurrentGroup(); + } + + // public + UndoManager_addAction = function(fun) + { + if (disabled > 0) + return; + + // remaining parameters after fun are arguments to be supplied to fun + var args = new Array(); + for (var i = 1; i < arguments.length; i++) + args.push(arguments[i]); + + if (!inUndo && !inRedo && (redoStack.length > 0)) + redoStack.length = 0; + + var stack = inUndo ? redoStack : undoStack; + if (currentGroup == null) + UndoManager_newGroup(null); + + // Only add a group to the undo stack one it has at least one action, to avoid having + // empty groups present. + if (currentGroup.actions.length == 0) { + if (!inUndo && !inRedo && (stack.length == UNDO_LIMIT)) + stack.shift(); + stack.push(currentGroup); + } + + currentGroup.actions.push(new UndoAction(fun,args)); + } + + // public + UndoManager_newGroup = function(type,onClose) + { + if (disabled > 0) + return; + + closeCurrentGroup(); + + // We don't actually add the group to the undo stack until the first request to add an + // action to it. This way we don't end up with empty groups in the undo stack, which + // simplifies logic for moving back and forward through the undo history. + + if ((type == null) || (type == "")) + type = "Anonymous"; + currentGroup = new UndoGroup(type,onClose); + } + + // public + UndoManager_groupType = function() + { + if (undoStack.length > 0) + return undoStack[undoStack.length-1].type; + else + return null; + } + + UndoManager_disableWhileExecuting = function(fun) { + disabled++; + try { + return fun(); + } + finally { + disabled--; + } + } + + UndoManager_isActive = function() + { + return (inUndo || inRedo); + } + + UndoManager_isDisabled = function() { + return (disabled > 0); + } + + UndoManager_clear = function() { + undoStack.length = 0; + redoStack.length = 0; + } + + function saveProperty(obj,name) + { + if (obj.hasOwnProperty(name)) + UndoManager_addAction(UndoManager_setProperty,obj,name,obj[name]); + else + UndoManager_addAction(UndoManager_deleteProperty,obj,name); + } + + UndoManager_setProperty = function(obj,name,value) + { + if (obj.hasOwnProperty(name) && (obj[name] == value)) + return; // no point in adding an undo action + saveProperty(obj,name); + obj[name] = value; + } + + UndoManager_deleteProperty = function(obj,name) + { + if (!obj.hasOwnProperty(name)) + return; // no point in adding an undo action + saveProperty(obj,name); + delete obj[name]; + } + +})(); + +window.undoSupported = true; http://git-wip-us.apache.org/repos/asf/incubator-corinthia/blob/9bf02bb2/experiments/editorFramework/src/Javascript_Layer_0/Viewport.js ---------------------------------------------------------------------- diff --git a/experiments/editorFramework/src/Javascript_Layer_0/Viewport.js b/experiments/editorFramework/src/Javascript_Layer_0/Viewport.js new file mode 100644 index 0000000..47fbdfd --- /dev/null +++ b/experiments/editorFramework/src/Javascript_Layer_0/Viewport.js @@ -0,0 +1,80 @@ +// 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. + +var Viewport_init; +var Viewport_setViewportWidth; +var Viewport_setTextScale; + +(function() { + + var viewportMetaElement = null; + + // public + Viewport_init = function(width,textScale) + { + var head = DOM_documentHead(document); + for (var child = head.firstChild; child != null; child = child.nextSibling) { + if ((child._type == HTML_META) && (child.getAttribute("name") == "viewport")) { + viewportMetaElement = child; + break; + } + } + + if (viewportMetaElement == null) { + viewportMetaElement = DOM_createElement(document,"META"); + DOM_setAttribute(viewportMetaElement,"name","viewport"); + DOM_appendChild(head,viewportMetaElement); + } + + if (width != 0) { + // Only set the width and text scale if they are not already set, to avoid triggering + // an extra layout at load time + var contentValue = "width = "+width+", user-scalable = no"; + if (viewportMetaElement.getAttribute("content") != contentValue) + DOM_setAttribute(viewportMetaElement,"content",contentValue); + } + + if (textScale != 0) { + var pct = textScale+"%"; + if (document.documentElement.style.getPropertyValue("-webkit-text-size-adjust") != pct) + DOM_setStyleProperties(document.documentElement,{"-webkit-text-size-adjust": pct}); + } + } + + // public + Viewport_setViewportWidth = function(width) + { + var contentValue = "width = "+width+", user-scalable = no"; + if (viewportMetaElement.getAttribute("content") != contentValue) + DOM_setAttribute(viewportMetaElement,"content",contentValue); + + Selection_update(); + Cursor_ensureCursorVisible(); + } + + // public + Viewport_setTextScale = function(textScale) + { + var pct = textScale+"%"; + if (document.documentElement.style.getPropertyValue("-webkit-text-size-adjust") != pct) + DOM_setStyleProperties(document.documentElement,{"-webkit-text-size-adjust": pct}); + + Selection_update(); + Cursor_ensureCursorVisible(); + } + +})(); http://git-wip-us.apache.org/repos/asf/incubator-corinthia/blob/9bf02bb2/experiments/editorFramework/src/Javascript_Layer_0/check-dom-methods.sh ---------------------------------------------------------------------- diff --git a/experiments/editorFramework/src/Javascript_Layer_0/check-dom-methods.sh b/experiments/editorFramework/src/Javascript_Layer_0/check-dom-methods.sh new file mode 100644 index 0000000..6e17a4e --- /dev/null +++ b/experiments/editorFramework/src/Javascript_Layer_0/check-dom-methods.sh @@ -0,0 +1,15 @@ +#!/bin/bash +jsgrep -F '.createElement' | grep -vF '// check-ok' +jsgrep -F '.createTextNode' | grep -vF '// check-ok' +jsgrep -F '.createComment' | grep -vF '// check-ok' +jsgrep -F '.appendChild' | grep -vF '// check-ok' +jsgrep -F '.insertBefore' | grep -vF '// check-ok' +jsgrep -F '.removeChild' | grep -vF '// check-ok' +jsgrep -F '.cloneNode' | grep -vF '// check-ok' +jsgrep -F '.nodeName' | grep -vE '(dtdsource/|tests/|treevis/)' | grep -vF '// check-ok' +jsgrep -F '.setAttribute' | grep -vE '(dtdsource/|treevis/|docx/)' | grep -vF '// check-ok' +jsgrep -F '.removeAttribute' | grep -vE '(dtdsource/|treevis/|docx/)' | grep -vF '// check-ok' +jsgrep -F '.setProperty' | grep -vE '(dtdsource/|treevis/)' | grep -vF '// check-ok' +jsgrep -F '.removeProperty' | grep -vE '(dtdsource/|treevis/)' | grep -vF '// check-ok' +jsgrep -E '\.style\[.* = ' | grep -vE '(treevis/|docx/)' | grep -vF '// check-ok' +jsgrep -E '\.style\..* = ' | grep -vE '(treevis/|docx/)' | grep -vF '// check-ok'