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 AB1D99E70 for ; Wed, 17 Dec 2014 13:29:12 +0000 (UTC) Received: (qmail 36400 invoked by uid 500); 17 Dec 2014 13:29:12 -0000 Delivered-To: apmail-corinthia-commits-archive@corinthia.apache.org Received: (qmail 36387 invoked by uid 500); 17 Dec 2014 13:29:12 -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 36378 invoked by uid 99); 17 Dec 2014 13:29:12 -0000 Received: from athena.apache.org (HELO athena.apache.org) (140.211.11.136) by apache.org (qpsmtpd/0.29) with ESMTP; Wed, 17 Dec 2014 13:29:12 +0000 X-ASF-Spam-Status: No, hits=-2000.0 required=5.0 tests=ALL_TRUSTED,T_RP_MATCHES_RCVD X-Spam-Check-By: apache.org Received: from [140.211.11.3] (HELO mail.apache.org) (140.211.11.3) by apache.org (qpsmtpd/0.29) with SMTP; Wed, 17 Dec 2014 13:28:37 +0000 Received: (qmail 33609 invoked by uid 99); 17 Dec 2014 13:28:13 -0000 Received: from tyr.zones.apache.org (HELO tyr.zones.apache.org) (140.211.11.114) by apache.org (qpsmtpd/0.29) with ESMTP; Wed, 17 Dec 2014 13:28:13 +0000 Received: by tyr.zones.apache.org (Postfix, from userid 65534) id 645CC82E492; Wed, 17 Dec 2014 13:28:13 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit From: pmkelly@apache.org To: commits@corinthia.incubator.apache.org Date: Wed, 17 Dec 2014 13:28:59 -0000 Message-Id: In-Reply-To: <9867ce2e8a674e46a1b0776f45bb10a3@git.apache.org> References: <9867ce2e8a674e46a1b0776f45bb10a3@git.apache.org> X-Mailer: ASF-Git Admin Mailer Subject: [49/92] [abbrv] [partial] incubator-corinthia git commit: Add editing code from UX Write X-Virus-Checked: Checked by ClamAV on apache.org http://git-wip-us.apache.org/repos/asf/incubator-corinthia/blob/03bd5af0/Editor/src/Cursor.js ---------------------------------------------------------------------- diff --git a/Editor/src/Cursor.js b/Editor/src/Cursor.js new file mode 100644 index 0000000..606c4bd --- /dev/null +++ b/Editor/src/Cursor.js @@ -0,0 +1,934 @@ +// Copyright 2011-2014 UX Productivity Pty Ltd +// +// Licensed 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 Cursor_ensurePositionVisible; +var Cursor_ensureCursorVisible; +var Cursor_scrollDocumentForY; +var Cursor_positionCursor; +var Cursor_getCursorPosition; +var Cursor_moveLeft; +var Cursor_moveRight; +var Cursor_moveToStartOfDocument; +var Cursor_moveToEndOfDocument; +var Cursor_updateBRAtEndOfParagraph; +var Cursor_insertReference; +var Cursor_insertLink; +var Cursor_insertCharacter; +var Cursor_deleteCharacter; +var Cursor_enterPressed; +var Cursor_getPrecedingWord; +var Cursor_getAdjacentNodeWithType; +var Cursor_getLinkProperties; +var Cursor_setLinkProperties; +var Cursor_setReferenceTarget; +var Cursor_makeContainerInsertionPoint; +var Cursor_set; +var Cursor_insertFootnote; +var Cursor_insertEndnote; + +(function() { + + var cursorX = null; + + Cursor_ensurePositionVisible = function(pos,center) + { + // If we can't find the cursor rect for some reason, just don't do anything. + // This is better than using an incorrect position or throwing an exception. + var rect = Position_displayRectAtPos(pos) + if (rect != null) { + var extraSpace = 4; + + var cursorTop = rect.top + window.scrollY - extraSpace; + var cursorBottom = rect.top + rect.height + window.scrollY + extraSpace; + + var windowTop = window.scrollY; + var windowBottom = window.scrollY + window.innerHeight; + + if (center) { + var newY = Math.floor(cursorTop + rect.height/2 - window.innerHeight/2); + window.scrollTo(window.scrollX,newY); + } + else if (cursorTop < windowTop) { + window.scrollTo(window.scrollX,cursorTop); + } + else if (cursorBottom > windowBottom) { + window.scrollTo(window.scrollX,cursorBottom - window.innerHeight); + } + } + } + + // public + Cursor_ensureCursorVisible = function(center) + { + var selRange = Selection_get(); + if (selRange != null) + Cursor_ensurePositionVisible(selRange.end,center); + } + + Cursor_scrollDocumentForY = function(y) + { + var absY = window.scrollY + y; + if (absY-44 < window.scrollY) { + window.scrollTo(window.scrollX,absY-44); + y = absY - window.scrollY; + } + else if (absY+44 >= window.scrollY + window.innerHeight) { + window.scrollTo(window.scrollX,absY+44 - window.innerHeight); + y = absY - window.scrollY; + } + return y; + } + + // public + Cursor_positionCursor = function(x,y,wordBoundary) + { + if (UndoManager_groupType() != "Cursor movement") + UndoManager_newGroup("Cursor movement"); + + y = Cursor_scrollDocumentForY(y); + + var result = null; + var position = Position_atPoint(x,y); + if (position == null) + return null; + + var node = Position_closestActualNode(position); + for (; node != null; node = node.parentNode) { + var type = node._type; + if ((type == HTML_A) && + (node.hasAttribute("href")) && + (result == null)) { + + var arange = new Range(node,0,node,node.childNodes.length); + var rects = Range_getClientRects(arange); + var insideLink = false; + for (var i = 0; i < rects.length; i++) { + if (rectContainsPoint(rects[i],x,y)) + insideLink = true; + } + + if (insideLink) { + var href = node.getAttribute("href"); + if ((href != null) && (href.charAt(0) == "#")) { + if (isInTOC(node)) + result = "intocreference-"+href.substring(1); + else + result = "inreference"; + } + else { + result = "inlink"; + } + } + } + else if ((type == HTML_IMG) && (result == null)) { + for (var anc = node; anc != null; anc = anc.parentNode) { + if (anc._type == HTML_FIGURE) { + result = "infigure"; + break; + } + } + } + else if (isAutoCorrectNode(node) && (result == null)) { + result = "incorrection"; + } + else if (isTOCNode(node)) { + var rect = node.getBoundingClientRect(); + if (x >= rect.left + rect.width/2) + position = new Position(node.parentNode,DOM_nodeOffset(node)+1); + else + position = new Position(node.parentNode,DOM_nodeOffset(node)); + break; + } + } + + var position = Position_closestMatchForwards(position,Position_okForMovement); + if ((position != null) && isOpaqueNode(position.node)) + position = Position_nextMatch(position,Position_okForMovement); + if (position == null) + return false; + + var selectionRange = Selection_get(); + var samePosition = ((selectionRange != null) && Range_isEmpty(selectionRange) && + (position.node == selectionRange.start.node) && + (position.offset == selectionRange.start.offset)); + if (samePosition && (result == null)) + result = "same"; + + if (wordBoundary) { + var startOfWord = Selection_posAtStartOfWord(position); + var endOfWord = Selection_posAtEndOfWord(position); + if ((startOfWord.node != position.node) || (startOfWord.node != position.node)) + throw new Error("Word boundary in different node"); + var distanceBefore = position.offset - startOfWord.offset; + var distanceAfter = endOfWord.offset - position.offset; + if (distanceBefore <= distanceAfter) + position = startOfWord; + else + position = endOfWord; + } + + Cursor_set(position.node,position.offset); + return result; + } + + // public + Cursor_getCursorPosition = function() + { + var selRange = Selection_get(); + if (selRange == null) + return null; + + // FIXME: in the cases where this is called from Objective C, test what happens if we + // return a null rect + var rect = Position_displayRectAtPos(selRange.end); + if (rect == null) + return null; + + var left = rect.left + window.scrollX; + var top = rect.top + window.scrollY; + var height = rect.height; + return { x: left, y: top, width: 0, height: height }; + } + + // public + Cursor_moveLeft = function() + { + var range = Selection_get(); + if (range == null) + return; + + var pos = Position_prevMatch(range.start,Position_okForMovement); + if (pos != null) + Cursor_set(pos.node,pos.offset); + Cursor_ensureCursorVisible(); + } + + // public + Cursor_moveRight = function() + { + var range = Selection_get(); + if (range == null) + return; + + var pos = Position_nextMatch(range.start,Position_okForMovement); + if (pos != null) + Cursor_set(pos.node,pos.offset); + Cursor_ensureCursorVisible(); + } + + // public + Cursor_moveToStartOfDocument = function() + { + var pos = new Position(document.body,0); + pos = Position_closestMatchBackwards(pos,Position_okForMovement); + Cursor_set(pos.node,pos.offset); + Cursor_ensureCursorVisible(); + } + + // public + Cursor_moveToEndOfDocument = function() + { + var pos = new Position(document.body,document.body.childNodes.length); + pos = Position_closestMatchForwards(pos,Position_okForMovement); + Cursor_set(pos.node,pos.offset); + Cursor_ensureCursorVisible(); + } + + // An empty paragraph does not get shown and cannot be edited. We can fix this by adding + // a BR element as a child + // public + Cursor_updateBRAtEndOfParagraph = function(node) + { + var paragraph = node; + while ((paragraph != null) && !isParagraphNode(paragraph)) + paragraph = paragraph.parentNode; + if (paragraph != null) { + + var br = null; + var last = paragraph; + do { + + var child = last; + while ((child != null) && isWhitespaceTextNode(child)) + child = child.previousSibling; + + if ((child != null) && (child._type == HTML_BR)) + br = child; + + last = last.lastChild; + + } while ((last != null) && isInlineNode(last)); + + if (nodeHasContent(paragraph)) { + // Paragraph has content: don't want BR at end + if (br != null) { + DOM_deleteNode(br); + } + } + else { + // Paragraph consists only of whitespace: must have BR at end + if (br == null) { + br = DOM_createElement(document,"BR"); + DOM_appendChild(paragraph,br); + } + } + } + } + + // public + Cursor_insertReference = function(itemId) + { + var a = DOM_createElement(document,"A"); + DOM_setAttribute(a,"href","#"+itemId); + Clipboard_pasteNodes([a]); + } + + // public + Cursor_insertLink = function(text,url) + { + var a = DOM_createElement(document,"A"); + DOM_setAttribute(a,"href",url); + DOM_appendChild(a,DOM_createTextNode(document,text)); + Clipboard_pasteNodes([a]); + } + + var nbsp = String.fromCharCode(160); + + function spaceToNbsp(pos) + { + var node = pos.node; + var offset = pos.offset; + + if ((node.nodeType == Node.TEXT_NODE) && (offset > 0) && + (isWhitespaceString(node.nodeValue.charAt(offset-1)))) { + // Insert first, to preserve any tracked positions + DOM_insertCharacters(node,offset-1,nbsp); + DOM_deleteCharacters(node,offset,offset+1); + } + } + + function nbspToSpace(pos) + { + var node = pos.node; + var offset = pos.offset; + + if ((node.nodeType == Node.TEXT_NODE) && (offset > 0) && + (node.nodeValue.charAt(offset-1) == nbsp)) { + // Insert first, to preserve any tracked positions + DOM_insertCharacters(node,offset-1," "); + DOM_deleteCharacters(node,offset,offset+1); + } + } + + function checkNbsp() + { + Selection_preserveWhileExecuting(function() { + var selRange = Selection_get(); + if (selRange != null) + nbspToSpace(selRange.end); + }); + } + + function isPosAtStartOfParagraph(pos) + { + if ((pos.node.nodeType == Node.ELEMENT_NODE) && (pos.offset == 0) && + !isInlineNode(pos.node)) { + return true; + } + + + + while (pos != null) { + if (pos.node.nodeType == Node.ELEMENT_NODE) { + if ((pos.offset == 0) && !isInlineNode(pos.node)) + return true; + else + pos = Position_prev(pos); + } + else if (pos.node.nodeType == Node.TEXT_NODE) { + if (pos.offset > 0) + return false; + else + pos = Position_prev(pos); + } + else { + return false; + } + } + + return false; + } + + // public + Cursor_insertCharacter = function(str,allowInvalidPos,allowNoParagraph) + { + var firstInsertion = (UndoManager_groupType() != "Insert text"); + + if (firstInsertion) + UndoManager_newGroup("Insert text",checkNbsp); + + if (str == "-") { + var preceding = Cursor_getPrecedingWord(); + if (preceding.match(/[0-9]\s*$/)) + str = String.fromCharCode(0x2013); // en dash + else if (preceding.match(/\s+$/)) + str = String.fromCharCode(0x2014); // em dash + } + + var selRange = Selection_get(); + if (selRange == null) + return; + + if (!Range_isEmpty(selRange)) { + Selection_deleteContents(true); + selRange = Selection_get(); + } + var pos = selRange.start; + pos = Position_preferTextPosition(pos); + if ((str == " ") && isPosAtStartOfParagraph(pos)) + return; + if (!allowInvalidPos && !Position_okForInsertion(pos)) { + var elemPos = Position_preferElementPosition(pos); + if (Position_okForInsertion(elemPos)) { + pos = elemPos; + } + else { + var oldPos = pos; + pos = Position_closestMatchForwards(selRange.start,Position_okForInsertion); + var difference = new Range(oldPos.node,oldPos.offset,pos.node,pos.offset); + difference = Range_forwards(difference); + Position_trackWhileExecuting([pos],function() { + if (!Range_hasContent(difference)) { + Selection_deleteRangeContents(difference,true); + } + }); + } + } + var node = pos.node; + var offset = pos.offset; + + if ((str == " ") && + !firstInsertion && + (node.nodeType == Node.TEXT_NODE) && + (offset > 0) && + (node.nodeValue.charAt(offset-1) == nbsp)) { + + if (!node.nodeValue.substring(0,offset).match(/\.\s+$/)) { + DOM_deleteCharacters(node,offset-1,offset); + DOM_insertCharacters(node,offset-1,"."); + } + } + + if (isWhitespaceString(str) && (node.nodeType == Node.TEXT_NODE) && (offset > 0)) { + var prevChar = node.nodeValue.charAt(offset-1); + if (isWhitespaceString(prevChar) || (prevChar == nbsp)) { + Selection_update(); + Cursor_ensureCursorVisible(); + return; + } + } + + nbspToSpace(pos); + + // If the user enters two double quotes in succession (open and close), replace them with + // just one plain double quote character + if ((str == "”") && (node.nodeType == Node.TEXT_NODE) && + (offset > 0) && (node.nodeValue.charAt(offset-1) == "“")) { + DOM_deleteCharacters(node,offset-1,offset); + offset--; + str = "\""; + } + + if (node.nodeType == Node.ELEMENT_NODE) { + var emptyTextNode = DOM_createTextNode(document,""); + if (offset >= node.childNodes.length) + DOM_appendChild(node,emptyTextNode); + else + DOM_insertBefore(node,emptyTextNode,node.childNodes[offset]); + node = emptyTextNode; + offset = 0; + } + + if (str == " ") + DOM_insertCharacters(node,offset,nbsp); + else + DOM_insertCharacters(node,offset,str); + + // must be done *after* inserting the text + if (!allowNoParagraph) { + switch (node.parentNode._type) { + case HTML_CAPTION: + case HTML_FIGCAPTION: + // Do nothing + break; + default: + Hierarchy_ensureInlineNodesInParagraph(node,true); + break; + } + } + + offset += str.length; + + pos = new Position(node,offset); + Position_trackWhileExecuting([pos],function() { + Formatting_mergeWithNeighbours(pos.node,Formatting_MERGEABLE_INLINE); + }); + + Cursor_set(pos.node,pos.offset); + Range_trackWhileExecuting(Selection_get(),function() { + Cursor_updateBRAtEndOfParagraph(pos.node); + }); + + Selection_update(); + Cursor_ensureCursorVisible(); + } + + // public + Cursor_deleteCharacter = function() + { + if (UndoManager_groupType() != "Delete text") + UndoManager_newGroup("Delete text",checkNbsp); + + Selection_preferElementPositions(); + var selRange = Selection_get(); + if (selRange == null) + return; + + if (!Range_isEmpty(selRange)) { + Selection_deleteContents(true); + } + else { + var currentPos = selRange.start; + + // Special case of pressing backspace after a table, figure, or TOC + var back = Position_closestMatchBackwards(currentPos,Position_okForMovement); + if ((back != null) && (back.node.nodeType == Node.ELEMENT_NODE) && (back.offset > 0)) { + var prevNode = back.node.childNodes[back.offset-1]; + if (isSpecialBlockNode(prevNode)) { + var p = DOM_createElement(document,"P"); + DOM_insertBefore(prevNode.parentNode,p,prevNode); + DOM_deleteNode(prevNode); + Cursor_updateBRAtEndOfParagraph(p); + Cursor_set(p,0); + Cursor_ensureCursorVisible(); + return; + } + if (prevNode._type == HTML_A) { + Cursor_set(back.node,back.offset-1); + Selection_preserveWhileExecuting(function() { + DOM_deleteNode(prevNode); + }); + return; + } + } + + currentPos = Position_preferTextPosition(currentPos); + var prevPos = Position_prevMatch(currentPos,Position_okForMovement); + if (prevPos != null) { + var startBlock = firstBlockAncestor(Position_closestActualNode(prevPos)); + var endBlock = firstBlockAncestor(Position_closestActualNode(selRange.end)); + if ((startBlock != endBlock) && + isParagraphNode(startBlock) && !nodeHasContent(startBlock)) { + DOM_deleteNode(startBlock); + Cursor_set(selRange.end.node,selRange.end.offset) + } + else { + var range = new Range(prevPos.node,prevPos.offset, + selRange.end.node,selRange.end.offset); + Selection_deleteRangeContents(range,true); + } + } + } + + selRange = Selection_get(); + if (selRange != null) + spaceToNbsp(selRange.end); + Selection_update(); + Cursor_ensureCursorVisible(); + + function firstBlockAncestor(node) + { + while (isInlineNode(node)) + node = node.parentNode; + return node; + } + } + + // public + Cursor_enterPressed = function() + { + UndoManager_newGroup("New paragraph"); + + Selection_preferElementPositions(); + var selRange = Selection_get(); + if (selRange == null) + return; + + Range_trackWhileExecuting(selRange,function() { + if (!Range_isEmpty(selRange)) + Selection_deleteContents(true); + }); + + // Are we inside a figure or table caption? If so, put an empty paragraph directly after it + var inCaption = false; + var inFigCaption = false; + var closestNode = Position_closestActualNode(selRange.start); + for (var ancestor = closestNode; ancestor != null; ancestor = ancestor.parentNode) { + switch (ancestor._type) { + case HTML_CAPTION: + inCaption = true; + break; + case HTML_FIGCAPTION: + inFigCaption = true; + break; + case HTML_TABLE: + case HTML_FIGURE: + if ((inCaption && (ancestor._type == HTML_TABLE)) || + (inFigCaption && (ancestor._type == HTML_FIGURE))) { + var p = DOM_createElement(document,"P"); + DOM_insertBefore(ancestor.parentNode,p,ancestor.nextSibling); + Cursor_updateBRAtEndOfParagraph(p); + Selection_set(p,0,p,0); + return; + } + break; + } + } + + var check = Position_preferElementPosition(selRange.start); + if (check.node.nodeType == Node.ELEMENT_NODE) { + var before = check.node.childNodes[check.offset-1]; + var after = check.node.childNodes[check.offset]; + if (((before != null) && isSpecialBlockNode(before)) || + ((after != null) && isSpecialBlockNode(after))) { + var p = DOM_createElement(document,"P"); + DOM_insertBefore(check.node,p,check.node.childNodes[check.offset]); + Cursor_updateBRAtEndOfParagraph(p); + Cursor_set(p,0); + Cursor_ensureCursorVisible(); + return; + } + } + + Range_trackWhileExecuting(selRange,function() { + Range_ensureInlineNodesInParagraph(selRange); + Range_ensureValidHierarchy(selRange); + }); + + var pos = selRange.start; + + var detail = Range_detail(selRange); + switch (detail.startParent._type) { + case HTML_OL: + case HTML_UL: { + var li = DOM_createElement(document,"LI"); + DOM_insertBefore(detail.startParent,li,detail.startChild); + + Cursor_set(li,0); + Cursor_ensureCursorVisible(); + return; + } + } + + if (isAutoCorrectNode(pos.node)) { + pos = Position_preferTextPosition(pos); + selRange.start = selRange.end = pos; + } + + Range_trackWhileExecuting(selRange,function() { + + // If we're directly in a container node, add a paragraph, so we have something to + // split. + if (isContainerNode(pos.node) && (pos.node._type != HTML_LI)) { + var p = DOM_createElement(document,"P"); + DOM_insertBefore(pos.node,p,pos.node.childNodes[pos.offset]); + pos = new Position(p,0); + } + + var blockToSplit = getBlockToSplit(pos); + var stopAt = blockToSplit.parentNode; + + if (positionAtStartOfHeading(pos)) { + var container = getContainerOrParagraph(pos.node); + pos = new Position(container,0); + pos = Formatting_movePreceding(pos,function(n) { return (n == stopAt); },true); + } + else if (pos.node.nodeType == Node.TEXT_NODE) { + pos = Formatting_splitTextAfter(pos,function(n) { return (n == stopAt); },true); + } + else { + pos = Formatting_moveFollowing(pos,function(n) { return (n == stopAt); },true); + } + }); + + Cursor_set(pos.node,pos.offset); + selRange = Selection_get(); + + Range_trackWhileExecuting(selRange,function() { + if ((pos.node.nodeType == Node.TEXT_NODE) && (pos.node.nodeValue.length == 0)) { + DOM_deleteNode(pos.node); + } + + var detail = Range_detail(selRange); + + // If a preceding paragraph has become empty as a result of enter being pressed + // while the cursor was in it, then update the BR at the end of the paragraph + var start = detail.startChild ? detail.startChild : detail.startParent; + for (var ancestor = start; ancestor != null; ancestor = ancestor.parentNode) { + var prev = ancestor.previousSibling; + if ((prev != null) && isParagraphNode(prev) && !nodeHasContent(prev)) { + DOM_deleteAllChildren(prev); + Cursor_updateBRAtEndOfParagraph(prev); + break; + } + else if ((prev != null) && (prev._type == HTML_LI) && !nodeHasContent(prev)) { + var next; + for (var child = prev.firstChild; child != null; child = next) { + next = child.nextSibling; + if (isWhitespaceTextNode(child)) + DOM_deleteNode(child); + else + Cursor_updateBRAtEndOfParagraph(child); + } + break; + } + } + + for (var ancestor = start; ancestor != null; ancestor = ancestor.parentNode) { + + if (isParagraphNode(ancestor)) { + var nextSelector = Styles_nextSelectorAfter(ancestor); + if (nextSelector != null) { + var nextElementName = null; + var nextClassName = null; + + + var dotIndex = nextSelector.indexOf("."); + if (dotIndex >= 0) { + nextElementName = nextSelector.substring(0,dotIndex); + nextClassName = nextSelector.substring(dotIndex+1); + } + else { + nextElementName = nextSelector; + } + + ancestor = DOM_replaceElement(ancestor,nextElementName); + DOM_removeAttribute(ancestor,"id"); + DOM_setAttribute(ancestor,"class",nextClassName); + } + } + + if (isParagraphNode(ancestor) && !nodeHasContent(ancestor)) { + Cursor_updateBRAtEndOfParagraph(prev); + break; + } + else if ((ancestor._type == HTML_LI) && !nodeHasContent(ancestor)) { + DOM_deleteAllChildren(ancestor); + break; + } + } + + Cursor_updateBRAtEndOfParagraph(Range_singleNode(selRange)); + }); + + Selection_set(selRange.start.node,selRange.start.offset, + selRange.end.node,selRange.end.offset); + cursorX = null; + Cursor_ensureCursorVisible(); + + function getBlockToSplit(pos) + { + var blockToSplit = null; + for (var n = pos.node; n != null; n = n.parentNode) { + if (n._type == HTML_LI) { + blockToSplit = n; + break; + } + } + if (blockToSplit == null) { + blockToSplit = pos.node; + while (isInlineNode(blockToSplit)) + blockToSplit = blockToSplit.parentNode; + } + return blockToSplit; + } + + function getContainerOrParagraph(node) + { + while ((node != null) && isInlineNode(node)) + node = node.parentNode; + return node; + } + + function positionAtStartOfHeading(pos) + { + var container = getContainerOrParagraph(pos.node); + if (isHeadingNode(container)) { + var startOffset = 0; + if (isOpaqueNode(container.firstChild)) + startOffset = 1; + var range = new Range(container,startOffset,pos.node,pos.offset); + return !Range_hasContent(range); + } + else + return false; + } + } + + Cursor_getPrecedingWord = function() { + var selRange = Selection_get(); + if ((selRange == null) && !Range_isEmpty(selRange)) + return ""; + + var node = selRange.start.node; + var offset = selRange.start.offset; + if (node.nodeType != Node.TEXT_NODE) + return ""; + + return node.nodeValue.substring(0,offset); + } + + Cursor_getAdjacentNodeWithType = function(type) + { + var selRange = Selection_get(); + var position = selRange.start; + while (position != null) { + var node = Position_closestActualNode(position); + for (; node != null; node = node.parentNode) { + if (node._type == type) + return node; + } + position = Position_prev(position); + } + return null; + } + + Cursor_getLinkProperties = function() + { + var a = Cursor_getAdjacentNodeWithType(HTML_A); + if (a == null) + return null; + + return { href: a.getAttribute("href"), + text: getNodeText(a) }; + } + + Cursor_setLinkProperties = function(properties) + { + var a = Cursor_getAdjacentNodeWithType(HTML_A); + if (a == null) + return null; + + Selection_preserveWhileExecuting(function() { + DOM_setAttribute(a,"href",properties.href); + DOM_deleteAllChildren(a); + DOM_appendChild(a,DOM_createTextNode(document,properties.text)); + }); + } + + Cursor_setReferenceTarget = function(itemId) + { + var a = Cursor_getAdjacentNodeWithType(HTML_A); + if (a != null) + Outline_setReferenceTarget(a,itemId); + } + + // Deletes the current selection contents and ensures that the cursor is located directly + // inside the nearest container element, i.e. not inside a paragraph or inline node. This + // is intended for preventing things like inserting a table of contants inside a heading + Cursor_makeContainerInsertionPoint = function() + { + var selRange = Selection_get(); + if (selRange == null) + return; + + if (!Range_isEmpty(selRange)) { + Selection_deleteContents(); + selRange = Selection_get(); + } + + var parent; + var previousSibling; + var nextSibling; + + if (selRange.start.node.nodeType == Node.ELEMENT_NODE) { + parent = selRange.start.node; + nextSibling = selRange.start.node.childNodes[selRange.start.offset]; + } + else { + if (selRange.start.offset > 0) + Formatting_splitTextBefore(selRange.start); + parent = selRange.start.node.parentNode; + nextSibling = selRange.start.node; + } + + var offset = DOM_nodeOffset(nextSibling,parent); + + if (isContainerNode(parent)) { + Cursor_set(parent,offset); + return; + } + + if ((offset > 0) && isItemNumber(parent.childNodes[offset-1])) + offset--; + + Formatting_moveFollowing(new Position(parent,offset),isContainerNode); + Formatting_movePreceding(new Position(parent,offset),isContainerNode); + + offset = 0; + while (!isContainerNode(parent)) { + var old = parent; + offset = DOM_nodeOffset(parent); + parent = parent.parentNode; + DOM_deleteNode(old); + } + + Cursor_set(parent,offset); + cursorX = null; + } + + Cursor_set = function(node,offset,keepCursorX) + { + Selection_set(node,offset,node,offset); + if (!keepCursorX) + cursorX = null; + } + + function insertNote(className,content) + { + var footnote = DOM_createElement(document,"span"); + DOM_setAttribute(footnote,"class",className); + DOM_appendChild(footnote,DOM_createTextNode(document,content)); + + var range = Selection_get(); + Formatting_splitAroundSelection(range,false); + + var pos = Position_preferElementPosition(range.start); + + DOM_insertBefore(pos.node,footnote,pos.node.childNodes[pos.offset]); + Selection_set(footnote,0,footnote,footnote.childNodes.length); + Cursor_updateBRAtEndOfParagraph(footnote); + } + + Cursor_insertFootnote = function(content) + { + insertNote("footnote",content); + } + + Cursor_insertEndnote = function(content) + { + insertNote("endnote",content); + } + +})(); http://git-wip-us.apache.org/repos/asf/incubator-corinthia/blob/03bd5af0/Editor/src/DOM.js ---------------------------------------------------------------------- diff --git a/Editor/src/DOM.js b/Editor/src/DOM.js new file mode 100644 index 0000000..723f20e --- /dev/null +++ b/Editor/src/DOM.js @@ -0,0 +1,963 @@ +// Copyright 2011-2014 UX Productivity Pty Ltd +// +// Licensed 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. + +// Helper functions +var DOM_assignNodeIds; + +// Primitive node creation operations +var DOM_createElement; +var DOM_createElementNS; +var DOM_createTextNode; +var DOM_createComment; +var DOM_cloneNode; + +// Primitive and high-level node mutation operations +var DOM_appendChild; +var DOM_insertBefore; +var DOM_deleteNode; +var DOM_setAttribute; +var DOM_setAttributeNS; +var DOM_removeAttribute; +var DOM_removeAttributeNS; +var DOM_setStyleProperties; +var DOM_insertCharacters; +var DOM_moveCharacters; +var DOM_deleteCharacters; +var DOM_setNodeValue; + +// High-level DOM operations +var DOM_getAttribute; +var DOM_getAttributeNS; +var DOM_getStringAttribute; +var DOM_getStringAttributeNS; +var DOM_getStyleProperties; +var DOM_deleteAllChildren; +var DOM_shallowCopyElement; +var DOM_replaceElement; +var DOM_wrapNode; +var DOM_wrapSiblings; +var DOM_mergeWithNextSibling; +var DOM_nodesMergeable; +var DOM_replaceCharacters; +var DOM_addTrackedPosition; +var DOM_removeTrackedPosition; +var DOM_removeAdjacentWhitespace; +var DOM_documentHead; +var DOM_ensureUniqueIds; +var DOM_nodeOffset; +var DOM_maxChildOffset; +var DOM_ignoreMutationsWhileExecuting; +var DOM_getIgnoreMutations; +var DOM_addListener; +var DOM_removeListener; +var DOM_Listener; + +(function() { + + var nextNodeId = 0; + var nodeData = new Object(); + var ignoreMutations = 0; + + //////////////////////////////////////////////////////////////////////////////////////////////// + // // + // DOM Helper Functions // + // // + //////////////////////////////////////////////////////////////////////////////////////////////// + + function addUndoAction() + { + if (window.undoSupported) + UndoManager_addAction.apply(null,arrayCopy(arguments)); + } + + function assignNodeId(node) + { + if (node._nodeId != null) + throw new Error(node+" already has id"); + node._nodeId = nextNodeId++; + node._type = ElementTypes[node.nodeName]; + return node; + } + + function checkNodeId(node) + { + if (node._nodeId == null) + throw new Error(node.nodeName+" lacks _nodeId"); + } + + // public + DOM_assignNodeIds = function(root) + { + if (root._nodeId != null) + throw new Error(root+" already has id"); + recurse(root); + return; + + function recurse(node) { + node._nodeId = nextNodeId++; + node._type = ElementTypes[node.nodeName]; + for (var child = node.firstChild; child != null; child = child.nextSibling) + recurse(child); + } + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + // // + // Primitive DOM Operations // + // // + //////////////////////////////////////////////////////////////////////////////////////////////// + + /* + + The following functions are considered "primitive", in that they are the core functions + through which all manipulation of the DOM ultimately occurs. All other DOM functions call + these, either directly or indirectly, instead of making direct method calls on node objects. + These functions are divided into two categories: node creation and mode mutation. + + The creation functions are as follows: + + * createElement(document,elementName) + * createElementNS(document,namespaceURI,qualifiedName) + * createTextNode(document,data) + * createComment(document,data) + * cloneNode(original,deep,noIdAttr) + + The purpose of these is to ensure that a unique _nodeId value is assigned to each node object, + which is needed for using the NodeSet and NodeMap classes. All nodes in a document must have + this set; we use our own functions for this because DOM provides no other way of uniquely + identifying nodes in a way that allows them to be stored in a hash table. + + The mutation functions are as follows: + + * insertBeforeInternal(parent,newChild,refChild) + * deleteNodeInternal(node,deleteDescendantData) + * setAttribute(element,name,value) + * setAttributeNS(element,namespaceURI,qualifiedName,value) + * setStyleProperties(element,properties) + * insertCharacters(textNode,offset,characters) + * deleteCharacters(textNode,startOffset,endOffset) + * moveCharacters(srcTextNode,srcStartOffset,srcEndOffset,destTextNode,destOffset) + * setNodeValue(textNode,value) + + These functions exist to allow us to record undo information. We can't use DOM mutation events + for this purpose they're not fully supported in WebKit. + + Every time a mutation operation is performed on a node, we add an action to the undo stack + corresponding to the inverse of that operaton, i.e. an action that undoes the operaton. It + is absolutely critical that all changes to a DOM node go through these functions, regardless + of whether or not the node currently resides in the tree. This ensures that the undo history + is able to correctly revert the tree to the same state that it was in at the relevant point + in time. + + By routing all DOM modifications through these few functions, virtually all of the other + javascript code can be ignorant of the undo manager, provided the only state they change is + in the DOM. Parts of the code which maintain their own state about the document, such as the + style manager, must implement their own undo-compliant state manipulation logic. + + *** IMPORTANT *** + + Just in case it isn't already clear, you must *never* make direct calls to methods like + appendChild() and createElement() on the node objects themselves. Doing so will result in + subtle and probably hard-to-find bugs. As far as all javascript code for UX Write is + concerned, consider the public functions defined in this file to be the DOM API. You can use + check-dom-methods.sh to search for any cases where this rule has been violated. + + */ + + // public + DOM_createElement = function(document,elementName) + { + return assignNodeId(document.createElement(elementName)); // check-ok + } + + // public + DOM_createElementNS = function(document,namespaceURI,qualifiedName) + { + return assignNodeId(document.createElementNS(namespaceURI,qualifiedName)); // check-ok + } + + // public + DOM_createTextNode = function(document,data) + { + return assignNodeId(document.createTextNode(data)); // check-ok + } + + // public + DOM_createComment = function(document,data) + { + return assignNodeId(document.createComment(data)); // check-ok + } + + // public + DOM_cloneNode = function(original,deep,noIdAttr) + { + var clone = original.cloneNode(deep); // check-ok + DOM_assignNodeIds(clone); + if (noIdAttr) + clone.removeAttribute("id"); // check-ok + return clone; + } + + function insertBeforeInternal(parent,newChild,refChild) + { + if (newChild.parentNode == null) { + addUndoAction(deleteNodeInternal,newChild) + } + else { + var oldParent = newChild.parentNode; + var oldNext = newChild.nextSibling; + addUndoAction(insertBeforeInternal,oldParent,newChild,oldNext); + } + + parent.insertBefore(newChild,refChild); // check-ok + } + + function deleteNodeInternal(node,deleteDescendantData) + { + checkNodeId(node); + + addUndoAction(insertBeforeInternal,node.parentNode,node,node.nextSibling); + + if (node.parentNode == null) + throw new Error("Undo delete "+nodeString(node)+": parent is null"); + node.parentNode.removeChild(node); // check-ok + + // Delete all data associated with the node. This is not preserved across undo/redo; + // currently the only thing we are using this data for is tracked positions, and we + // are going to be recording undo information for the selection separately, so this is + // not a problem. + if (deleteDescendantData) + deleteNodeDataRecursive(node); + else + deleteNodeData(node); + + return; + + function deleteNodeData(current) + { + delete nodeData[current._nodeId]; + } + + function deleteNodeDataRecursive(current) + { + deleteNodeData(current); + for (var child = current.firstChild; child != null; child = child.nextSibling) + deleteNodeDataRecursive(child); + } + } + + // public + DOM_setAttribute = function(element,name,value) + { + if (element.hasAttribute(name)) + addUndoAction(DOM_setAttribute,element,name,element.getAttribute(name)); + else + addUndoAction(DOM_setAttribute,element,name,null); + + if (value == null) + element.removeAttribute(name); // check-ok + else + element.setAttribute(name,value); // check-ok + } + + // public + DOM_setAttributeNS = function(element,namespaceURI,qualifiedName,value) + { + var localName = qualifiedName.replace(/^.*:/,""); + if (element.hasAttributeNS(namespaceURI,localName)) { + var oldValue = element.getAttributeNS(namespaceURI,localName); + var oldQName = element.getAttributeNodeNS(namespaceURI,localName).nodeName; // check-ok + addUndoAction(DOM_setAttributeNS,element,namespaceURI,oldQName,oldValue) + } + else { + addUndoAction(DOM_setAttributeNS,element,namespaceURI,localName,null); + } + + if (value == null) + element.removeAttributeNS(namespaceURI,localName); // check-ok + else + element.setAttributeNS(namespaceURI,qualifiedName,value); // check-ok + } + + // public + DOM_setStyleProperties = function(element,properties) + { + if (Object.getOwnPropertyNames(properties).length == 0) + return; + + if (element.hasAttribute("style")) + addUndoAction(DOM_setAttribute,element,"style",element.getAttribute("style")); + else + addUndoAction(DOM_setAttribute,element,"style",null); + + for (var name in properties) + element.style.setProperty(name,properties[name]); // check-ok + + if (element.getAttribute("style") == "") + element.removeAttribute("style"); // check-ok + } + + // public + DOM_insertCharacters = function(textNode,offset,characters) + { + if (textNode.nodeType != Node.TEXT_NODE) + throw new Error("DOM_insertCharacters called on non-text node"); + if ((offset < 0) || (offset > textNode.nodeValue.length)) + throw new Error("DOM_insertCharacters called with invalid offset"); + trackedPositionsForNode(textNode).forEach(function (position) { + if (position.offset > offset) + position.offset += characters.length; + }); + textNode.nodeValue = textNode.nodeValue.slice(0,offset) + + characters + + textNode.nodeValue.slice(offset); + var startOffset = offset; + var endOffset = offset + characters.length; + addUndoAction(DOM_deleteCharacters,textNode,startOffset,endOffset); + } + + // public + DOM_deleteCharacters = function(textNode,startOffset,endOffset) + { + if (textNode.nodeType != Node.TEXT_NODE) + throw new Error("DOM_deleteCharacters called on non-text node "+nodeString(textNode)); + if (endOffset == null) + endOffset = textNode.nodeValue.length; + if (endOffset < startOffset) + throw new Error("DOM_deleteCharacters called with invalid start/end offset"); + trackedPositionsForNode(textNode).forEach(function (position) { + var deleteCount = endOffset - startOffset; + if ((position.offset > startOffset) && (position.offset < endOffset)) + position.offset = startOffset; + else if (position.offset >= endOffset) + position.offset -= deleteCount; + }); + + var removed = textNode.nodeValue.slice(startOffset,endOffset); + addUndoAction(DOM_insertCharacters,textNode,startOffset,removed); + + textNode.nodeValue = textNode.nodeValue.slice(0,startOffset) + + textNode.nodeValue.slice(endOffset); + } + + // public + DOM_moveCharacters = function(srcTextNode,srcStartOffset,srcEndOffset,destTextNode,destOffset, + excludeStartPos,excludeEndPos) + { + if (srcTextNode == destTextNode) + throw new Error("src and dest text nodes cannot be the same"); + if (srcStartOffset > srcEndOffset) + throw new Error("Invalid src range "+srcStartOffset+" - "+srcEndOffset); + if (srcStartOffset < 0) + throw new Error("srcStartOffset < 0"); + if (srcEndOffset > srcTextNode.nodeValue.length) + throw new Error("srcEndOffset beyond end of src length"); + if (destOffset < 0) + throw new Error("destOffset < 0"); + if (destOffset > destTextNode.nodeValue.length) + throw new Error("destOffset beyond end of dest length"); + + var length = srcEndOffset - srcStartOffset; + + addUndoAction(DOM_moveCharacters,destTextNode,destOffset,destOffset+length, + srcTextNode,srcStartOffset,excludeStartPos,excludeEndPos); + + trackedPositionsForNode(destTextNode).forEach(function (pos) { + var startMatch = excludeStartPos ? (pos.offset > destOffset) + : (pos.offset >= destOffset); + if (startMatch) + pos.offset += length; + }); + trackedPositionsForNode(srcTextNode).forEach(function (pos) { + + var startMatch = excludeStartPos ? (pos.offset > srcStartOffset) + : (pos.offset >= srcStartOffset); + var endMatch = excludeEndPos ? (pos.offset < srcEndOffset) + : (pos.offset <= srcEndOffset); + + if (startMatch && endMatch) { + pos.node = destTextNode; + pos.offset = destOffset + (pos.offset - srcStartOffset); + } + else if (pos.offset >= srcEndOffset) { + pos.offset -= length; + } + }); + var extract = srcTextNode.nodeValue.substring(srcStartOffset,srcEndOffset); + srcTextNode.nodeValue = srcTextNode.nodeValue.slice(0,srcStartOffset) + + srcTextNode.nodeValue.slice(srcEndOffset); + destTextNode.nodeValue = destTextNode.nodeValue.slice(0,destOffset) + + extract + + destTextNode.nodeValue.slice(destOffset); + } + + // public + DOM_setNodeValue = function(textNode,value) + { + if (textNode.nodeType != Node.TEXT_NODE) + throw new Error("DOM_setNodeValue called on non-text node"); + trackedPositionsForNode(textNode).forEach(function (position) { + position.offset = 0; + }); + var oldValue = textNode.nodeValue; + addUndoAction(DOM_setNodeValue,textNode,oldValue); + textNode.nodeValue = value; + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + // // + // High-level DOM Operations // + // // + //////////////////////////////////////////////////////////////////////////////////////////////// + + function appendChildInternal(parent,newChild) + { + insertBeforeInternal(parent,newChild,null); + } + + // public + DOM_appendChild = function(node,child) + { + return DOM_insertBefore(node,child,null); + } + + // public + DOM_insertBefore = function(parent,child,nextSibling) + { + var newOffset; + if (nextSibling != null) + newOffset = DOM_nodeOffset(nextSibling); + else + newOffset = parent.childNodes.length; + + var oldParent = child.parentNode; + if (oldParent != null) { // already in tree + var oldOffset = DOM_nodeOffset(child); + + if ((oldParent == parent) && (newOffset > oldOffset)) + newOffset--; + + trackedPositionsForNode(oldParent).forEach(function (position) { + if (position.offset > oldOffset) { + position.offset--; + } + else if (position.offset == oldOffset) { + position.node = parent; + position.offset = newOffset; + } + }); + } + + var result = insertBeforeInternal(parent,child,nextSibling); + trackedPositionsForNode(parent).forEach(function (position) { + if (position.offset > newOffset) + position.offset++; + }); + return result; + } + + // public + DOM_deleteNode = function(node) + { + if (node.parentNode == null) // already deleted + return; + adjustPositionsRecursive(node); + deleteNodeInternal(node,true); + + function adjustPositionsRecursive(current) + { + for (var child = current.firstChild; child != null; child = child.nextSibling) + adjustPositionsRecursive(child); + + trackedPositionsForNode(current.parentNode).forEach(function (position) { + var offset = DOM_nodeOffset(current); + if (offset < position.offset) { + position.offset--; + } + }); + trackedPositionsForNode(current).forEach(function (position) { + var offset = DOM_nodeOffset(current); + position.node = current.parentNode; + position.offset = offset; + }); + } + } + + // public + DOM_removeAttribute = function(element,name,value) + { + DOM_setAttribute(element,name,null); + } + + // public + DOM_removeAttributeNS = function(element,namespaceURI,localName) + { + DOM_setAttributeNS(element,namespaceURI,localName,null) + } + + // public + DOM_getAttribute = function(element,name) + { + if (element.hasAttribute(name)) + return element.getAttribute(name); + else + return null; + } + + // public + DOM_getAttributeNS = function(element,namespaceURI,localName) + { + if (element.hasAttributeNS(namespaceURI,localName)) + return element.getAttributeNS(namespaceURI,localName); + else + return null; + } + + // public + DOM_getStringAttribute = function(element,name) + { + var value = element.getAttribute(name); + return (value == null) ? "" : value; + } + + // public + DOM_getStringAttributeNS = function(element,namespaceURI,localName) + { + var value = element.getAttributeNS(namespaceURI,localName); + return (value == null) ? "" : value; + } + + // public + DOM_getStyleProperties = function(node) + { + var properties = new Object(); + if (node.nodeType == Node.ELEMENT_NODE) { + for (var i = 0; i < node.style.length; i++) { + var name = node.style[i]; + var value = node.style.getPropertyValue(name); + properties[name] = value; + } + } + return properties; + } + + // public + DOM_deleteAllChildren = function(parent) + { + while (parent.firstChild != null) + DOM_deleteNode(parent.firstChild); + } + + // public + DOM_shallowCopyElement = function(element) + { + return DOM_cloneNode(element,false,true); + } + + // public + DOM_removeNodeButKeepChildren = function(node) + { + if (node.parentNode == null) + throw new Error("Node "+nodeString(node)+" has no parent"); + var offset = DOM_nodeOffset(node); + var childCount = node.childNodes.length; + + trackedPositionsForNode(node.parentNode).forEach(function (position) { + if (position.offset > offset) + position.offset += childCount-1; + }); + + trackedPositionsForNode(node).forEach(function (position) { + position.node = node.parentNode; + position.offset += offset; + }); + + var parent = node.parentNode; + var nextSibling = node.nextSibling; + deleteNodeInternal(node,false); + + while (node.firstChild != null) { + var child = node.firstChild; + insertBeforeInternal(parent,child,nextSibling); + } + } + + // public + DOM_replaceElement = function(oldElement,newName) + { + var listeners = listenersForNode(oldElement); + var newElement = DOM_createElement(document,newName); + for (var i = 0; i < oldElement.attributes.length; i++) { + var name = oldElement.attributes[i].nodeName; // check-ok + var value = oldElement.getAttribute(name); + DOM_setAttribute(newElement,name,value); + } + + var positions = arrayCopy(trackedPositionsForNode(oldElement)); + if (positions != null) { + for (var i = 0; i < positions.length; i++) { + if (positions[i].node != oldElement) + throw new Error("replaceElement: position with wrong node"); + positions[i].node = newElement; + } + } + + var parent = oldElement.parentNode; + var nextSibling = oldElement.nextSibling; + while (oldElement.firstChild != null) + appendChildInternal(newElement,oldElement.firstChild); + // Deletion must be done first so if it's a heading, the outline code picks up the change + // correctly. Otherwise, there could be two elements in the document with the same id at + // the same time. + deleteNodeInternal(oldElement,false); + insertBeforeInternal(parent,newElement,nextSibling); + + for (var i = 0; i < listeners.length; i++) + listeners[i].afterReplaceElement(oldElement,newElement); + + return newElement; + } + + // public + DOM_wrapNode = function(node,elementName) + { + return DOM_wrapSiblings(node,node,elementName); + } + + DOM_wrapSiblings = function(first,last,elementName) + { + var parent = first.parentNode; + var wrapper = DOM_createElement(document,elementName); + + if (first.parentNode != last.parentNode) + throw new Error("first and last are not siblings"); + + if (parent != null) { + var firstOffset = DOM_nodeOffset(first); + var lastOffset = DOM_nodeOffset(last); + var nodeCount = lastOffset - firstOffset + 1; + trackedPositionsForNode(parent).forEach(function (position) { + if ((position.offset >= firstOffset) && (position.offset <= lastOffset+1)) { + position.node = wrapper; + position.offset -= firstOffset; + } + else if (position.offset > lastOffset+1) { + position.offset -= (nodeCount-1); + } + }); + + insertBeforeInternal(parent,wrapper,first); + } + + var end = last.nextSibling; + var current = first; + while (current != end) { + var next = current.nextSibling; + appendChildInternal(wrapper,current); + current = next; + } + return wrapper; + } + + // public + DOM_mergeWithNextSibling = function(current,whiteList) + { + var parent = current.parentNode; + var next = current.nextSibling; + + if ((next == null) || !DOM_nodesMergeable(current,next,whiteList)) + return; + + var currentLength = DOM_maxChildOffset(current); + var nextOffset = DOM_nodeOffset(next); + + var lastChild = null; + + if (current.nodeType == Node.ELEMENT_NODE) { + lastChild = current.lastChild; + DOM_insertBefore(current,next,null); + DOM_removeNodeButKeepChildren(next); + } + else { + DOM_insertCharacters(current,current.nodeValue.length,next.nodeValue); + + trackedPositionsForNode(next).forEach(function (position) { + position.node = current; + position.offset = position.offset+currentLength; + }); + + trackedPositionsForNode(current.parentNode).forEach(function (position) { + if (position.offset == nextOffset) { + position.node = current; + position.offset = currentLength; + } + }); + + DOM_deleteNode(next); + } + + if (lastChild != null) + DOM_mergeWithNextSibling(lastChild,whiteList); + } + + // public + DOM_nodesMergeable = function(a,b,whiteList) + { + if ((a.nodeType == Node.TEXT_NODE) && (b.nodeType == Node.TEXT_NODE)) + return true; + else if ((a.nodeType == Node.ELEMENT_NODE) && (b.nodeType == Node.ELEMENT_NODE)) + return elementsMergableTypes(a,b); + else + return false; + + function elementsMergableTypes(a,b) + { + if (whiteList["force"] && isParagraphNode(a) && isParagraphNode(b)) + return true; + if ((a._type == b._type) && + whiteList[a._type] && + (a.attributes.length == b.attributes.length)) { + for (var i = 0; i < a.attributes.length; i++) { + var attrName = a.attributes[i].nodeName; // check-ok + if (a.getAttribute(attrName) != b.getAttribute(attrName)) + return false; + } + return true; + } + + return false; + } + } + + function getDataForNode(node,create) + { + if (node._nodeId == null) + throw new Error("getDataForNode: node "+node.nodeName+" has no _nodeId property"); + if ((nodeData[node._nodeId] == null) && create) + nodeData[node._nodeId] = new Object(); + return nodeData[node._nodeId]; + } + + function trackedPositionsForNode(node) + { + var data = getDataForNode(node,false); + if ((data != null) && (data.trackedPositions != null)) { + // Sanity check + for (var i = 0; i < data.trackedPositions.length; i++) { + if (data.trackedPositions[i].node != node) + throw new Error("Position "+data.trackedPositions[i]+" has wrong node"); + } + return arrayCopy(data.trackedPositions); + } + else { + return []; + } + } + + function listenersForNode(node) + { + var data = getDataForNode(node,false); + if ((data != null) && (data.listeners != null)) + return data.listeners; + else + return []; + } + + // public + DOM_replaceCharacters = function(textNode,startOffset,endOffset,replacement) + { + // Note that we do the insertion *before* the deletion so that the position is properly + // maintained, and ends up at the end of the replacement (unless it was previously at + // startOffset, in which case it will stay the same) + DOM_insertCharacters(textNode,startOffset,replacement); + DOM_deleteCharacters(textNode,startOffset+replacement.length,endOffset+replacement.length); + } + + // public + DOM_addTrackedPosition = function(position) + { + var data = getDataForNode(position.node,true); + if (data.trackedPositions == null) + data.trackedPositions = new Array(); + data.trackedPositions.push(position); + } + + // public + DOM_removeTrackedPosition = function(position) + { + var data = getDataForNode(position.node,false); + if ((data == null) || (data.trackedPositions == null)) + throw new Error("DOM_removeTrackedPosition: no registered positions for this node "+ + "("+position.node.nodeName+")"); + for (var i = 0; i < data.trackedPositions.length; i++) { + if (data.trackedPositions[i] == position) { + data.trackedPositions.splice(i,1); + return; + } + } + throw new Error("DOM_removeTrackedPosition: position is not registered ("+ + data.trackedPositions.length+" others)"); + } + + // public + DOM_removeAdjacentWhitespace = function(node) + { + while ((node.previousSibling != null) && (isWhitespaceTextNode(node.previousSibling))) + DOM_deleteNode(node.previousSibling); + while ((node.nextSibling != null) && (isWhitespaceTextNode(node.nextSibling))) + DOM_deleteNode(node.nextSibling); + } + + // public + DOM_documentHead = function(document) + { + var html = document.documentElement; + for (var child = html.firstChild; child != null; child = child.nextSibling) { + if (child._type == HTML_HEAD) + return child; + } + throw new Error("Document contains no HEAD element"); + } + + // public + DOM_ensureUniqueIds = function(root) + { + var ids = new Object(); + var duplicates = new Array(); + + discoverDuplicates(root); + renameDuplicates(); + + return; + + function discoverDuplicates(node) + { + if (node.nodeType != Node.ELEMENT_NODE) + return; + + var id = node.getAttribute("id"); + if ((id != null) && (id != "")) { + if (ids[id]) + duplicates.push(node); + else + ids[id] = true; + } + for (var child = node.firstChild; child != null; child = child.nextSibling) + discoverDuplicates(child); + } + + function renameDuplicates() + { + var nextNumberForPrefix = new Object(); + for (var i = 0; i < duplicates.length; i++) { + var id = duplicates[i].getAttribute("id"); + var prefix = id.replace(/[0-9]+$/,""); + var num = nextNumberForPrefix[prefix] ? nextNumberForPrefix[prefix] : 1; + + var candidate; + do { + candidate = prefix + num; + num++; + } while (ids[candidate]); + + DOM_setAttribute(duplicates[i],"id",candidate); + ids[candidate] = true; + nextNumberForPrefix[prefix] = num; + } + } + } + + // public + DOM_nodeOffset = function(node,parent) + { + if ((node == null) && (parent != null)) + return DOM_maxChildOffset(parent); + var offset = 0; + for (var n = node.parentNode.firstChild; n != node; n = n.nextSibling) + offset++; + return offset; + } + + // public + DOM_maxChildOffset = function(node) + { + if (node.nodeType == Node.TEXT_NODE) + return node.nodeValue.length; + else if (node.nodeType == Node.ELEMENT_NODE) + return node.childNodes.length; + else + throw new Error("maxOffset: invalid node type ("+node.nodeType+")"); + } + + function incIgnoreMutations() + { + UndoManager_addAction(decIgnoreMutations); + ignoreMutations++; + } + + function decIgnoreMutations() + { + UndoManager_addAction(incIgnoreMutations); + ignoreMutations--; + if (ignoreMutations < 0) + throw new Error("ignoreMutations is now negative"); + } + + // public + DOM_ignoreMutationsWhileExecuting = function(fun) + { + incIgnoreMutations(); + try { + return fun(); + } + finally { + decIgnoreMutations(); + } + } + + // public + DOM_getIgnoreMutations = function() + { + return ignoreMutations; + } + + // public + DOM_addListener = function(node,listener) + { + var data = getDataForNode(node,true); + if (data.listeners == null) + data.listeners = [listener]; + else + data.listeners.push(listener); + } + + // public + DOM_removeListener = function(node,listener) + { + var list = listenersForNode(node); + var index = list.indexOf(listener); + if (index >= 0) + list.splice(index,1); + } + + // public + function Listener() + { + } + + Listener.prototype.afterReplaceElement = function(oldElement,newElement) {} + + DOM_Listener = Listener; + +})(); http://git-wip-us.apache.org/repos/asf/incubator-corinthia/blob/03bd5af0/Editor/src/Editor.js ---------------------------------------------------------------------- diff --git a/Editor/src/Editor.js b/Editor/src/Editor.js new file mode 100644 index 0000000..88a6ce2 --- /dev/null +++ b/Editor/src/Editor.js @@ -0,0 +1,110 @@ +// Copyright 2011-2014 UX Productivity Pty Ltd +// +// Licensed 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 Editor_getBackMessages; +var Editor_debug; +var Editor_addOutlineItem; +var Editor_updateOutlineItem; +var Editor_removeOutlineItem; +var Editor_outlineUpdated; +var Editor_setCursor; +var Editor_setSelectionHandles; +var Editor_clearSelectionHandlesAndCursor; +var Editor_setSelectionBounds; +var Editor_updateAutoCorrect; +var Editor_error; +var debug; + +(function(){ + + var backMessages = new Array(); + + function addBackMessage() + { + backMessages.push(arrayCopy(arguments)); + return null; + } + + Editor_getBackMessages = function() + { + var result = JSON.stringify(backMessages); + backMessages = new Array(); + return result; + }; + + Editor_debug = function(str) + { + addBackMessage("debug",str); + }; + + Editor_error = function(error,type) + { + if (type == null) + type = ""; + addBackMessage("error",error.toString(),type); + }; + + Editor_addOutlineItem = function(itemId,type,title) + { + addBackMessage("addOutlineItem",itemId,type,title); + }; + + Editor_updateOutlineItem = function(itemId,title) + { + addBackMessage("updateOutlineItem",itemId,title); + }; + + Editor_removeOutlineItem = function(itemId) + { + addBackMessage("removeOutlineItem",itemId); + }; + + Editor_outlineUpdated = function() + { + addBackMessage("outlineUpdated"); + }; + + Editor_setCursor = function(x,y,width,height) + { + addBackMessage("setCursor",x,y,width,height); + }; + + Editor_setSelectionHandles = function(x1,y1,height1,x2,y2,height2) + { + addBackMessage("setSelectionHandles",x1,y1,height1,x2,y2,height2); + }; + + Editor_setTableSelection = function(x,y,width,height) + { + addBackMessage("setTableSelection",x,y,width,height); + }; + + Editor_setSelectionBounds = function(left,top,right,bottom) + { + addBackMessage("setSelectionBounds",left,top,right,bottom); + }; + + Editor_clearSelectionHandlesAndCursor = function() + { + addBackMessage("clearSelectionHandlesAndCursor"); + }; + + Editor_updateAutoCorrect = function() + { + addBackMessage("updateAutoCorrect"); + }; + + debug = Editor_debug; + +})(); http://git-wip-us.apache.org/repos/asf/incubator-corinthia/blob/03bd5af0/Editor/src/ElementTypes.js ---------------------------------------------------------------------- diff --git a/Editor/src/ElementTypes.js b/Editor/src/ElementTypes.js new file mode 100644 index 0000000..06702a2 --- /dev/null +++ b/Editor/src/ElementTypes.js @@ -0,0 +1,344 @@ +// Automatically generated from elements.txt +ElementTypes = { + "#DOCUMENT": 1, + "#document": 1, + "#TEXT": 2, + "#text": 2, + "#COMMENT": 3, + "#comment": 3, + "A": 4, + "a": 4, + "ABBR": 5, + "abbr": 5, + "ADDRESS": 6, + "address": 6, + "AREA": 7, + "area": 7, + "ARTICLE": 8, + "article": 8, + "ASIDE": 9, + "aside": 9, + "AUDIO": 10, + "audio": 10, + "B": 11, + "b": 11, + "BASE": 12, + "base": 12, + "BDI": 13, + "bdi": 13, + "BDO": 14, + "bdo": 14, + "BLOCKQUOTE": 15, + "blockquote": 15, + "BODY": 16, + "body": 16, + "BR": 17, + "br": 17, + "BUTTON": 18, + "button": 18, + "CANVAS": 19, + "canvas": 19, + "CAPTION": 20, + "caption": 20, + "CITE": 21, + "cite": 21, + "CODE": 22, + "code": 22, + "COL": 23, + "col": 23, + "COLGROUP": 24, + "colgroup": 24, + "COMMAND": 25, + "command": 25, + "DATA": 26, + "data": 26, + "DATALIST": 27, + "datalist": 27, + "DD": 28, + "dd": 28, + "DEL": 29, + "del": 29, + "DETAILS": 30, + "details": 30, + "DFN": 31, + "dfn": 31, + "DIALOG": 32, + "dialog": 32, + "DIV": 33, + "div": 33, + "DL": 34, + "dl": 34, + "DT": 35, + "dt": 35, + "EM": 36, + "em": 36, + "EMBED": 37, + "embed": 37, + "FIELDSET": 38, + "fieldset": 38, + "FIGCAPTION": 39, + "figcaption": 39, + "FIGURE": 40, + "figure": 40, + "FOOTER": 41, + "footer": 41, + "FORM": 42, + "form": 42, + "H1": 43, + "h1": 43, + "H2": 44, + "h2": 44, + "H3": 45, + "h3": 45, + "H4": 46, + "h4": 46, + "H5": 47, + "h5": 47, + "H6": 48, + "h6": 48, + "HEAD": 49, + "head": 49, + "HEADER": 50, + "header": 50, + "HGROUP": 51, + "hgroup": 51, + "HR": 52, + "hr": 52, + "HTML": 53, + "html": 53, + "I": 54, + "i": 54, + "IFRAME": 55, + "iframe": 55, + "IMG": 56, + "img": 56, + "INPUT": 57, + "input": 57, + "INS": 58, + "ins": 58, + "KBD": 59, + "kbd": 59, + "KEYGEN": 60, + "keygen": 60, + "LABEL": 61, + "label": 61, + "LEGEND": 62, + "legend": 62, + "LI": 63, + "li": 63, + "LINK": 64, + "link": 64, + "MAP": 65, + "map": 65, + "MARK": 66, + "mark": 66, + "MENU": 67, + "menu": 67, + "META": 68, + "meta": 68, + "METER": 69, + "meter": 69, + "NAV": 70, + "nav": 70, + "NOSCRIPT": 71, + "noscript": 71, + "OBJECT": 72, + "object": 72, + "OL": 73, + "ol": 73, + "OPTGROUP": 74, + "optgroup": 74, + "OPTION": 75, + "option": 75, + "OUTPUT": 76, + "output": 76, + "P": 77, + "p": 77, + "PARAM": 78, + "param": 78, + "PRE": 79, + "pre": 79, + "PROGRESS": 80, + "progress": 80, + "Q": 81, + "q": 81, + "RP": 82, + "rp": 82, + "RT": 83, + "rt": 83, + "RUBY": 84, + "ruby": 84, + "S": 85, + "s": 85, + "SAMP": 86, + "samp": 86, + "SCRIPT": 87, + "script": 87, + "SECTION": 88, + "section": 88, + "SELECT": 89, + "select": 89, + "SMALL": 90, + "small": 90, + "SOURCE": 91, + "source": 91, + "SPAN": 92, + "span": 92, + "STRONG": 93, + "strong": 93, + "STYLE": 94, + "style": 94, + "SUB": 95, + "sub": 95, + "SUMMARY": 96, + "summary": 96, + "SUP": 97, + "sup": 97, + "TABLE": 98, + "table": 98, + "TBODY": 99, + "tbody": 99, + "TD": 100, + "td": 100, + "TEXTAREA": 101, + "textarea": 101, + "TFOOT": 102, + "tfoot": 102, + "TH": 103, + "th": 103, + "THEAD": 104, + "thead": 104, + "TIME": 105, + "time": 105, + "TITLE": 106, + "title": 106, + "TR": 107, + "tr": 107, + "TRACK": 108, + "track": 108, + "U": 109, + "u": 109, + "UL": 110, + "ul": 110, + "VAR": 111, + "var": 111, + "VIDEO": 112, + "video": 112, + "WBR": 113, + "wbr": 113, +}; + +HTML_DOCUMENT = 1; +HTML_TEXT = 2; +HTML_COMMENT = 3; +HTML_A = 4; +HTML_ABBR = 5; +HTML_ADDRESS = 6; +HTML_AREA = 7; +HTML_ARTICLE = 8; +HTML_ASIDE = 9; +HTML_AUDIO = 10; +HTML_B = 11; +HTML_BASE = 12; +HTML_BDI = 13; +HTML_BDO = 14; +HTML_BLOCKQUOTE = 15; +HTML_BODY = 16; +HTML_BR = 17; +HTML_BUTTON = 18; +HTML_CANVAS = 19; +HTML_CAPTION = 20; +HTML_CITE = 21; +HTML_CODE = 22; +HTML_COL = 23; +HTML_COLGROUP = 24; +HTML_COMMAND = 25; +HTML_DATA = 26; +HTML_DATALIST = 27; +HTML_DD = 28; +HTML_DEL = 29; +HTML_DETAILS = 30; +HTML_DFN = 31; +HTML_DIALOG = 32; +HTML_DIV = 33; +HTML_DL = 34; +HTML_DT = 35; +HTML_EM = 36; +HTML_EMBED = 37; +HTML_FIELDSET = 38; +HTML_FIGCAPTION = 39; +HTML_FIGURE = 40; +HTML_FOOTER = 41; +HTML_FORM = 42; +HTML_H1 = 43; +HTML_H2 = 44; +HTML_H3 = 45; +HTML_H4 = 46; +HTML_H5 = 47; +HTML_H6 = 48; +HTML_HEAD = 49; +HTML_HEADER = 50; +HTML_HGROUP = 51; +HTML_HR = 52; +HTML_HTML = 53; +HTML_I = 54; +HTML_IFRAME = 55; +HTML_IMG = 56; +HTML_INPUT = 57; +HTML_INS = 58; +HTML_KBD = 59; +HTML_KEYGEN = 60; +HTML_LABEL = 61; +HTML_LEGEND = 62; +HTML_LI = 63; +HTML_LINK = 64; +HTML_MAP = 65; +HTML_MARK = 66; +HTML_MENU = 67; +HTML_META = 68; +HTML_METER = 69; +HTML_NAV = 70; +HTML_NOSCRIPT = 71; +HTML_OBJECT = 72; +HTML_OL = 73; +HTML_OPTGROUP = 74; +HTML_OPTION = 75; +HTML_OUTPUT = 76; +HTML_P = 77; +HTML_PARAM = 78; +HTML_PRE = 79; +HTML_PROGRESS = 80; +HTML_Q = 81; +HTML_RP = 82; +HTML_RT = 83; +HTML_RUBY = 84; +HTML_S = 85; +HTML_SAMP = 86; +HTML_SCRIPT = 87; +HTML_SECTION = 88; +HTML_SELECT = 89; +HTML_SMALL = 90; +HTML_SOURCE = 91; +HTML_SPAN = 92; +HTML_STRONG = 93; +HTML_STYLE = 94; +HTML_SUB = 95; +HTML_SUMMARY = 96; +HTML_SUP = 97; +HTML_TABLE = 98; +HTML_TBODY = 99; +HTML_TD = 100; +HTML_TEXTAREA = 101; +HTML_TFOOT = 102; +HTML_TH = 103; +HTML_THEAD = 104; +HTML_TIME = 105; +HTML_TITLE = 106; +HTML_TR = 107; +HTML_TRACK = 108; +HTML_U = 109; +HTML_UL = 110; +HTML_VAR = 111; +HTML_VIDEO = 112; +HTML_WBR = 113; +HTML_COUNT = 114; http://git-wip-us.apache.org/repos/asf/incubator-corinthia/blob/03bd5af0/Editor/src/Equations.js ---------------------------------------------------------------------- diff --git a/Editor/src/Equations.js b/Editor/src/Equations.js new file mode 100644 index 0000000..d18d21d --- /dev/null +++ b/Editor/src/Equations.js @@ -0,0 +1,52 @@ +// Copyright 2011-2014 UX Productivity Pty Ltd +// +// Licensed 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 Equations_insertEquation; + +(function() { + + Equations_insertEquation = function() + { + var math = DOM_createElementNS(document,"http://www.w3.org/1998/Math/MathML","math"); + var mrow = DOM_createElementNS(document,"http://www.w3.org/1998/Math/MathML","mrow"); + var msup = DOM_createElementNS(document,"http://www.w3.org/1998/Math/MathML","msup"); + var mi = DOM_createElementNS(document,"http://www.w3.org/1998/Math/MathML","mi"); + var mn = DOM_createElementNS(document,"http://www.w3.org/1998/Math/MathML","mn"); + var mfrac = DOM_createElementNS(document,"http://www.w3.org/1998/Math/MathML","mfrac"); + var mrow1 = DOM_createElementNS(document,"http://www.w3.org/1998/Math/MathML","mrow"); + var mrow2 = DOM_createElementNS(document,"http://www.w3.org/1998/Math/MathML","mrow"); + var mi1 = DOM_createElementNS(document,"http://www.w3.org/1998/Math/MathML","mi"); + var mi2 = DOM_createElementNS(document,"http://www.w3.org/1998/Math/MathML","mi"); + var mo = DOM_createElementNS(document,"http://www.w3.org/1998/Math/MathML","mo"); + + DOM_appendChild(mi,DOM_createTextNode(document,"x")); + DOM_appendChild(mn,DOM_createTextNode(document,"2")); + DOM_appendChild(mo,DOM_createTextNode(document,"+")); + DOM_appendChild(mi1,DOM_createTextNode(document,"a")); + DOM_appendChild(mi2,DOM_createTextNode(document,"b")); + DOM_appendChild(mrow1,mi1); + DOM_appendChild(mrow2,mi2); + DOM_appendChild(mfrac,mrow1); + DOM_appendChild(mfrac,mrow2); + DOM_appendChild(msup,mi); + DOM_appendChild(msup,mn); + DOM_appendChild(mrow,msup); + DOM_appendChild(mrow,mo); + DOM_appendChild(mrow,mfrac); + DOM_appendChild(math,mrow); + + Clipboard_pasteNodes([math]); + } + +})(); http://git-wip-us.apache.org/repos/asf/incubator-corinthia/blob/03bd5af0/Editor/src/Figures.js ---------------------------------------------------------------------- diff --git a/Editor/src/Figures.js b/Editor/src/Figures.js new file mode 100644 index 0000000..21c8511 --- /dev/null +++ b/Editor/src/Figures.js @@ -0,0 +1,122 @@ +// Copyright 2011-2014 UX Productivity Pty Ltd +// +// Licensed 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 Figures_insertFigure; +var Figures_getSelectedFigureId; +var Figures_getProperties; +var Figures_setProperties; +var Figures_getGeometry; + +(function() { + + // public + Figures_insertFigure = function(filename,width,numbered,caption) + { + UndoManager_newGroup("Insert figure"); + + var figure = DOM_createElement(document,"FIGURE"); + var img = DOM_createElement(document,"IMG"); + DOM_setAttribute(img,"src",encodeURI(filename)); + DOM_setStyleProperties(img,{"width": width}); + DOM_appendChild(figure,img); + + if ((caption != null) && (caption != "")) { + var figcaption = DOM_createElement(document,"FIGCAPTION"); + DOM_appendChild(figcaption,DOM_createTextNode(document,caption)); + DOM_appendChild(figure,figcaption); + } + + Clipboard_pasteNodes([figure]); + + // Now that the figure 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(figure.getAttribute("id"),numbered); + + // Place the cursor directly after the figure + var offset = DOM_nodeOffset(figure); + var pos = new Position(figure.parentNode,offset); + pos = Position_closestMatchForwards(pos,Position_okForMovement); + Selection_set(pos.node,pos.offset,pos.node,pos.offset); + + PostponedActions_add(UndoManager_newGroup); + } + + Figures_getSelectedFigureId = function() + { + var element = Cursor_getAdjacentNodeWithType(HTML_FIGURE); + return element ? element.getAttribute("id") : null; + } + + // public + Figures_getProperties = function(itemId) + { + var figure = document.getElementById(itemId); + if (figure == null) + return null; + var rect = figure.getBoundingClientRect(); + var result = { width: null, src: null }; + + var img = firstDescendantOfType(figure,HTML_IMG); + if (img != null) { + result.src = decodeURI(img.getAttribute("src")); + result.width = img.style.width; + + if ((result.width == null) || (result.width == "")) + result.width = DOM_getAttribute(img,"width"); + } + return result; + } + + // public + Figures_setProperties = function(itemId,width,src) + { + var figure = document.getElementById(itemId); + if (figure == null) + return null; + var img = firstDescendantOfType(figure,HTML_IMG); + if (img != null) { + if (src == null) + DOM_removeAttribute(img,"src"); + else + DOM_setAttribute(img,"src",encodeURI(src)); + + DOM_setStyleProperties(img,{"width": width}); + if (img.getAttribute("style") == "") + DOM_removeAttribute(img,"style"); + Selection_update(); + } + } + + // public + Figures_getGeometry = function(itemId) + { + var figure = document.getElementById(itemId); + if ((figure == null) || (figure.parentNode == null)) + return null; + var img = firstDescendantOfType(figure,HTML_IMG); + if (img == null) + return null; + + var figcaption = firstChildOfType(figure,HTML_FIGCAPTION); + + var result = new Object(); + result.contentRect = xywhAbsElementRect(img); + result.fullRect = xywhAbsElementRect(figure); + result.parentRect = xywhAbsElementRect(figure.parentNode); + result.hasCaption = (figcaption != null); + return result; + } + +})();