blob: 7362a9ee0926f5b3a55ea73a1da4e4cf29c6fa7c [file] [log] [blame]
// 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 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();
}
function tryDeleteEmptyCaption(pos)
{
var caption = Position_captionAncestor(pos);
if ((caption == null) || nodeHasContent(caption))
return false;
var container = Position_figureOrTableAncestor(pos);
if (container == null)
return false;
Cursor_set(container.parentNode,DOM_nodeOffset(container)+1);
Selection_preserveWhileExecuting(function() {
DOM_deleteNode(caption);
});
return true;
}
function tryDeleteEmptyNote(pos)
{
var note = Position_noteAncestor(pos);
if ((note == null) || nodeHasContent(note))
return false;
var parent = note.parentNode;
Cursor_set(note.parentNode,DOM_nodeOffset(note)+1);
Selection_preserveWhileExecuting(function() {
DOM_deleteNode(note);
});
return true;
}
// 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 cases of pressing backspace after a table, figure, TOC, hyperlink,
// footnote, or endnote. For each of these we delete the whole thing.
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) || isNoteNode(prevNode)) {
Cursor_set(back.node,back.offset-1);
Selection_preserveWhileExecuting(function() {
DOM_deleteNode(prevNode);
});
return;
}
}
// Backspace inside an empty figure or table caption
if (tryDeleteEmptyCaption(currentPos))
return;
currentPos = Position_preferTextPosition(currentPos);
var prevPos = Position_prevMatch(currentPos,Position_okForMovement);
// Backspace inside or just after a footnote or endnote
if (tryDeleteEmptyNote(currentPos))
return;
if ((prevPos != null) && tryDeleteEmptyNote(prevPos))
return;
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;
}
}
// Are we inside a footnote or endnote? If so, move the cursor immediately after it
var note = null;
if (selRange.start.node.nodeType == Node.TEXT_NODE) {
note = Position_noteAncestor(selRange.start);
}
else {
// We can't use Position_noteAncestor in this case, because we want to to break
// the paragraph *before* the note, not after
var checkNode = selRange.start.node;
for (var anc = checkNode; anc != null; anc = anc.parentNode) {
if (isNoteNode(anc)) {
note = anc;
break;
}
}
}
if (note != null) {
var noteOffset = DOM_nodeOffset(note);
selRange = new Range(note.parentNode,noteOffset+1,note.parentNode,noteOffset+1);
}
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 pos = Position_preferElementPosition(selRange.start);
var node = pos.node;
var offset = pos.offset;
while (true) {
if (node._type == type)
return node;
if (node.nodeType == Node.ELEMENT_NODE) {
var before = node.childNodes[offset-1];
if ((before != null) && (before._type == type))
return before;
var after = node.childNodes[offset];
if ((after != null) && (after._type == type))
return after;
}
if (node.parentNode == null)
return null;
offset = DOM_nodeOffset(node);
node = node.parentNode;
}
}
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 moveRangeOutsideOfNote(range)
{
var node = range.start.node;
var offset = range.start.offset;
for (var anc = node; anc != null; anc = anc.parentNode) {
if (isNoteNode(anc) && (anc.parentNode != null)) {
node = anc.parentNode;
offset = DOM_nodeOffset(anc)+1;
return new Range(node,offset,node,offset);
}
}
return range;
}
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();
range = moveRangeOutsideOfNote(range);
Formatting_splitAroundSelection(range,false);
// If we're part-way through a text node, splitAroundSelection will give us an
// empty text node between the before and after text. For formatting purposes that's
// fine (not sure if necessary), but when inserting a footnote or endnote we want
// to avoid this as it causes problems with cursor movement - specifically, the cursor
// is allowed to go inside the empty text node, and this doesn't show up in the correct
// position on screen.
var pos = range.start;
if ((pos.node._type == HTML_TEXT) &&
(pos.node.nodeValue.length == 0)) {
var empty = pos.node;
pos = new Position(empty.parentNode,DOM_nodeOffset(empty));
DOM_deleteNode(empty);
}
else {
pos = Position_preferElementPosition(pos);
}
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);
}
})();