corinthia-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From pmke...@apache.org
Subject [46/92] [abbrv] [partial] incubator-corinthia git commit: Add editing code from UX Write
Date Wed, 17 Dec 2014 13:28:56 GMT
http://git-wip-us.apache.org/repos/asf/incubator-corinthia/blob/03bd5af0/Editor/src/Outline.js
----------------------------------------------------------------------
diff --git a/Editor/src/Outline.js b/Editor/src/Outline.js
new file mode 100644
index 0000000..4d838df
--- /dev/null
+++ b/Editor/src/Outline.js
@@ -0,0 +1,1419 @@
+// 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.
+
+// FIXME: The TOC/ItemList stuff won't work with Undo, because we're making DOM mutations in
+// response to other DOM mutations, so at undo time the changes will be made twice
+
+var Outline_init;
+var Outline_removeListeners;
+var Outline_moveSection;
+var Outline_deleteItem;
+var Outline_goToItem;
+var Outline_setTitle;
+var Outline_setNumbered;
+var Outline_getItemElement;
+var Outline_getOutline;
+var Outline_plainText;
+var Outline_insertTableOfContents;
+var Outline_insertListOfFigures;
+var Outline_insertListOfTables;
+var Outline_setPrintMode;
+var Outline_examinePrintLayout;
+var Outline_setReferenceTarget;
+var Outline_detectSectionNumbering;
+var Outline_findUsedStyles;
+var Outline_scheduleUpdateStructure;
+
+(function() {
+
+    var itemsByNode = null;
+    var refsById = null;
+    var nextItemId = 1;
+    var outlineDirty = false;
+    var ignoreModifications = 0;
+    var sectionNumberRegex = /^\s*(Chapter\s+)?\d+(\.\d+)*\.?\s+/i;
+    var figureNumberRegex = /^\s*Figure\s+\d+(\.\d+)*:?\s*/i;
+    var tableNumberRegex = /^\s*Table\s+\d+(\.\d+)*:?\s*/i;
+    var sections = null;
+    var figures = null;
+    var tables = null;
+    var doneInit = false;
+    var printMode = false;
+
+    function Category(type,nodeFilter,numberRegex)
+    {
+        this.type = type;
+        this.nodeFilter = nodeFilter;
+        this.numberRegex = numberRegex;
+        this.list = new DoublyLinkedList();
+        this.tocs = new NodeMap();
+    }
+
+    function addItemInternal(category,item,prevItem,title)
+    {
+        UndoManager_addAction(removeItemInternal,category,item);
+        category.list.insertAfter(item,prevItem);
+        item.title = title;
+        category.tocs.forEach(function(node,toc) { TOC_addOutlineItem(toc,item.id); });
+        Editor_addOutlineItem(item.id,category.type,title);
+    }
+
+    function removeItemInternal(category,item)
+    {
+        UndoManager_addAction(addItemInternal,category,item,item.prev,item.title);
+        category.list.remove(item);
+        category.tocs.forEach(function(node,toc) { TOC_removeOutlineItem(toc,item.id); });
+        item.title = null;
+        Editor_removeOutlineItem(item.id);
+    }
+
+    function Category_add(category,node)
+    {
+        var item = itemsByNode.get(node);
+        if (item == null)
+            item = new OutlineItem(category,node);
+
+        var prevItem = findPrevItemOfType(node,category.nodeFilter);
+        addItemInternal(category,item,prevItem,null);
+
+        // Register for notifications to changes to this item's node content. We may need to
+        // update the title when such a modification occurs.
+        node.addEventListener("DOMSubtreeModified",item.modificationListener);
+
+        OutlineItem_updateItemTitle(item);
+        scheduleUpdateStructure();
+        return item;
+
+        function findPrevItemOfType(node,typeFun)
+        {
+            do node = prevNode(node);
+            while ((node != null) && !typeFun(node));
+            return (node == null) ? null : itemsByNode.get(node);
+        }
+    }
+
+    function findFirstTextDescendant(node)
+    {
+        if (isWhitespaceTextNode(node))
+            return;
+        if (node.nodeType == Node.TEXT_NODE)
+            return node;
+        for (var child = node.firstChild; child != null; child = child.nextSibling) {
+            var result = findFirstTextDescendant(child);
+            if (result != null)
+                return result;
+        }
+        return null;
+    }
+
+    function Category_remove(category,node)
+    {
+        var item = itemsByNode.get(node);
+        if (item == null) {
+            throw new Error("Attempt to remove non-existant "+node.nodeName+
+                            " item "+node.getAttribute("id"));
+        }
+        removeItemInternal(category,item);
+        item.node.removeEventListener("DOMSubtreeModified",item.modificationListener);
+        var titleNode = OutlineItem_getTitleNode(item,false);
+        if ((titleNode != null) &&
+            ((item.type == "figure") || (item.type == "table")) &&
+            (titleNode.firstChild == null) &&
+            (titleNode.lastChild == null)) {
+            DOM_deleteNode(titleNode);
+        }
+        scheduleUpdateStructure();
+    }
+
+    function addTOCInternal(category,node,toc)
+    {
+        UndoManager_addAction(removeTOCInternal,category,node);
+        category.tocs.put(node,toc);
+    }
+
+    function removeTOCInternal(category,node)
+    {
+        var toc = category.tocs.get(node);
+        if (toc == null)
+            throw new Error("Attempt to remove ItemList that doesn't exist");
+
+        UndoManager_addAction(addTOCInternal,category,node,toc);
+
+        category.tocs.remove(node);
+    }
+
+    function Category_addTOC(category,node)
+    {
+        var toc = new TOC(node);
+        addTOCInternal(category,node,toc);
+
+        for (var item = category.list.first; item != null; item = item.next) {
+            TOC_addOutlineItem(toc,item.id);
+            TOC_updateOutlineItem(toc,item.id,item.title);
+        }
+
+        scheduleUpdateStructure();
+    }
+
+    function Category_removeTOC(category,node)
+    {
+        removeTOCInternal(category,node);
+    }
+
+    function TOC(node)
+    {
+        this.node = node;
+        this.textNodes = new Object();
+    }
+
+    function TOC_addOutlineItem(toc,id)
+    {
+        toc.textNodes[id] = DOM_createTextNode(document,"");
+    }
+
+    function TOC_removeOutlineItem(toc,id)
+    {
+        delete toc.textNodes[id];
+    }
+
+    function TOC_updateOutlineItem(toc,id,title)
+    {
+        DOM_setNodeValue(toc.textNodes[id],title);
+    }
+
+    function TOC_updateStructure(toc,structure,toplevelShadows,pageNumbers)
+    {
+        Hierarchy_ensureValidHierarchy(toc.node);
+        DOM_deleteAllChildren(toc.node);
+
+        var cls = toc.node.getAttribute("class");
+
+        if (toplevelShadows.length == 0) {
+            createEmptyTOC(toc.node);
+        }
+        else {
+            recurse(toplevelShadows,toc.node,1);
+        }
+
+        if (printMode) {
+            var brk = DOM_createElement(document,"DIV");
+            DOM_setStyleProperties(brk,{ "clear": "both" });
+            DOM_appendChild(toc.node,brk);
+        }
+
+        function createEmptyTOC(parent)
+        {
+            if (!printMode) {
+                var str = "";
+
+                if (cls == Keys.SECTION_TOC)
+                    str = "[No sections defined]";
+                else if (cls == Keys.FIGURE_TOC)
+                    str = "[No figures defined]";
+                else if (cls == Keys.TABLE_TOC)
+                    str = "[No tables defined]";
+
+                var text = DOM_createTextNode(document,str);
+
+                var div = DOM_createElement(document,"P");
+                DOM_setAttribute(div,"class","toc1");
+                DOM_appendChild(div,text);
+                DOM_appendChild(parent,div);
+            }
+        }
+
+        function recurse(shadows,parent,level)
+        {
+            if (level > 3)
+                return;
+
+            for (var i = 0; i < shadows.length; i++) {
+                var shadow = shadows[i];
+                var item = shadow.item;
+
+                if (printMode) {
+                    var div = DOM_createElement(document,"P");
+                    DOM_setAttribute(div,"class","toc"+level+"-print");
+                    DOM_appendChild(parent,div);
+
+                    var leftSpan = DOM_createElement(document,"SPAN");
+                    DOM_setAttribute(leftSpan,"class","toctitle");
+
+                    var rightSpan = DOM_createElement(document,"SPAN");
+                    DOM_setAttribute(rightSpan,"class","tocpageno");
+
+                    DOM_appendChild(div,leftSpan);
+                    DOM_appendChild(div,rightSpan);
+
+                    if (item.computedNumber != null) {
+                        var text = DOM_createTextNode(document,item.computedNumber+" ");
+                        DOM_appendChild(leftSpan,text);
+                    }
+
+                    DOM_appendChild(leftSpan,toc.textNodes[item.id]);
+                    var pageNo = pageNumbers ? pageNumbers.get(item.node) : null;
+                    if (pageNo == null)
+                        DOM_appendChild(rightSpan,DOM_createTextNode(document,"XXXX"));
+                    else
+                        DOM_appendChild(rightSpan,DOM_createTextNode(document,pageNo));
+                }
+                else {
+                    var div = DOM_createElement(document,"P");
+                    DOM_setAttribute(div,"class","toc"+level);
+                    DOM_appendChild(parent,div);
+
+                    var a = DOM_createElement(document,"A");
+                    DOM_setAttribute(a,"href","#"+item.id);
+                    DOM_appendChild(div,a);
+
+                    if (item.computedNumber != null)
+                        DOM_appendChild(a,DOM_createTextNode(document,item.computedNumber+" "));
+                    DOM_appendChild(a,toc.textNodes[item.id]);
+                }
+
+                recurse(shadow.children,parent,level+1);
+            }
+        }
+    }
+
+    function OutlineItem(category,node)
+    {
+        var type = category.type;
+        var item = this;
+        if ((node != null) && (node.hasAttribute("id"))) {
+            this.id = node.getAttribute("id");
+        }
+        else {
+            this.id = generateItemId();
+            if (node != null)
+                DOM_setAttribute(node,"id",this.id);
+        }
+        this.category = category;
+        this.type = type;
+        this.node = node;
+        this.title = null;
+        this.computedNumber = null;
+
+        this.spareSpan = DOM_createElement(document,"SPAN");
+        DOM_appendChild(this.spareSpan,DOM_createTextNode(document,""));
+        var spanClass = null;
+        if (this.type == "section")
+            spanClass = Keys.HEADING_NUMBER;
+        else if (this.type == "figure")
+            spanClass = Keys.FIGURE_NUMBER;
+        else if (this.type == "table")
+            spanClass = Keys.TABLE_NUMBER;
+        DOM_setAttribute(this.spareSpan,"class",spanClass);
+
+        // titleNode
+        if (this.type == "figure") {
+            this.spareTitle = DOM_createElement(document,"FIGCAPTION");
+        }
+        else if (this.type == "table") {
+            this.spareTitle = DOM_createElement(document,"CAPTION");
+        }
+
+        this.prev = null;
+        this.next = null;
+        this.modificationListener = function(event) { itemModified(item); }
+
+        itemsByNode.put(this.node,this);
+
+        Object.seal(this);
+        return;
+
+        function generateItemId()
+        {
+            var id;
+            do {
+                id = "item"+(nextItemId++);
+            } while (document.getElementById(id) != null);
+            return id;
+        }
+    }
+
+    function OutlineItem_getTitleNode(item,create)
+    {
+        if (item.type == "section") {
+            return item.node;
+        }
+        else if (item.type == "figure") {
+            var titleNode = findChild(item.node,HTML_FIGCAPTION);
+            if ((titleNode == null) && create) {
+                titleNode = item.spareTitle;
+                DOM_appendChild(item.node,titleNode);
+            }
+            return titleNode;
+        }
+        else if (item.type == "table") {
+            var titleNode = findChild(item.node,HTML_CAPTION);
+            if ((titleNode == null) && create) {
+                titleNode = item.spareTitle;
+                DOM_insertBefore(item.node,titleNode,item.node.firstChild);
+            }
+            return titleNode;
+        }
+
+        function findChild(node,type)
+        {
+            for (var child = node.firstChild; child != null; child = child.nextSibling) {
+                if (child._type == type)
+                    return child;
+            }
+            return null;
+        }
+    }
+
+    function OutlineItem_updateItemTitle(item)
+    {
+        var titleNode = OutlineItem_getTitleNode(item,false);
+        if (titleNode != null)
+            newTitle = normalizeWhitespace(getNodeText(titleNode));
+        else
+            newTitle = "";
+
+        if (item.title != newTitle) {
+            UndoManager_addAction(Editor_updateOutlineItem,item.id,item.title);
+            Editor_updateOutlineItem(item.id,newTitle);
+            item.title = newTitle;
+            item.category.tocs.forEach(function(node,toc) {
+                TOC_updateOutlineItem(toc,item.id,item.title);
+            });
+        }
+    }
+
+    function getNodeTextAfter(node)
+    {
+        var text = "";
+        for (var child = node.nextSibling; child != null; child = child.nextSibling)
+            text += getNodeText(child);
+        return text;
+    }
+
+    // private
+    function itemModified(item)
+    {
+        if (UndoManager_isActive())
+            return;
+        if (ignoreModifications > 0)
+            return;
+        OutlineItem_updateItemTitle(item);
+        updateRefsForItem(item);
+    }
+
+    function addRefForId(id,node)
+    {
+        UndoManager_addAction(removeRefForId,id,node);
+        if (refsById[id] == null)
+            refsById[id] = new Array();
+        refsById[id].push(node);
+    }
+
+    function removeRefForId(id,node)
+    {
+        UndoManager_addAction(addRefForId,id,node);
+        if (refsById[id] == null)
+            throw new Error("refRemoved: refsById["+id+"] is null");
+        var index = refsById[id].indexOf(node);
+        if (index < 0)
+            throw new Error("refRemoved: refsById["+id+"] does not contain node");
+        refsById[id].splice(index,1);
+        if (refsById[id] == null)
+            delete refsById[id];
+    }
+
+    // private
+    function refInserted(node)
+    {
+        var href = node.getAttribute("href");
+        if (href.charAt(0) != "#")
+            throw new Error("refInserted: not a # reference");
+        var id = href.substring(1);
+        addRefForId(id,node);
+        scheduleUpdateStructure();
+    }
+
+    // private
+    function refRemoved(node)
+    {
+        var href = node.getAttribute("href");
+        if (href.charAt(0) != "#")
+            throw new Error("refInserted: not a # reference");
+        var id = href.substring(1);
+        removeRefForId(id,node);
+    }
+
+    // private
+    function acceptNode(node)
+    {
+        for (var p = node; p != null; p = p.parentNode) {
+            if ((p._type == HTML_SPAN) && (p.getAttribute("class") == Keys.HEADING_NUMBER))
+                return false;
+        }
+        return true;
+    }
+
+    // private
+    function docNodeInserted(event)
+    {
+        if (UndoManager_isActive())
+            return;
+        if (DOM_getIgnoreMutations())
+            return;
+        try {
+            if (!acceptNode(event.target))
+                return;
+            recurse(event.target);
+        }
+        catch (e) {
+            Editor_error(e);
+        }
+
+        function recurse(node)
+        {
+            switch (node._type) {
+            case HTML_H1:
+            case HTML_H2:
+            case HTML_H3:
+            case HTML_H4:
+            case HTML_H5:
+            case HTML_H6: {
+                if (!isInTOC(node))
+                    Category_add(sections,node);
+                break;
+            }
+            case HTML_FIGURE:
+                Category_add(figures,node);
+                break;
+            case HTML_TABLE:
+                Category_add(tables,node);
+                break;
+            case HTML_A: {
+                if (isRefNode(node) && !isInTOC(node)) {
+                    refInserted(node);
+                }
+                break;
+            }
+            case HTML_NAV: {
+                var cls = node.getAttribute("class");
+                if (cls == Keys.SECTION_TOC)
+                    Category_addTOC(sections,node);
+                else if (cls == Keys.FIGURE_TOC)
+                    Category_addTOC(figures,node);
+                else if (cls == Keys.TABLE_TOC)
+                    Category_addTOC(tables,node);
+                break;
+            }
+            }
+
+            var next;
+            for (var child = node.firstChild; child != null; child = next) {
+                next = child.nextSibling;
+                recurse(child);
+            }
+        }
+    }
+
+    // private
+    function docNodeRemoved(event)
+    {
+        if (UndoManager_isActive())
+            return;
+        if (DOM_getIgnoreMutations())
+            return;
+        try {
+            if (!acceptNode(event.target))
+                return;
+            recurse(event.target);
+        }
+        catch (e) {
+            Editor_error(e);
+        }
+
+        function recurse(node)
+        {
+            switch (node._type) {
+            case HTML_H1:
+            case HTML_H2:
+            case HTML_H3:
+            case HTML_H4:
+            case HTML_H5:
+            case HTML_H6:
+                if (!isInTOC(node))
+                    Category_remove(sections,node);
+                break;
+            case HTML_FIGURE:
+                Category_remove(figures,node);
+                break;
+            case HTML_TABLE:
+                Category_remove(tables,node);
+                break;
+            case HTML_A:
+                if (isRefNode(node) && !isInTOC(node))
+                    refRemoved(node);
+                break;
+            case HTML_NAV:
+                var cls = node.getAttribute("class");
+                if (cls == Keys.SECTION_TOC)
+                    Category_removeTOC(sections,node);
+                else if (cls == Keys.FIGURE_TOC)
+                    Category_removeTOC(figures,node);
+                else if (cls == Keys.TABLE_TOC)
+                    Category_removeTOC(tables,node);
+                break;
+            }
+
+            for (var child = node.firstChild; child != null; child = child.nextSibling)
+                recurse(child);
+        }
+    }
+
+    // private
+    function scheduleUpdateStructure()
+    {
+        if (UndoManager_isActive())
+            return;
+        if (!outlineDirty) {
+            outlineDirty = true;
+            PostponedActions_add(updateStructure);
+        }
+    }
+
+    Outline_scheduleUpdateStructure = scheduleUpdateStructure;
+
+    // private
+    function updateStructure()
+    {
+        if (!outlineDirty)
+            return;
+        outlineDirty = false;
+        if (UndoManager_isActive())
+            throw new Error("Structure update event while undo or redo active");
+        Selection_preserveWhileExecuting(function() {
+            updateStructureReal();
+        });
+    }
+
+    function Shadow(node)
+    {
+        this.node = node;
+        this.item = itemsByNode.get(node);
+        this.children = [];
+        this.parent = null;
+
+        switch (node._type) {
+        case HTML_H1:
+            this.level = 1;
+            break;
+        case HTML_H2:
+            this.level = 2;
+            break;
+        case HTML_H3:
+            this.level = 3;
+            break;
+        case HTML_H4:
+            this.level = 4;
+            break;
+        case HTML_H5:
+            this.level = 5;
+            break;
+        case HTML_H6:
+            this.level = 6;
+            break;
+        default:
+            this.level = 0;
+            break;
+        }
+    }
+
+    function Shadow_last(shadow)
+    {
+        if (shadow.children.length == 0)
+            return shadow;
+        else
+            return Shadow_last(shadow.children[shadow.children.length-1]);
+    }
+
+    function Shadow_outerNext(shadow,structure)
+    {
+        var last = Shadow_last(shadow);
+        if (last == null)
+            return null;
+        else if (last.item.next == null)
+            return null;
+        else
+            return structure.shadowsByNode.get(last.item.next.node);
+    }
+
+    function firstTextDescendant(node)
+    {
+        if (node.nodeType == Node.TEXT_NODE)
+            return node;
+        for (var child = node.firstChild; child != null; child = child.nextSibling) {
+            var result = firstTextDescendant(child);
+            if (result != null)
+                return result;
+        }
+        return null;
+    }
+
+    function Structure()
+    {
+        this.toplevelSections = new Array();
+        this.toplevelFigures = new Array();
+        this.toplevelTables = new Array();
+        this.shadowsByNode = new NodeMap();
+    }
+
+    function discoverStructure()
+    {
+        var structure = new Structure();
+        var nextToplevelSectionNumber = 1;
+        var nextFigureNumber = 1;
+        var nextTableNumber = 1;
+        var headingNumbering = Styles_headingNumbering();
+
+        var counters = { h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0, table: 0, figure: 0 };
+
+        var current = null;
+
+        for (var section = sections.list.first; section != null; section = section.next) {
+            structure.shadowsByNode.put(section.node,new Shadow(section.node));
+        }
+        for (var figure = figures.list.first; figure != null; figure = figure.next) {
+            structure.shadowsByNode.put(figure.node,new Shadow(figure.node));
+        }
+        for (var table = tables.list.first; table != null; table = table.next) {
+            structure.shadowsByNode.put(table.node,new Shadow(table.node));
+        }
+
+        for (var section = sections.list.first; section != null; section = section.next) {
+            var shadow = structure.shadowsByNode.get(section.node);
+            shadow.parent = null;
+            shadow.children = [];
+            shadow.nextChildSectionNumber = 1;
+        }
+
+        ignoreModifications++;
+
+        for (var section = sections.list.first; section != null; section = section.next) {
+            var shadow = structure.shadowsByNode.get(section.node);
+            var node = section.node;
+            var item = shadow.item;
+
+            if (!headingNumbering || (DOM_getAttribute(item.node,"class") == "Unnumbered")) {
+                item.computedNumber = null;
+            }
+            else {
+                var level = parseInt(node.nodeName.charAt(1));
+                counters[node.nodeName.toLowerCase()]++;
+                for (var inner = level+1; inner <= 6; inner++)
+                    counters["h"+inner] = 0;
+                item.computedNumber = "";
+                for (var i = 1; i <= level; i++) {
+                    if (i == 1)
+                        item.computedNumber += counters["h"+i];
+                    else
+                        item.computedNumber += "." + counters["h"+i];
+                }
+            }
+
+            while ((current != null) && (shadow.level < current.level+1))
+                current = current.parent;
+
+            shadow.parent = current;
+            if (current == null)
+                structure.toplevelSections.push(shadow);
+            else
+                current.children.push(shadow);
+
+            current = shadow;
+        }
+
+        for (var figure = figures.list.first; figure != null; figure = figure.next) {
+            var shadow = structure.shadowsByNode.get(figure.node);
+            var item = shadow.item;
+
+            var titleNode = OutlineItem_getTitleNode(item,false);
+            if ((titleNode == null) || DOM_getAttribute(titleNode,"class") == "Unnumbered") {
+                item.computedNumber = null;
+            }
+            else {
+                counters.figure++;
+                item.computedNumber = ""+counters.figure;
+            }
+
+            structure.toplevelFigures.push(shadow);
+        }
+
+        for (var table = tables.list.first; table != null; table = table.next) {
+            var shadow = structure.shadowsByNode.get(table.node);
+            var item = shadow.item;
+
+            var titleNode = OutlineItem_getTitleNode(item,false);
+            if ((titleNode == null) || DOM_getAttribute(titleNode,"class") == "Unnumbered") {
+                item.computedNumber = null;
+            }
+            else {
+                counters.table++;
+                item.computedNumber = ""+counters.table;
+            }
+
+            structure.toplevelTables.push(shadow);
+        }
+
+        ignoreModifications--;
+
+        return structure;
+    }
+
+    function updateStructureReal(pageNumbers)
+    {
+        var structure = discoverStructure();
+
+        for (var section = sections.list.first; section != null; section = section.next) {
+            var shadow = structure.shadowsByNode.get(section.node);
+            updateRefsForItem(shadow.item);
+        }
+
+        for (var figure = figures.list.first; figure != null; figure = figure.next) {
+            var shadow = structure.shadowsByNode.get(figure.node);
+            updateRefsForItem(shadow.item);
+        }
+
+        for (var table = tables.list.first; table != null; table = table.next) {
+            var shadow = structure.shadowsByNode.get(table.node);
+            updateRefsForItem(shadow.item);
+        }
+
+        sections.tocs.forEach(function (node,toc) {
+            TOC_updateStructure(toc,structure,structure.toplevelSections,pageNumbers);
+        });
+        figures.tocs.forEach(function (node,toc) {
+            TOC_updateStructure(toc,structure,structure.toplevelFigures,pageNumbers);
+        });
+        tables.tocs.forEach(function (node,toc) {
+            TOC_updateStructure(toc,structure,structure.toplevelTables,pageNumbers);
+        });
+
+        Editor_outlineUpdated();
+    }
+
+    Outline_getOutline = function()
+    {
+        var structure = discoverStructure();
+        var encSections = new Array();
+        var encFigures = new Array();
+        var encTables = new Array();
+
+        for (var i = 0; i < structure.toplevelSections.length; i++)
+            encodeShadow(structure.toplevelSections[i],encSections);
+        for (var i = 0; i < structure.toplevelFigures.length; i++)
+            encodeShadow(structure.toplevelFigures[i],encFigures);
+        for (var i = 0; i < structure.toplevelTables.length; i++)
+            encodeShadow(structure.toplevelTables[i],encTables);
+
+        return { sections: encSections,
+                 figures: encFigures,
+                 tables: encTables };
+
+        function encodeShadow(shadow,result)
+        {
+            var encChildren = new Array();
+            for (var i = 0; i < shadow.children.length; i++)
+                encodeShadow(shadow.children[i],encChildren);
+
+            var obj = { id: shadow.item.id,
+                        number: shadow.item.computedNumber ? shadow.item.computedNumber : "",
+                        children: encChildren };
+            result.push(obj);
+        }
+    }
+
+    function updateRefsForItem(item)
+    {
+        var id = item.node.getAttribute("id");
+        var refs = refsById[id];
+        if (refs == null)
+            return;
+        for (var i = 0; i < refs.length; i++) {
+            DOM_deleteAllChildren(refs[i]);
+            var text = null;
+
+            var className = DOM_getAttribute(refs[i],"class");
+            if (className == "uxwrite-ref-num") {
+                text = item.computedNumber;
+            }
+            else if (className == "uxwrite-ref-text") {
+                if (item.type == "section") {
+                    if (item.numberSpan != null)
+                        text = getNodeTextAfter(item.numberSpan);
+                    else
+                        text = normalizeWhitespace(getNodeText(item.node));
+                }
+                else if ((item.type == "figure") || (item.type == "table")) {
+                    var titleNode = OutlineItem_getTitleNode(item,false);
+                    if (titleNode != null) {
+                        text = getNodeText(titleNode);
+
+                        if ((item.computedNumber != null) && (item.type == "figure"))
+                            text = "Figure "+item.computedNumber+": "+text;
+                        else if ((item.computedNumber != null) && (item.type == "table"))
+                            text = "Table "+item.computedNumber+": "+text;
+                    }
+                }
+            }
+            else if (className == "uxwrite-ref-caption-text") {
+                if (item.type == "section") {
+                    if (item.numberSpan != null)
+                        text = getNodeTextAfter(item.numberSpan);
+                    else
+                        text = normalizeWhitespace(getNodeText(item.node));
+                }
+                else if ((item.type == "figure") || (item.type == "table")) {
+                    var titleNode = OutlineItem_getTitleNode(item,false);
+                    if (titleNode != null) {
+                        if (item.numberSpan != null)
+                            text = getNodeTextAfter(item.numberSpan);
+                        else
+                            text = normalizeWhitespace(getNodeText(titleNode));
+                    }
+                }
+            }
+            else if (className == "uxwrite-ref-label-num") {
+                if (item.computedNumber != null) {
+                    if (item.type == "section")
+                        text = "Section "+item.computedNumber;
+                    else if (item.type == "figure")
+                        text = "Figure "+item.computedNumber;
+                    else if (item.type == "table")
+                        text = "Table "+item.computedNumber;
+                }
+            }
+            else {
+                if (item.computedNumber != null)
+                    text = item.computedNumber;
+                else
+                    text = item.title;
+            }
+
+            if (text == null)
+                text = "?";
+
+            DOM_appendChild(refs[i],DOM_createTextNode(document,text));
+        }
+    }
+
+    Outline_plainText = function()
+    {
+        var strings = new Array();
+        var structure = discoverStructure();
+
+        strings.push("Sections:\n");
+        for (var section = sections.list.first; section != null; section = section.next) {
+            var shadow = structure.shadowsByNode.get(section.node);
+            if (shadow.level == 1)
+                printSectionRecursive(shadow,"    ");
+        }
+        strings.push("Figures:\n");
+        for (var figure = figures.list.first; figure != null; figure = figure.next) {
+            var shadow = structure.shadowsByNode.get(figure.node);
+            var titleNode = OutlineItem_getTitleNode(figure,false);
+            var title = titleNode ? getNodeText(titleNode) : "[no caption]";
+            if (shadow.item.computedNumber != null) {
+                if (title.length > 0)
+                    title = shadow.item.computedNumber+" "+title;
+                else
+                    title = shadow.item.computedNumber;
+            }
+            strings.push("    "+title+" ("+figure.id+")\n");
+        }
+        strings.push("Tables:\n");
+        for (var table = tables.list.first; table != null; table = table.next) {
+            var shadow = structure.shadowsByNode.get(table.node);
+            var titleNode = OutlineItem_getTitleNode(table,false);
+            var title = titleNode ? getNodeText(titleNode) : "[no caption]";
+            if (shadow.item.computedNumber != null) {
+                if (title.length > 0)
+                    title = shadow.item.computedNumber+" "+title;
+                else
+                    title = shadow.item.computedNumber;
+            }
+            strings.push("    "+title+" ("+table.id+")\n");
+        }
+        return strings.join("");
+
+        function printSectionRecursive(shadow,indent)
+        {
+            var titleNode = OutlineItem_getTitleNode(shadow.item,false);
+            var content = getNodeText(titleNode);
+            if (shadow.item.computedNumber != null)
+                content = shadow.item.computedNumber+" "+content;
+            if (isWhitespaceString(content))
+                content = "[empty]";
+            strings.push(indent+content+" ("+shadow.item.id+")\n");
+            for (var i = 0; i < shadow.children.length; i++)
+                printSectionRecursive(shadow.children[i],indent+"    ");
+        }
+    }
+
+    // public
+    Outline_init = function()
+    {
+        Selection_preserveWhileExecuting(function() {
+
+            function isTableNode(node)
+            {
+                return (node._type == HTML_TABLE);
+            }
+
+            function isFigureNode(node)
+            {
+                return (node._type == HTML_FIGURE);
+            }
+
+            function isNonTOCHeadingNode(node)
+            {
+                return (HEADING_ELEMENTS[node._type] && !isInTOC(node));
+            }
+
+            sections = new Category("section",isNonTOCHeadingNode,sectionNumberRegex);
+            figures = new Category("figure",isFigureNode,figureNumberRegex);
+            tables = new Category("table",isTableNode,tableNumberRegex);
+            itemsByNode = new NodeMap();
+            refsById = new Object();
+
+            DOM_ensureUniqueIds(document.documentElement);
+            document.addEventListener("DOMNodeInserted",docNodeInserted);
+            document.addEventListener("DOMNodeRemoved",docNodeRemoved);
+
+            docNodeInserted({target:document});
+        });
+        doneInit = true;
+    }
+
+    // public (for the undo tests, when they report results)
+    Outline_removeListeners = function()
+    {
+        document.removeEventListener("DOMNodeInserted",docNodeInserted);
+        document.removeEventListener("DOMNodeRemoved",docNodeRemoved);
+
+        removeCategoryListeners(sections);
+        removeCategoryListeners(figures);
+        removeCategoryListeners(tables);
+
+        function removeCategoryListeners(category)
+        {
+            for (var item = category.list.first; item != null; item = item.next)
+                item.node.removeEventListener("DOMSubtreeModified",item.modificationListener);
+        }
+    }
+
+    // private
+    function getShadowNodes(structure,shadow,result)
+    {
+        var endShadow = Shadow_outerNext(shadow,structure);
+        var endNode = endShadow ? endShadow.item.node : null;
+        for (var n = shadow.item.node; (n != null) && (n != endNode); n = n.nextSibling)
+            result.push(n);
+    }
+
+    // public
+    Outline_moveSection = function(sectionId,parentId,nextId)
+    {
+        UndoManager_newGroup("Move section");
+        Selection_clear();
+
+        updateStructure(); // make sure pointers are valid
+        // FIXME: I don't think we'll need the updateStructure() call now that we have
+        // discoverStructure(). In fact this function is a perfect illustration of why
+        // waiting till after the postponed action has been performed before relying on the
+        // pointer validity was a problem.
+
+
+        var structure = discoverStructure();
+
+        var node = document.getElementById(sectionId);
+        var section = itemsByNode.get(node);
+        var shadow = structure.shadowsByNode.get(node);
+
+        // FIXME: We should throw an exception if a parentId or nextId which does not exist
+        // in the document is specified. However there are currently some tests (like
+        // moveSection-nested*) which rely us interpreting such parameters as null.
+        var parentNode = parentId ? document.getElementById(parentId) : null;
+        var nextNode = nextId ? document.getElementById(nextId) : null;
+        var parent = parentNode ? structure.shadowsByNode.get(parentNode) : null;
+        var next = nextNode ? structure.shadowsByNode.get(nextNode) : null;
+
+        var sectionNodes = new Array();
+        getShadowNodes(structure,shadow,sectionNodes);
+
+        if ((next == null) && (parent != null))
+            next = Shadow_outerNext(parent,structure);
+
+        if (next == null) {
+            for (var i = 0; i < sectionNodes.length; i++)
+                DOM_appendChild(document.body,sectionNodes[i]);
+        }
+        else {
+            for (var i = 0; i < sectionNodes.length; i++)
+                DOM_insertBefore(next.item.node.parentNode,sectionNodes[i],next.item.node);
+        }
+
+        var pos = new Position(node,0,node,0);
+        pos = Position_closestMatchForwards(pos,Position_okForInsertion);
+        Selection_set(pos.node,pos.offset,pos.node,pos.offset);
+
+        scheduleUpdateStructure();
+        PostponedActions_add(UndoManager_newGroup);
+    }
+
+    // public
+    Outline_deleteItem = function(itemId)
+    {
+        UndoManager_newGroup("Delete outline item");
+        var structure = discoverStructure();
+        Selection_preserveWhileExecuting(function() {
+            var node = document.getElementById(itemId);
+            var item = itemsByNode.get(node);
+            var shadow = structure.shadowsByNode.get(item.node);
+            if (item.type == "section") {
+                var sectionNodes = new Array();
+                getShadowNodes(structure,shadow,sectionNodes);
+                for (var i = 0; i < sectionNodes.length; i++)
+                    DOM_deleteNode(sectionNodes[i]);
+            }
+            else {
+                DOM_deleteNode(item.node);
+            }
+        });
+
+        scheduleUpdateStructure();
+        PostponedActions_add(UndoManager_newGroup);
+    }
+
+    // public
+    Outline_goToItem = function(itemId)
+    {
+        if (itemId == null) {
+            window.scrollTo(0);
+        }
+        else {
+            var node = document.getElementById(itemId);
+            if (node == null) {
+                // FIXME: this can happen if the user added some headings, pressed undo one or
+                // more times (in which case the editor's view of the outline structure fails to
+                // be updated), and then they click on an item. This is really an error but we
+                // handle it gracefully for now rather than causing a null pointer exception to
+                // be thrown.
+                return;
+            }
+            var position = new Position(node,0);
+            position = Position_closestMatchForwards(position,Position_okForMovement);
+            Selection_set(position.node,position.offset,position.node,position.offset);
+
+            var section = document.getElementById(itemId);
+            var location = webkitConvertPointFromNodeToPage(section,new WebKitPoint(0,0));
+            window.scrollTo(0,location.y);
+        }
+    }
+
+    // public
+    Outline_getItemElement = function(itemId)
+    {
+        return document.getElementById(itemId);
+    }
+
+    // public
+    Outline_setNumbered = function(itemId,numbered)
+    {
+        var node = document.getElementById(itemId);
+        var item = itemsByNode.get(node);
+
+        Selection_preserveWhileExecuting(function() {
+            if (item.type == "section") {
+                if (numbered)
+                    DOM_removeAttribute(node,"class");
+                else
+                    DOM_setAttribute(node,"class","Unnumbered");
+            }
+            else if ((item.type == "figure") || (item.type == "table")) {
+                if (numbered) {
+                    var caption = OutlineItem_getTitleNode(item,true);
+                    DOM_removeAttribute(caption,"class");
+                }
+                else {
+                    var caption = OutlineItem_getTitleNode(item,false);
+                    if (caption != null) {
+                        if (nodeHasContent(caption))
+                            DOM_setAttribute(caption,"class","Unnumbered");
+                        else
+                            DOM_deleteNode(caption);
+                    }
+                }
+            }
+        });
+
+        scheduleUpdateStructure();
+    }
+
+    // public
+    Outline_setTitle = function(itemId,title)
+    {
+        var node = document.getElementById(itemId);
+        var item = itemsByNode.get(node);
+        Selection_preserveWhileExecuting(function() {
+            var titleNode = OutlineItem_getTitleNode(item,true);
+            var oldEmpty = (item.title == "");
+            var newEmpty = (title == "");
+            if (oldEmpty != newEmpty) {
+                // Add or remove the : at the end of table and figure numbers
+                scheduleUpdateStructure();
+            }
+            if (item.numberSpan != null) {
+                while (item.numberSpan.nextSibling != null)
+                    DOM_deleteNode(item.numberSpan.nextSibling);
+            }
+            else {
+                DOM_deleteAllChildren(titleNode);
+            }
+            DOM_appendChild(titleNode,DOM_createTextNode(document,title));
+            OutlineItem_updateItemTitle(item);
+        });
+    }
+
+    // private
+    // FIXME: prevent a TOC from being inserted inside a heading, figure, or table
+    function insertTOC(key,initialText)
+    {
+        var div = DOM_createElement(document,"NAV");
+        DOM_setAttribute(div,"class",key);
+        Cursor_makeContainerInsertionPoint();
+        Clipboard_pasteNodes([div]);
+    }
+
+    // public
+    Outline_insertTableOfContents = function()
+    {
+        insertTOC(Keys.SECTION_TOC);
+    }
+
+    // public
+    Outline_insertListOfFigures = function()
+    {
+        insertTOC(Keys.FIGURE_TOC);
+    }
+
+    // public
+    Outline_insertListOfTables = function()
+    {
+        insertTOC(Keys.TABLE_TOC);
+    }
+
+    // public
+    Outline_setPrintMode = function(newPrintMode)
+    {
+        printMode = newPrintMode;
+        scheduleUpdateStructure();
+    }
+
+    // public
+    Outline_examinePrintLayout = function(pageHeight)
+    {
+        var result = new Object();
+        var structure = discoverStructure();
+        var pageNumbers = new NodeMap();
+
+        result.destsByPage = new Object();
+        result.linksByPage = new Object();
+        result.leafRectsByPage = new Object();
+
+        itemsByNode.forEach(function(node,item) {
+            var rect = node.getBoundingClientRect();
+            var pageNo = 1+Math.floor(rect.top/pageHeight);
+            var pageTop = (pageNo-1)*pageHeight;
+            var id = node.getAttribute("id");
+            pageNumbers.put(node,pageNo);
+
+            if (result.destsByPage[pageNo] == null)
+                result.destsByPage[pageNo] = new Array();
+            result.destsByPage[pageNo].push({ itemId: id,
+                                              x: rect.left,
+                                              y: rect.top - pageTop});
+        });
+
+        var links = document.getElementsByTagName("A");
+        for (var i = 0; i < links.length; i++) {
+            var a = links[i];
+
+            if (!a.hasAttribute("href"))
+                continue;
+
+            var offset = DOM_nodeOffset(a);
+            var range = new Range(a.parentNode,offset,a.parentNode,offset+1);
+            var rects = Range_getClientRects(range);
+            for (var rectIndex = 0; rectIndex < rects.length; rectIndex++) {
+                var rect = rects[rectIndex];
+                var pageNo = 1+Math.floor(rect.top/pageHeight);
+                var pageTop = (pageNo-1)*pageHeight;
+
+                if (result.linksByPage[pageNo] == null)
+                    result.linksByPage[pageNo] = new Array();
+                result.linksByPage[pageNo].push({ pageNo: pageNo,
+                                                  left: rect.left,
+                                                  top: rect.top - pageTop,
+                                                  width: rect.width,
+                                                  height: rect.height,
+                                                  href: a.getAttribute("href"), });
+            }
+        }
+
+        recurse(document.body);
+
+        updateStructureReal(pageNumbers);
+        return result;
+
+
+        function recurse(node)
+        {
+            if (node.firstChild == null) {
+                var offset = DOM_nodeOffset(node);
+                var range = new Range(node.parentNode,offset,node.parentNode,offset+1);
+                var rects = Range_getClientRects(range);
+                for (var i = 0; i < rects.length; i++) {
+                    var rect = rects[i];
+
+                    var pageNo = 1+Math.floor(rect.top/pageHeight);
+                    var pageTop = (pageNo-1)*pageHeight;
+
+                    if (result.leafRectsByPage[pageNo] == null)
+                        result.leafRectsByPage[pageNo] = new Array();
+                    result.leafRectsByPage[pageNo].push({ left: rect.left,
+                                                          top: rect.top - pageTop,
+                                                          width: rect.width,
+                                                          height: rect.height });
+                }
+            }
+
+            for (var child = node.firstChild; child != null; child = child.nextSibling)
+                recurse(child);
+        }
+    }
+
+    Outline_setReferenceTarget = function(node,itemId)
+    {
+        Selection_preserveWhileExecuting(function() {
+            refRemoved(node);
+            DOM_setAttribute(node,"href","#"+itemId);
+            refInserted(node);
+        });
+    }
+
+    Outline_detectSectionNumbering = function()
+    {
+        var sectionNumbering = detectNumbering(sections);
+        if (sectionNumbering)
+            makeNumberingExplicit(sections);
+        makeNumberingExplicit(figures);
+        makeNumberingExplicit(tables);
+        return sectionNumbering;
+    }
+
+    function detectNumbering(category)
+    {
+        for (var item = category.list.first; item != null; item = item.next) {
+
+            var firstText = null;
+            var titleNode = OutlineItem_getTitleNode(item);
+
+            if (titleNode != null)
+                firstText = findFirstTextDescendant(titleNode);
+            if (firstText != null) {
+                var regex = category.numberRegex;
+                var str = firstText.nodeValue;
+                if (str.match(category.numberRegex))
+                    return true;
+            }
+        }
+    }
+
+    function makeNumberingExplicit(category)
+    {
+        for (var item = category.list.first; item != null; item = item.next) {
+            var firstText = null;
+            var titleNode = OutlineItem_getTitleNode(item);
+
+            if (titleNode != null)
+                firstText = findFirstTextDescendant(titleNode);
+            if (firstText != null) {
+                var regex = category.numberRegex;
+                var str = firstText.nodeValue;
+                if (str.match(category.numberRegex)) {
+                    var oldValue = str;
+                    var newValue = str.replace(category.numberRegex,"");
+                    DOM_setNodeValue(firstText,newValue);
+                }
+                else {
+                    var titleNode = OutlineItem_getTitleNode(item,true);
+                    if (titleNode != null)
+                        DOM_setAttribute(titleNode,"class","Unnumbered");
+                }
+            }
+        }
+    }
+
+    // Search through the document for any elements corresponding to built-in styles that are
+    // normally latent (i.e. only included in the stylesheet if used)
+    Outline_findUsedStyles = function()
+    {
+        var used = new Object();
+        recurse(document.body);
+        return used;
+
+        function recurse(node)
+        {
+            switch (node._type) {
+            case HTML_NAV: {
+                var className = DOM_getAttribute(node,"class");
+                if ((className == "tableofcontents") ||
+                    (className == "listoffigures") ||
+                    (className == "listoftables")) {
+                    used["nav."+className] = true;
+                }
+                break;
+            }
+            case HTML_FIGCAPTION:
+            case HTML_CAPTION:
+            case HTML_H1:
+            case HTML_H2:
+            case HTML_H3:
+            case HTML_H4:
+            case HTML_H5:
+            case HTML_H6: {
+                var elementName = node.nodeName.toLowerCase();
+                var className = DOM_getAttribute(node,"class");
+                if ((className == null) || (className == ""))
+                    used[elementName] = true;
+                else if (className == "Unnumbered")
+                    used[elementName+".Unnumbered"] = true;
+                break;
+            }
+            }
+
+            for (var child = node.firstChild; child != null; child = child.nextSibling)
+                recurse(child);
+        }
+    }
+
+})();

http://git-wip-us.apache.org/repos/asf/incubator-corinthia/blob/03bd5af0/Editor/src/Position.js
----------------------------------------------------------------------
diff --git a/Editor/src/Position.js b/Editor/src/Position.js
new file mode 100644
index 0000000..6af3c7b
--- /dev/null
+++ b/Editor/src/Position.js
@@ -0,0 +1,1027 @@
+// 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 Position;
+var Position_assertValid;
+var Position_prev;
+var Position_next;
+var Position_trackWhileExecuting;
+var Position_closestActualNode;
+var Position_okForInsertion;
+var Position_okForMovement;
+var Position_prevMatch;
+var Position_nextMatch;
+var Position_closestMatchForwards;
+var Position_closestMatchBackwards;
+var Position_track;
+var Position_untrack;
+var Position_rectAtPos;
+var Position_displayRectAtPos;
+var Position_preferTextPosition;
+var Position_preferElementPosition;
+var Position_compare;
+var Position_atPoint;
+
+(function() {
+
+    // public
+    Position = function(node,offset)
+    {
+        if (node == document.documentElement)
+            throw new Error("node is root element");
+        Object.defineProperty(this,"self",{value: {}});
+        var self = this.self;
+        self.this = this;
+        self.node = node;
+        self.offset = offset;
+        self.origOffset = offset;
+        self.tracking = 0;
+        this.posId = null;
+        this.targetX = null;
+
+        Object.defineProperty(this,"node",{
+            get: function() { return this.self.node },
+            set: setNode,
+            enumerable: true });
+        Object.defineProperty(this,"offset",{
+            get: function() { return this.self.offset },
+            set: function(value) { this.self.offset = value },
+            enumerable: true});
+        Object.defineProperty(this,"origOffset",{
+            get: function() { return this.self.origOffset },
+            set: function(value) { this.self.origOffset = value },
+            enumerable: true});
+
+        Object.preventExtensions(this);
+    }
+
+    function actuallyStartTracking(self)
+    {
+        DOM_addTrackedPosition(self.this);
+    }
+
+    function actuallyStopTracking(self)
+    {
+        DOM_removeTrackedPosition(self.this);
+    }
+
+    function startTracking(self)
+    {
+        if (self.tracking == 0)
+            actuallyStartTracking(self);
+        self.tracking++;
+    }
+
+    function stopTracking(self)
+    {
+        self.tracking--;
+        if (self.tracking == 0)
+            actuallyStopTracking(self);
+    }
+
+    function setNode(node)
+    {
+        var self = this.self;
+        if (self.tracking > 0)
+            actuallyStopTracking(self);
+
+        self.node = node;
+
+        if (self.tracking > 0)
+            actuallyStartTracking(self);
+    }
+
+    function setNodeAndOffset(self,node,offset)
+    {
+        self.this.node = node;
+        self.this.offset = offset;
+    }
+
+    // public
+    Position.prototype.toString = function()
+    {
+        var self = this.self;
+        var result;
+        if (self.node.nodeType == Node.TEXT_NODE) {
+            var extra = "";
+            if (self.offset > self.node.nodeValue.length) {
+                for (var i = self.node.nodeValue.length; i < self.offset; i++)
+                    extra += "!";
+            }
+            var id = "";
+            if (window.debugIds)
+                id = self.node._nodeId+":";
+            result = id+JSON.stringify(self.node.nodeValue.slice(0,self.offset)+extra+"|"+
+                                       self.node.nodeValue.slice(self.offset));
+        }
+        else {
+            result = "("+nodeString(self.node)+","+self.offset+")";
+        }
+        if (this.posId != null)
+            result = "["+this.posId+"]"+result;
+        return result;
+    }
+
+    function positionSpecial(pos,forwards,backwards)
+    {
+        var node = pos.node;
+        var offset = pos.offset;
+
+        var prev = node.childNodes[offset-1];
+        var next = node.childNodes[offset];
+
+        // Moving left from the start of a caption - go to the end of the table
+        if ((node._type == HTML_CAPTION) && backwards && (prev == null))
+            return new Position(node.parentNode,node.parentNode.childNodes.length);
+
+        // Moving right from the end of a caption - go after the table
+        if ((node._type == HTML_CAPTION) && forwards && (next == null))
+            return new Position(node.parentNode.parentNode,DOM_nodeOffset(node.parentNode)+1);
+
+        // Moving left from just after a table - go to the end of the caption (if there is one)
+        if ((prev != null) && (prev._type == HTML_TABLE) && backwards) {
+            var firstChild = firstChildElement(prev);
+            if ((firstChild._type == HTML_CAPTION))
+                return new Position(firstChild,firstChild.childNodes.length);
+        }
+
+        // Moving right from just before a table - bypass the the caption (if there is one)
+        if ((next != null) && (next._type == HTML_TABLE) && forwards) {
+            var firstChild = firstChildElement(next);
+            if (firstChild._type == HTML_CAPTION)
+                return new Position(next,DOM_nodeOffset(firstChild)+1);
+        }
+
+        // Moving right from the end of a table - go to the start of the caption (if there is one)
+        if ((node._type == HTML_TABLE) && (next == null) && forwards) {
+            var firstChild = firstChildElement(node);
+            if (firstChild._type == HTML_CAPTION)
+                return new Position(firstChild,0);
+        }
+
+        // Moving left just after a caption node - skip the caption
+        if ((prev != null) && (prev._type == HTML_CAPTION) && backwards)
+            return new Position(node,offset-1);
+
+        return null;
+    }
+
+    // public
+    Position_assertValid = function(pos,description)
+    {
+        if (description == null)
+            description = "Position";
+
+        for (var ancestor = pos.node; ancestor != document.body; ancestor = ancestor.parentNode) {
+            if (ancestor == null)
+                throw new Error(description+" node "+pos.node.nodeName+" is not in tree");
+        }
+
+        var max;
+        if (pos.node.nodeType == Node.ELEMENT_NODE)
+            max = pos.node.childNodes.length;
+        else if (pos.node.nodeType == Node.TEXT_NODE)
+            max = pos.node.nodeValue.length;
+        else
+            throw new Error(description+" has invalid node type "+pos.node.nodeType);
+
+        if ((pos.offset < 0) || (pos.offset > max)) {
+            throw new Error(description+" (in "+pos.node.nodeName+") has invalid offset "+
+                            pos.offset+" (max allowed is "+max+")");
+        }
+    }
+
+    // public
+    Position_prev = function(pos)
+    {
+        if (pos.node.nodeType == Node.ELEMENT_NODE) {
+            var r = positionSpecial(pos,false,true);
+            if (r != null)
+                return r;
+            if (pos.offset == 0) {
+                return upAndBack(pos);
+            }
+            else {
+                var child = pos.node.childNodes[pos.offset-1];
+                return new Position(child,DOM_maxChildOffset(child));
+            }
+        }
+        else if (pos.node.nodeType == Node.TEXT_NODE) {
+            if (pos.offset > 0)
+                return new Position(pos.node,pos.offset-1);
+            else
+                return upAndBack(pos);
+        }
+        else {
+            return null;
+        }
+
+        function upAndBack(pos)
+        {
+            if (pos.node == pos.node.ownerDocument.body)
+                return null;
+            else
+                return new Position(pos.node.parentNode,DOM_nodeOffset(pos.node));
+        }
+    }
+
+    // public
+    Position_next = function(pos)
+    {
+        if (pos.node.nodeType == Node.ELEMENT_NODE) {
+            var r = positionSpecial(pos,true,false);
+            if (r != null)
+                return r;
+            if (pos.offset == pos.node.childNodes.length)
+                return upAndForwards(pos);
+            else
+                return new Position(pos.node.childNodes[pos.offset],0);
+        }
+        else if (pos.node.nodeType == Node.TEXT_NODE) {
+            if (pos.offset < pos.node.nodeValue.length)
+                return new Position(pos.node,pos.offset+1);
+            else
+                return upAndForwards(pos);
+        }
+        else {
+            return null;
+        }
+
+        function upAndForwards(pos)
+        {
+            if (pos.node == pos.node.ownerDocument.body)
+                return null;
+            else
+                return new Position(pos.node.parentNode,DOM_nodeOffset(pos.node)+1);
+        }
+    }
+
+    // public
+    Position_trackWhileExecuting = function(positions,fun)
+    {
+        for (var i = 0; i < positions.length; i++)
+            startTracking(positions[i].self);
+        try {
+            return fun();
+        }
+        finally {
+            for (var i = 0; i < positions.length; i++)
+                stopTracking(positions[i].self);
+        }
+    }
+
+    // public
+    Position_closestActualNode = function(pos,preferElement)
+    {
+        var node = pos.node;
+        var offset = pos.offset;
+        if ((node.nodeType != Node.ELEMENT_NODE) || (node.firstChild == null))
+            return node;
+        else if (offset == 0)
+            return node.firstChild;
+        else if (offset >= node.childNodes.length)
+            return node.lastChild;
+
+        var prev = node.childNodes[offset-1];
+        var next = node.childNodes[offset];
+        if (preferElement &&
+            (next.nodeType != Node.ELEMENT_NODE) &&
+            (prev.nodeType == Node.ELEMENT_NODE)) {
+            return prev;
+        }
+        else {
+            return next;
+        }
+    }
+
+    // public
+    Position_okForInsertion = function(pos)
+    {
+        return Position_okForMovement(pos,true);
+    }
+
+    function nodeCausesLineBreak(node)
+    {
+        return ((node._type == HTML_BR) || !isInlineNode(node));
+    }
+
+    function spacesUntilNextContent(node)
+    {
+        var spaces = 0;
+        while (true) {
+            if (node.firstChild) {
+                node = node.firstChild;
+            }
+            else if (node.nextSibling) {
+                node = node.nextSibling;
+            }
+            else {
+                while ((node.parentNode != null) && (node.parentNode.nextSibling == null)) {
+                    node = node.parentNode;
+                    if (nodeCausesLineBreak(node))
+                        return null;
+                }
+                if (node.parentNode == null)
+                    node = null;
+                else
+                    node = node.parentNode.nextSibling;
+            }
+
+            if ((node == null) || nodeCausesLineBreak(node))
+                return null;
+            if (isOpaqueNode(node))
+                return spaces;
+            if (node.nodeType == Node.TEXT_NODE) {
+                if (isWhitespaceTextNode(node)) {
+                    spaces += node.nodeValue.length;
+                }
+                else {
+                    var matches = node.nodeValue.match(/^\s+/);
+                    if (matches == null)
+                        return spaces;
+                    spaces += matches[0].length;
+                    return spaces;
+                }
+            }
+        }
+    }
+
+    // public
+    Position_okForMovement = function(pos,insertion)
+    {
+        var node = pos.node;
+        var offset = pos.offset;
+        var type = node._type;
+
+        if (isOpaqueNode(node))
+            return false;
+
+        for (var ancestor = node; ancestor != null; ancestor = ancestor.parentNode) {
+            var ancestorType = node._type;
+            if (ancestorType == HTML_FIGCAPTION)
+                break;
+            else if (ancestorType == HTML_FIGURE)
+                return false;
+        }
+
+        if (node.nodeType == Node.TEXT_NODE) {
+            var value = node.nodeValue;
+
+            // If there are multiple adjacent text nodes, consider them as one (adjusting the
+            // offset appropriately)
+
+            var firstNode = node;
+            var lastNode = node;
+
+            while ((firstNode.previousSibling != null) &&
+                   (firstNode.previousSibling.nodeType == Node.TEXT_NODE)) {
+                firstNode = firstNode.previousSibling;
+                value = firstNode.nodeValue + value;
+                offset += firstNode.nodeValue.length;
+            }
+
+            while ((lastNode.nextSibling != null) &&
+                   (lastNode.nextSibling.nodeType == Node.TEXT_NODE)) {
+                lastNode = lastNode.nextSibling;
+                value += lastNode.nodeValue;
+            }
+
+            var prevChar = value.charAt(offset-1);
+            var nextChar = value.charAt(offset);
+            var havePrevChar = ((prevChar != null) && !isWhitespaceString(prevChar));
+            var haveNextChar = ((nextChar != null) && !isWhitespaceString(nextChar));
+            if (havePrevChar && haveNextChar) {
+                var prevCode = value.charCodeAt(offset-1);
+                var nextCode = value.charCodeAt(offset);
+                if ((prevCode >= 0xD800) && (prevCode <= 0xDBFF) &&
+                    (nextCode >= 0xDC00) && (nextCode <= 0xDFFF)) {
+                    return false; // In middle of surrogate pair
+                }
+                return true;
+            }
+
+            if (isWhitespaceString(value)) {
+                if (offset == 0) {
+                    if ((node == firstNode) &&
+                        (firstNode.previousSibling == null) && (lastNode.nextSibling == null))
+                        return true;
+                    if ((node.nextSibling != null) && (node.nextSibling._type == HTML_BR))
+                        return true;
+                    if ((node.firstChild == null) &&
+                        (node.previousSibling == null) &&
+                        (node.nextSibling == null)) {
+                        return true;
+                    }
+                    if (insertion && (node.previousSibling != null) &&
+                        isInlineNode(node.previousSibling) &&
+                        !isOpaqueNode(node.previousSibling) &&
+                        (node.previousSibling._type != HTML_BR))
+                        return true;
+                }
+                return false;
+            }
+
+            if (insertion)
+                return true;
+
+            var precedingText = value.substring(0,offset);
+            if (isWhitespaceString(precedingText)) {
+                return (haveNextChar &&
+                        ((node.previousSibling == null) ||
+                         (node.previousSibling._type == HTML_BR) ||
+                         (isParagraphNode(node.previousSibling)) ||
+                         (getNodeText(node.previousSibling).match(/\s$/)) ||
+                         isItemNumber(node.previousSibling) ||
+                         ((precedingText.length > 0))));
+            }
+
+            var followingText = value.substring(offset);
+            if (isWhitespaceString(followingText)) {
+                return (havePrevChar &&
+                        ((node.nextSibling == null) ||
+                         (followingText.length > 0) ||
+                         (spacesUntilNextContent(node) != 0)));
+            }
+
+            return (havePrevChar || haveNextChar);
+        }
+        else if (node.nodeType == Node.ELEMENT_NODE) {
+            if (node.firstChild == null) {
+                switch (type) {
+                case HTML_LI:
+                case HTML_TH:
+                case HTML_TD:
+                    return true;
+                default:
+                    if (PARAGRAPH_ELEMENTS[type])
+                        return true;
+                    else
+                        break;
+                }
+            }
+
+            var prevNode = node.childNodes[offset-1];
+            var nextNode = node.childNodes[offset];
+            var prevType = (prevNode != null) ? prevNode._type : 0;
+            var nextType = (nextNode != null) ? nextNode._type : 0;
+
+            if ((prevNode == null) && (nextNode == null) &&
+                (CONTAINERS_ALLOWING_CHILDREN[type] ||
+                (isInlineNode(node) && !isOpaqueNode(node) && (type != HTML_BR))))
+                return true;
+
+            if ((prevNode != null) && isSpecialBlockNode(prevNode))
+                return true;
+            if ((nextNode != null) && isSpecialBlockNode(nextNode))
+                return true;
+
+            if ((nextNode != null) && isItemNumber(nextNode))
+                return false;
+            if ((prevNode != null) && isItemNumber(prevNode))
+                return ((nextNode == null) || isWhitespaceTextNode(nextNode));
+
+            if ((nextNode != null) && (nextType == HTML_BR))
+                return ((prevType == 0) || (prevType != HTML_TEXT));
+
+            if ((prevNode != null) && (isOpaqueNode(prevNode) || (prevType == HTML_TABLE))) {
+
+                switch (nextType) {
+                case 0:
+                case HTML_TEXT:
+                case HTML_TABLE:
+                    return true;
+                default:
+                    return isOpaqueNode(nextNode);
+                }
+            }
+            if ((nextNode != null) && (isOpaqueNode(nextNode) || (nextType == HTML_TABLE))) {
+                switch (prevType) {
+                case 0:
+                case HTML_TEXT:
+                case HTML_TABLE:
+                    return true;
+                default:
+                    return isOpaqueNode(prevNode);
+                }
+            }
+        }
+
+        return false;
+    }
+
+    Position_prevMatch = function(pos,fun)
+    {
+        do {
+            pos = Position_prev(pos);
+        } while ((pos != null) && !fun(pos));
+        return pos;
+    }
+
+    Position_nextMatch = function(pos,fun)
+    {
+        do {
+            pos = Position_next(pos);
+        } while ((pos != null) && !fun(pos));
+        return pos;
+    }
+
+    function findEquivalentValidPosition(pos,fun)
+    {
+        var node = pos.node;
+        var offset = pos.offset;
+        if (node.nodeType == Node.ELEMENT_NODE) {
+            var before = node.childNodes[offset-1];
+            var after = node.childNodes[offset];
+            if ((before != null) && (before.nodeType == Node.TEXT_NODE)) {
+                var candidate = new Position(before,before.nodeValue.length);
+                if (fun(candidate))
+                    return candidate;
+            }
+            if ((after != null) && (after.nodeType == Node.TEXT_NODE)) {
+                var candidate = new Position(after,0);
+                if (fun(candidate))
+                    return candidate;
+            }
+        }
+
+        if ((pos.node.nodeType == Node.TEXT_NODE) &&
+            isWhitespaceString(pos.node.nodeValue.slice(pos.offset))) {
+            var str = pos.node.nodeValue;
+            var whitespace = str.match(/\s+$/);
+            if (whitespace) {
+                var adjusted = new Position(pos.node,
+                                            str.length - whitespace[0].length + 1);
+                return adjusted;
+            }
+        }
+        return pos;
+    }
+
+    // public
+    Position_closestMatchForwards = function(pos,fun)
+    {
+        if (pos == null)
+            return null;
+
+        if (!fun(pos))
+            pos = findEquivalentValidPosition(pos,fun);
+
+        if (fun(pos))
+            return pos;
+
+        var next = Position_nextMatch(pos,fun);
+        if (next != null)
+            return next;
+
+        var prev = Position_prevMatch(pos,fun);
+        if (prev != null)
+            return prev;
+
+        return new Position(document.body,document.body.childNodes.length);
+    }
+
+    // public
+    Position_closestMatchBackwards = function(pos,fun)
+    {
+        if (pos == null)
+            return null;
+
+        if (!fun(pos))
+            pos = findEquivalentValidPosition(pos,fun);
+
+        if (fun(pos))
+            return pos;
+
+        var prev = Position_prevMatch(pos,fun);
+        if (prev != null)
+            return prev;
+
+        var next = Position_nextMatch(pos,fun);
+        if (next != null)
+            return next;
+
+        return new Position(document.body,0);
+    }
+
+    Position_track = function(pos)
+    {
+        startTracking(pos.self);
+    }
+
+    Position_untrack = function(pos)
+    {
+        stopTracking(pos.self);
+    }
+
+    Position_rectAtPos = function(pos)
+    {
+        if (pos == null)
+            return null;
+        var range = new Range(pos.node,pos.offset,pos.node,pos.offset);
+        var rects = Range_getClientRects(range);
+
+        if ((rects.length > 0) && !rectIsEmpty(rects[0])) {
+            return rects[0];
+        }
+
+        if (isParagraphNode(pos.node) && (pos.offset == 0)) {
+            var rect = pos.node.getBoundingClientRect();
+            if (!rectIsEmpty(rect))
+                return rect;
+        }
+
+        return null;
+    }
+
+    function posAtStartOfParagraph(pos,paragraph)
+    {
+        return ((pos.node == paragraph.node) &&
+                (pos.offset == paragraph.startOffset));
+    }
+
+    function posAtEndOfParagraph(pos,paragraph)
+    {
+        return ((pos.node == paragraph.node) &&
+                (pos.offset == paragraph.endOffset));
+    }
+
+    function zeroWidthRightRect(rect)
+    {
+        return { left: rect.right, // 0 width
+                 right: rect.right,
+                 top: rect.top,
+                 bottom: rect.bottom,
+                 width: 0,
+                 height: rect.height };
+    }
+
+    function zeroWidthLeftRect(rect)
+    {
+        return { left: rect.left,
+                 right: rect.left, // 0 width
+                 top: rect.top,
+                 bottom: rect.bottom,
+                 width: 0,
+                 height: rect.height };
+    }
+
+    function exactRectAtPos(pos)
+    {
+        var node = pos.node;
+        var offset = pos.offset;
+
+        if (node.nodeType == Node.ELEMENT_NODE) {
+            if (offset > node.childNodes.length)
+                throw new Error("Invalid offset: "+offset+" of "+node.childNodes.length);
+
+            var before = node.childNodes[offset-1];
+            var after = node.childNodes[offset];
+
+            // Cursor is immediately before table -> return table rect
+            if ((before != null) && isSpecialBlockNode(before))
+                return zeroWidthRightRect(before.getBoundingClientRect());
+
+            // Cursor is immediately after table -> return table rect
+            else if ((after != null) && isSpecialBlockNode(after))
+                return zeroWidthLeftRect(after.getBoundingClientRect());
+
+            // Start of empty paragraph
+            if ((node.nodeType == Node.ELEMENT_NODE) && (offset == 0) &&
+                isParagraphNode(node) && !nodeHasContent(node)) {
+                return zeroWidthLeftRect(node.getBoundingClientRect());
+            }
+
+            return null;
+        }
+        else if (node.nodeType == Node.TEXT_NODE) {
+            // First see if the client rects returned by the range gives us a valid value. This
+            // won't be the case if the cursor is surrounded by both sides on whitespace.
+            var result = rectAtRightOfRange(new Range(node,offset,node,offset));
+            if (result != null)
+                return result;
+
+            if (offset > 0) {
+                // Try and get the rect of the previous character; the cursor goes after that
+                var result = rectAtRightOfRange(new Range(node,offset-1,node,offset));
+                if (result != null)
+                    return result;
+            }
+
+            return null;
+        }
+        else {
+            return null;
+        }
+
+        function rectAtRightOfRange(range)
+        {
+            var rects = Range_getClientRects(range);
+            if ((rects == null) || (rects.length == 0) || (rects[rects.length-1].height == 0))
+                return null;
+            return zeroWidthRightRect(rects[rects.length-1]);
+        }
+    }
+
+    Position_displayRectAtPos = function(pos)
+    {
+        rect = exactRectAtPos(pos);
+        if (rect != null)
+            return rect;
+
+        var paragraph = Text_findParagraphBoundaries(pos);
+
+        var backRect = null;
+        for (var backPos = pos; backPos != null; backPos = Position_prev(backPos)) {
+            backRect = exactRectAtPos(backPos);
+            if ((backRect != null) || posAtStartOfParagraph(backPos,paragraph))
+                break;
+        }
+
+        var forwardRect = null;
+        for (var forwardPos = pos; forwardPos != null; forwardPos = Position_next(forwardPos)) {
+            forwardRect = exactRectAtPos(forwardPos);
+            if ((forwardRect != null) || posAtEndOfParagraph(forwardPos,paragraph))
+                break;
+        }
+
+        if (backRect != null) {
+            return backRect;
+        }
+        else if (forwardRect != null) {
+            return forwardRect;
+        }
+        else {
+            // Fallback, e.g. for empty LI elements
+            var node = pos.node;
+            if (node.nodeType == Node.TEXT_NODE)
+                node = node.parentNode;
+            return zeroWidthLeftRect(node.getBoundingClientRect());
+        }
+    }
+
+    Position_equal = function(a,b)
+    {
+        if ((a == null) && (b == null))
+            return true;
+        if ((a != null) && (b != null) &&
+            (a.node == b.node) && (a.offset == b.offset))
+            return true;
+        return false;
+    }
+
+    Position_preferTextPosition = function(pos)
+    {
+        var node = pos.node;
+        var offset = pos.offset;
+        if (node.nodeType == Node.ELEMENT_NODE) {
+            var before = node.childNodes[offset-1];
+            var after = node.childNodes[offset];
+            if ((before != null) && (before.nodeType == Node.TEXT_NODE))
+                return new Position(before,before.nodeValue.length);
+            if ((after != null) && (after.nodeType == Node.TEXT_NODE))
+                return new Position(after,0);
+        }
+        return pos;
+    }
+
+    Position_preferElementPosition = function(pos)
+    {
+        if (pos.node.nodeType == Node.TEXT_NODE) {
+            if (pos.node.parentNode == null)
+                throw new Error("Position "+pos+" has no parent node");
+            if (pos.offset == 0)
+                return new Position(pos.node.parentNode,DOM_nodeOffset(pos.node));
+            if (pos.offset == pos.node.nodeValue.length)
+                return new Position(pos.node.parentNode,DOM_nodeOffset(pos.node)+1);
+        }
+        return pos;
+    }
+
+    Position_compare = function(first,second)
+    {
+        if ((first.node == second.node) && (first.offset == second.offset))
+            return 0;
+
+        var doc = first.node.ownerDocument;
+        if ((first.node.parentNode == null) && (first.node != doc.documentElement))
+            throw new Error("First node has been removed from document");
+        if ((second.node.parentNode == null) && (second.node != doc.documentElement))
+            throw new Error("Second node has been removed from document");
+
+        if (first.node == second.node)
+            return first.offset - second.offset;
+
+        var firstParent = null;
+        var firstChild = null;
+        var secondParent = null;
+        var secondChild = null;
+
+        if (second.node.nodeType == Node.ELEMENT_NODE) {
+            secondParent = second.node;
+            secondChild = second.node.childNodes[second.offset];
+        }
+        else {
+            secondParent = second.node.parentNode;
+            secondChild = second.node;
+        }
+
+        if (first.node.nodeType == Node.ELEMENT_NODE) {
+            firstParent = first.node;
+            firstChild = first.node.childNodes[first.offset];
+        }
+        else {
+            firstParent = first.node.parentNode;
+            firstChild = first.node;
+            if (firstChild == secondChild)
+                return 1;
+        }
+
+        var firstC = firstChild;
+        var firstP = firstParent;
+        while (firstP != null) {
+
+            var secondC = secondChild;
+            var secondP = secondParent;
+            while (secondP != null) {
+
+                if (firstP == secondC)
+                    return 1;
+
+                if (firstP == secondP) {
+                    // if secondC is last child, firstC must be secondC or come before it
+                    if (secondC == null)
+                        return -1;
+                    for (var n = firstC; n != null; n = n.nextSibling) {
+                        if (n == secondC)
+                            return -1;
+                    }
+                    return 1;
+                }
+
+                secondC = secondP;
+                secondP = secondP.parentNode;
+            }
+
+            firstC = firstP;
+            firstP = firstP.parentNode;
+        }
+        throw new Error("Could not find common ancestor");
+    }
+
+    // This function works around a bug in WebKit where caretRangeFromPoint sometimes returns an
+    // incorrect node (the last text node in the document). In a previous attempt to fix this bug,
+    // we first checked if the point was in the elements bounding rect, but this meant that it
+    // wasn't possible to place the cursor at the nearest node, if the click location was not
+    // exactly on a node.
+
+    // Now we instead check to see if the result of elementFromPoint is the same as the parent node
+    // of the text node returned by caretRangeFromPoint. If it isn't, then we assume that the latter
+    // result is incorrect, and return null.
+
+    // In the circumstances where this bug was observed, the last text node in the document was
+    // being returned from caretRangeFromPoint in some cases. In the typical case, this is going to
+    // be inside a paragraph node, but elementNodeFromPoint was returning the body element. The
+    // check we do now comparing the results of the two functions fixes this case, but won't work as
+    // intended if the document's last text node is a direct child of the body (as it may be in some
+    // HTML documents that users open).
+
+    Position_atPoint = function(x,y)
+    {
+        // In general, we can use document.caretRangeFromPoint(x,y) to determine the location of the
+        // cursor based on screen coordinates. However, this doesn't work if the screen coordinates
+        // are outside the bounding box of the document's body. So when this is true, we find either
+        // the first or last non-whitespace text node, calculate a y value that is half-way between
+        // the top and bottom of its first or last rect (respectively), and then make a call to
+        // caretRangeFromPoint with the same x value but this new y value. This results in the
+        // cursor being placed on the first or last line when the user taps outside the document
+        // bounds.
+
+        var bodyRect = document.body.getBoundingClientRect();
+        var boundaryRect = null;
+        if (y <= bodyRect.top)
+            boundaryRect = findFirstTextRect();
+        else if (y >= bodyRect.bottom)
+            boundaryRect = findLastTextRect();
+
+        if (boundaryRect != null) {
+            var boundaryY = boundaryRect.top + boundaryRect.height/2;
+            var range = document.caretRangeFromPoint(x,boundaryY);
+            if (range != null)
+                return new Position(range.startContainer,range.startOffset);
+        }
+
+        // We get here if the coordinates are inside the document's bounding rect, or if getting the
+        // position from the first or last rect failed for some reason.
+
+        var range = document.caretRangeFromPoint(x,y);
+        if (range == null)
+            return null;
+
+        var pos = new Position(range.startContainer,range.startOffset);
+
+        if (pos.node.nodeType == Node.ELEMENT_NODE) {
+            var prev = pos.node.childNodes[pos.offset-1];
+            var next = pos.node.childNodes[pos.offset];
+
+            if ((prev != null) && (prev._type == HTML_IMG) && elementContainsPoint(prev,x,y))
+                return new Position(prev,0);
+
+            if ((next != null) && (next._type == HTML_IMG) && elementContainsPoint(next,x,y))
+                return new Position(next,0);
+        }
+
+        pos = adjustPositionForFigure(pos);
+
+        return pos;
+    }
+
+    function elementContainsPoint(element,x,y)
+    {
+        var rect = element.getBoundingClientRect();
+        return ((x >= rect.left) && (x <= rect.right) &&
+                (y >= rect.top) && (y <= rect.bottom));
+    }
+
+    function isEmptyParagraphNode(node)
+    {
+        return ((node._type == HTML_P) &&
+                (node.lastChild != null) &&
+                (node.lastChild._type == HTML_BR) &&
+                !nodeHasContent(node));
+    }
+
+    function findLastTextRect()
+    {
+        var node = lastDescendant(document.body);
+
+        while ((node != null) &&
+               ((node.nodeType != Node.TEXT_NODE) || isWhitespaceTextNode(node))) {
+            if (isEmptyParagraphNode(node))
+                return node.getBoundingClientRect();
+            node = prevNode(node);
+        }
+
+        if (node != null) {
+            var domRange = document.createRange();
+            domRange.setStart(node,0);
+            domRange.setEnd(node,node.nodeValue.length);
+            var rects = domRange.getClientRects();
+            if ((rects != null) && (rects.length > 0))
+                return rects[rects.length-1];
+        }
+        return null;
+    }
+
+    function findFirstTextRect()
+    {
+        var node = firstDescendant(document.body);
+
+        while ((node != null) &&
+               ((node.nodeType != Node.TEXT_NODE) || isWhitespaceTextNode(node))) {
+            if (isEmptyParagraphNode(node))
+                return node.getBoundingClientRect();
+            node = nextNode(node);
+        }
+
+        if (node != null) {
+            var domRange = document.createRange();
+            domRange.setStart(node,0);
+            domRange.setEnd(node,node.nodeValue.length);
+            var rects = domRange.getClientRects();
+            if ((rects != null) && (rects.length > 0))
+                return rects[0];
+        }
+        return null;
+    }
+
+    function adjustPositionForFigure(position)
+    {
+        if (position == null)
+            return null;
+        if (position.node._type == HTML_FIGURE) {
+            var prev = position.node.childNodes[position.offset-1];
+            var next = position.node.childNodes[position.offset];
+            if ((prev != null) && (prev._type == HTML_IMG)) {
+                position = new Position(position.node.parentNode,
+                                        DOM_nodeOffset(position.node)+1);
+            }
+            else if ((next != null) && (next._type == HTML_IMG)) {
+                position = new Position(position.node.parentNode,
+                                        DOM_nodeOffset(position.node));
+            }
+        }
+        return position;
+    }
+
+})();

http://git-wip-us.apache.org/repos/asf/incubator-corinthia/blob/03bd5af0/Editor/src/PostponedActions.js
----------------------------------------------------------------------
diff --git a/Editor/src/PostponedActions.js b/Editor/src/PostponedActions.js
new file mode 100644
index 0000000..b63923f
--- /dev/null
+++ b/Editor/src/PostponedActions.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 PostponedActions_add;
+var PostponedActions_perform;
+
+(function() {
+
+    function PostponedAction(fun,undoDisabled)
+    {
+        this.fun = fun;
+        this.undoDisabled = undoDisabled;
+    }
+
+    var actions = new Array();
+
+    PostponedActions_add = function(action)
+    {
+        actions.push(new PostponedAction(action,UndoManager_isDisabled()));
+    }
+
+    PostponedActions_perform = function()
+    {
+        var count = 0;
+        while (actions.length > 0) {
+            if (count >= 10)
+                throw new Error("Too many postponed actions");
+            var actionsToPerform = actions;
+            actions = new Array();
+            for (var i = 0; i < actionsToPerform.length; i++) {
+                var action = actionsToPerform[i];
+                if (action.undoDisabled)
+                    UndoManager_disableWhileExecuting(action.fun);
+                else
+                    action.fun();
+            }
+            count++;
+        }
+    }
+
+})();


Mime
View raw message