blob: 51bc5d3d0a657042e8624b6cb513eed4d57d9a9a [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 Formatting_splitTextBefore;
var Formatting_splitTextAfter;
var Formatting_movePreceding;
var Formatting_moveFollowing;
var Formatting_splitAroundSelection;
var Formatting_mergeUpwards;
var Formatting_mergeWithNeighbours;
var Formatting_paragraphTextUpToPosition;
var Formatting_getAllNodeProperties;
var Formatting_getFormatting;
var Formatting_pushDownInlineProperties;
var Formatting_applyFormattingChanges;
var Formatting_formatInlineNode;
var Formatting_MERGEABLE_INLINE;
var Formatting_MERGEABLE_BLOCK;
var Formatting_MERGEABLE_BLOCK_AND_INLINE;
(function() {
// Some properties in CSS, such as 'margin', 'border', and 'padding', are shorthands which
// set multiple, more fine-grained properties. The CSS spec outlines what these are - e.g.
// an assignment to the 'margin' property is considered a simultaneous assignment to
// 'margin-left', 'margin-right', 'margin-top', and 'margin-bottom' properties.
// However, Firefox contains a bug (https://bugzilla.mozilla.org/show_bug.cgi?id=241234),
// which has gone unfixed for more than six years, whereby it actually sets different
// properties for *-left and *-right, which are reflected when examining the style property
// of an element. Additionally, it also gives an error if you try to set these, so if you simply
// get all the style properties and try to set them again it won't work.
// To get around this problem, we record the following set of replacements. When getting the
// style properties of an element, we replace any properties with the names given below with
// their corresponding spec name. A null entry means that property should be ignored altogether.
// You should always use getStyleProperties() instead of accessing element.style directly.
var CSS_PROPERTY_REPLACEMENTS = {
"margin-left-value": "margin-left",
"margin-left-ltr-source": null,
"margin-left-rtl-source": null,
"margin-right-value": "margin-right",
"margin-right-ltr-source": null,
"margin-right-rtl-source": null,
"padding-left-value": "padding-left",
"padding-left-ltr-source": null,
"padding-left-rtl-source": null,
"padding-right-value": "padding-right",
"padding-right-ltr-source": null,
"padding-right-rtl-source": null,
"border-right-width-value": "border-right-width",
"border-right-width-ltr-source": null,
"border-right-width-rtl-source": null,
"border-left-width-value": "border-left-width",
"border-left-width-ltr-source": null,
"border-left-width-rtl-source": null,
"border-right-color-value": "border-right-color",
"border-right-color-ltr-source": null,
"border-right-color-rtl-source": null,
"border-left-color-value": "border-left-color",
"border-left-color-ltr-source": null,
"border-left-color-rtl-source": null,
"border-right-style-value": "border-right-style",
"border-right-style-ltr-source": null,
"border-right-style-rtl-source": null,
"border-left-style-value": "border-left-style",
"border-left-style-ltr-source": null,
"border-left-style-rtl-source": null,
};
// private
function getStyleProperties(element,dontReplace)
{
var properties = new Object();
for (var i = 0; i < element.style.length; i++) {
var name = element.style[i];
var value = element.style.getPropertyValue(name);
var replacement;
if (dontReplace) {
replacement = name;
}
else {
replacement = CSS_PROPERTY_REPLACEMENTS[name];
if (typeof(replacement) == "undefined")
replacement = name;
}
if (replacement != null)
properties[replacement] = value;
}
return properties;
}
// public (for testing purposes only)
Formatting_splitAroundSelection = function(range,allowDirectInline)
{
Range_trackWhileExecuting(range,function() {
if (!allowDirectInline)
Range_ensureInlineNodesInParagraph(range);
Range_ensureValidHierarchy(range);
if ((range.start.node.nodeType == Node.TEXT_NODE) &&
(range.start.offset > 0)) {
Formatting_splitTextBefore(range.start);
if (range.end.node == range.start.node)
range.end.offset -= range.start.offset;
range.start.offset = 0;
}
else if (range.start.node.nodeType == Node.ELEMENT_NODE) {
Formatting_movePreceding(range.start,isBlockOrNoteNode);
}
else {
Formatting_movePreceding(new Position(range.start.node.parentNode,
DOM_nodeOffset(range.start.node)),
isBlockOrNoteNode);
}
// Save the start and end position of the range. The mutation listeners will move it
// when the following node is moved, which we don't actually want in this case.
var startNode = range.start.node;
var startOffset = range.start.offset;
var endNode = range.end.node;
var endOffset = range.end.offset;
if ((range.end.node.nodeType == Node.TEXT_NODE) &&
(range.end.offset < range.end.node.nodeValue.length)) {
Formatting_splitTextAfter(range.end);
}
else if (range.end.node.nodeType == Node.ELEMENT_NODE) {
Formatting_moveFollowing(range.end,isBlockOrNoteNode);
}
else {
Formatting_moveFollowing(new Position(range.end.node.parentNode,
DOM_nodeOffset(range.end.node)+1),
isBlockOrNoteNode);
}
range.start.node = startNode;
range.start.offset = startOffset;
range.end.node = endNode;
range.end.offset = endOffset;
});
}
// public
Formatting_mergeUpwards = function(node,whiteList)
{
while ((node != null) && whiteList[node._type]) {
var parent = node.parentNode;
Formatting_mergeWithNeighbours(node,whiteList,true);
node = parent;
}
}
function isDiscardable(node)
{
if (node.nodeType != Node.ELEMENT_NODE)
return false;
if (!isInlineNode(node))
return false;
if (isOpaqueNode(node))
return false;
for (var child = node.firstChild; child != null; child = child.nextSibling) {
if (!isDiscardable(child))
return false;
}
return true;
}
// public (for use by tests)
Formatting_mergeWithNeighbours = function(node,whiteList,trim)
{
var parent = node.parentNode;
if (parent == null)
return;
var start = node;
var end = node;
while ((start.previousSibling != null) &&
DOM_nodesMergeable(start.previousSibling,start,whiteList))
start = start.previousSibling;
while ((end.nextSibling != null) &&
DOM_nodesMergeable(end,end.nextSibling,whiteList))
end = end.nextSibling;
if (trim) {
while ((start.previousSibling != null) && isDiscardable(start.previousSibling))
DOM_deleteNode(start.previousSibling);
while ((end.nextSibling != null) && isDiscardable(end.nextSibling))
DOM_deleteNode(end.nextSibling);
}
if (start != end) {
var lastMerge;
do {
lastMerge = (start.nextSibling == end);
var lastChild = null;
if (start.nodeType == Node.ELEMENT_NODE)
lastChild = start.lastChild;
DOM_mergeWithNextSibling(start,whiteList);
if (lastChild != null)
Formatting_mergeWithNeighbours(lastChild,whiteList);
} while (!lastMerge);
}
}
// private
function mergeRange(range,whiteList)
{
var nodes = Range_getAllNodes(range);
for (var i = 0; i < nodes.length; i++) {
var next;
for (var p = nodes[i]; p != null; p = next) {
next = p.parentNode;
Formatting_mergeWithNeighbours(p,whiteList);
}
}
}
// public (called from cursor.js)
Formatting_splitTextBefore = function(pos,parentCheckFn,force)
{
var node = pos.node;
var offset = pos.offset;
if (parentCheckFn == null)
parentCheckFn = isBlockNode;
if (force || (offset > 0)) {
var before = DOM_createTextNode(document,"");
DOM_insertBefore(node.parentNode,before,node);
DOM_moveCharacters(node,0,offset,before,0,false,true);
Formatting_movePreceding(new Position(node.parentNode,DOM_nodeOffset(node)),
parentCheckFn,force);
return new Position(before,before.nodeValue.length);
}
else {
Formatting_movePreceding(new Position(node.parentNode,DOM_nodeOffset(node)),
parentCheckFn,force);
return pos;
}
}
// public
Formatting_splitTextAfter = function(pos,parentCheckFn,force)
{
var node = pos.node;
var offset = pos.offset;
if (parentCheckFn == null)
parentCheckFn = isBlockNode;
if (force || (offset < pos.node.nodeValue.length)) {
var after = DOM_createTextNode(document,"");
DOM_insertBefore(node.parentNode,after,node.nextSibling);
DOM_moveCharacters(node,offset,node.nodeValue.length,after,0,true,false);
Formatting_moveFollowing(new Position(node.parentNode,DOM_nodeOffset(node)+1),
parentCheckFn,force);
return new Position(after,0);
}
else {
Formatting_moveFollowing(new Position(node.parentNode,DOM_nodeOffset(node)+1),
parentCheckFn,force);
return pos;
}
}
// FIXME: movePreceding and moveNext could possibly be optimised by passing in a (parent,child)
// pair instead of (node,offset), i.e. parent is the same as node, but rather than passing the
// index of a child, we pass the child itself (or null if the offset is equal to
// childNodes.length)
// public
Formatting_movePreceding = function(pos,parentCheckFn,force)
{
var node = pos.node;
var offset = pos.offset;
if (parentCheckFn(node) || (node == document.body))
return new Position(node,offset);
var toMove = new Array();
var justWhitespace = true;
var result = new Position(node,offset);
for (var i = 0; i < offset; i++) {
if (!isWhitespaceTextNode(node.childNodes[i]))
justWhitespace = false;
toMove.push(node.childNodes[i]);
}
if ((toMove.length > 0) || force) {
if (justWhitespace && !force) {
for (var i = 0; i < toMove.length; i++)
DOM_insertBefore(node.parentNode,toMove[i],node);
}
else {
var copy = DOM_shallowCopyElement(node);
DOM_insertBefore(node.parentNode,copy,node);
for (var i = 0; i < toMove.length; i++)
DOM_insertBefore(copy,toMove[i],null);
result = new Position(copy,copy.childNodes.length);
}
}
Formatting_movePreceding(new Position(node.parentNode,DOM_nodeOffset(node)),
parentCheckFn,force);
return result;
}
// public
Formatting_moveFollowing = function(pos,parentCheckFn,force)
{
var node = pos.node;
var offset = pos.offset;
if (parentCheckFn(node) || (node == document.body))
return new Position(node,offset);
var toMove = new Array();
var justWhitespace = true;
var result = new Position(node,offset);
for (var i = offset; i < node.childNodes.length; i++) {
if (!isWhitespaceTextNode(node.childNodes[i]))
justWhitespace = false;
toMove.push(node.childNodes[i]);
}
if ((toMove.length > 0) || force) {
if (justWhitespace && !force) {
for (var i = 0; i < toMove.length; i++)
DOM_insertBefore(node.parentNode,toMove[i],node.nextSibling);
}
else {
var copy = DOM_shallowCopyElement(node);
DOM_insertBefore(node.parentNode,copy,node.nextSibling);
for (var i = 0; i < toMove.length; i++)
DOM_insertBefore(copy,toMove[i],null);
result = new Position(copy,0);
}
}
Formatting_moveFollowing(new Position(node.parentNode,DOM_nodeOffset(node)+1),
parentCheckFn,force);
return result;
}
// public
Formatting_paragraphTextUpToPosition = function(pos)
{
if (pos.node.nodeType == Node.TEXT_NODE) {
return stringToStartOfParagraph(pos.node,pos.offset);
}
else {
return stringToStartOfParagraph(Position_closestActualNode(pos),0);
}
function stringToStartOfParagraph(node,offset)
{
var start = node;
var components = new Array();
while (isInlineNode(node)) {
if (node.nodeType == Node.TEXT_NODE) {
if (node == start)
components.push(node.nodeValue.slice(0,offset));
else
components.push(node.nodeValue);
}
if (node.previousSibling != null) {
node = node.previousSibling;
while (isInlineNode(node) && (node.lastChild != null))
node = node.lastChild;
}
else {
node = node.parentNode;
}
}
return components.reverse().join("");
}
}
// public
Formatting_getFormatting = function()
{
// FIXME: implement a more efficient version of this algorithm which avoids duplicate checks
var range = Selection_get();
if (range == null)
return {};
Range_assertValid(range,"Selection");
var outermost = Range_getOutermostNodes(range,true);
var leafNodes = new Array();
for (var i = 0; i < outermost.length; i++) {
findLeafNodes(outermost[i],leafNodes);
}
var empty = Range_isEmpty(range);
var commonProperties = null;
for (var i = 0; i < leafNodes.length; i++) {
if (!isWhitespaceTextNode(leafNodes[i]) || empty) {
var leafNodeProperties = Formatting_getAllNodeProperties(leafNodes[i]);
if (leafNodeProperties["-uxwrite-paragraph-style"] == null)
leafNodeProperties["-uxwrite-paragraph-style"] = Keys.NONE_STYLE;
if (commonProperties == null)
commonProperties = leafNodeProperties;
else
commonProperties = intersection(commonProperties,leafNodeProperties);
}
}
if (commonProperties == null)
commonProperties = {"-uxwrite-paragraph-style": Keys.NONE_STYLE};
for (var i = 0; i < leafNodes.length; i++) {
var leaf = leafNodes[i];
if (leaf._type == HTML_LI) {
switch (leaf.parentNode._type) {
case HTML_UL:
commonProperties["-uxwrite-in-ul"] = "true";
break;
case HTML_OL:
commonProperties["-uxwrite-in-ol"] = "true";
break;
}
}
else {
for (var ancestor = leaf;
ancestor.parentNode != null;
ancestor = ancestor.parentNode) {
if (ancestor.parentNode._type == HTML_LI) {
var havePrev = false;
for (var c = ancestor.previousSibling; c != null; c = c.previousSibling) {
if (!isWhitespaceTextNode(c)) {
havePrev = true;
break;
}
}
if (!havePrev) {
var listNode = ancestor.parentNode.parentNode;
switch (listNode._type) {
case HTML_UL:
commonProperties["-uxwrite-in-ul"] = "true";
break;
case HTML_OL:
commonProperties["-uxwrite-in-ol"] = "true";
break;
}
}
}
}
}
}
getFlags(range.start,commonProperties);
return commonProperties;
function getFlags(pos,commonProperties)
{
var strBeforeCursor = Formatting_paragraphTextUpToPosition(pos);
if (isWhitespaceString(strBeforeCursor)) {
var firstInParagraph = true;
for (var p = pos.node; isInlineNode(p); p = p.parentNode) {
if (p.previousSibling != null)
firstInParagraph = false;
}
if (firstInParagraph)
commonProperties["-uxwrite-shift"] = "true";
}
if (strBeforeCursor.match(/\.\s*$/))
commonProperties["-uxwrite-shift"] = "true";
if (strBeforeCursor.match(/\([^\)]*$/))
commonProperties["-uxwrite-in-brackets"] = "true";
if (strBeforeCursor.match(/\u201c[^\u201d]*$/))
commonProperties["-uxwrite-in-quotes"] = "true";
}
function intersection(a,b)
{
var result = new Object();
for (var name in a) {
if (a[name] == b[name])
result[name] = a[name];
}
return result;
}
function findLeafNodes(node,result)
{
if (node.firstChild == null) {
result.push(node);
}
else {
for (var child = node.firstChild; child != null; child = child.nextSibling)
findLeafNodes(child,result);
}
}
}
// public
Formatting_getAllNodeProperties = function(node)
{
if (node == null)
throw new Error("Node is not in tree");
if (node == node.ownerDocument.body)
return new Object();
var properties = Formatting_getAllNodeProperties(node.parentNode);
if (node.nodeType == Node.ELEMENT_NODE) {
// Note: Style names corresponding to element names must be in lowercase, because
// canonicaliseSelector() in Styles.js always converts selectors to lowercase.
if (node.hasAttribute("STYLE")) {
var nodeProperties = getStyleProperties(node);
for (var name in nodeProperties)
properties[name] = nodeProperties[name];
}
var type = node._type;
switch (type) {
case HTML_B:
properties["font-weight"] = "bold";
break;
case HTML_I:
properties["font-style"] = "italic";
break;
case HTML_U: {
var components = [];
if (properties["text-decoration"] != null) {
var components = properties["text-decoration"].toLowerCase().split(/\s+/);
if (components.indexOf("underline") == -1)
properties["text-decoration"] += " underline";
}
else {
properties["text-decoration"] = "underline";
}
break;
}
// case HTML_TT:
// properties["-uxwrite-in-tt"] = "true";
// break;
case HTML_IMG:
properties["-uxwrite-in-image"] = "true";
break;
case HTML_FIGURE:
properties["-uxwrite-in-figure"] = "true";
break;
case HTML_TABLE:
properties["-uxwrite-in-table"] = "true";
break;
case HTML_A:
if (node.hasAttribute("href")) {
var href = node.getAttribute("href");
if (href.charAt(0) == "#")
properties["-uxwrite-in-reference"] = "true";
else
properties["-uxwrite-in-link"] = "true";
}
break;
case HTML_NAV: {
var className = DOM_getAttribute(node,"class");
if ((className == Keys.SECTION_TOC) ||
(className == Keys.FIGURE_TOC) ||
(className == Keys.TABLE_TOC))
properties["-uxwrite-in-toc"] = "true";
break;
}
default:
if (PARAGRAPH_ELEMENTS[type]) {
var name = node.nodeName.toLowerCase();
var selector;
if (node.hasAttribute("class"))
selector = name + "." + node.getAttribute("class");
else
selector = name;
properties["-uxwrite-paragraph-style"] = selector;
}
break;
}
if (OUTLINE_TITLE_ELEMENTS[type] && node.hasAttribute("id"))
properties["-uxwrite-in-item-title"] = node.getAttribute("id");
}
return properties;
}
var PARAGRAPH_PROPERTIES = {
"margin-left": true,
"margin-right": true,
"margin-top": true,
"margin-bottom": true,
"padding-left": true,
"padding-right": true,
"padding-top": true,
"padding-bottom": true,
"border-left-width": true,
"border-right-width": true,
"border-top-width": true,
"border-bottom-width": true,
"border-left-style": true,
"border-right-style": true,
"border-top-style": true,
"border-bottom-style": true,
"border-left-color": true,
"border-right-color": true,
"border-top-color": true,
"border-bottom-color": true,
"border-top-left-radius": true,
"border-top-right-radius": true,
"border-bottom-left-radius": true,
"border-bottom-right-radius": true,
"text-align": true,
"text-indent": true,
"line-height": true,
"display": true,
"width": true,
"height": true,
};
var SPECIAL_PROPERTIES = {
"-webkit-text-size-adjust": true, // set on HTML element for text scaling purposes
};
function isParagraphProperty(name)
{
return PARAGRAPH_PROPERTIES[name];
}
function isInlineProperty(name)
{
return !PARAGRAPH_PROPERTIES[name] && !SPECIAL_PROPERTIES[name];
}
// private
function putDirectInlineChildrenInParagraphs(parent)
{
var inlineChildren = new Array();
for (var child = parent.firstChild; child != null; child = child.nextSibling)
if (isInlineNode(child))
inlineChildren.push(child);
for (var i = 0; i < inlineChildren.length; i++) {
if (inlineChildren[i].parentNode == parent) { // may already have been moved
if (!isWhitespaceTextNode(inlineChildren[i]))
Hierarchy_wrapInlineNodesInParagraph(inlineChildren[i]);
}
}
}
// private
function getParagraphs(nodes)
{
var array = new Array();
var set = new NodeSet();
for (var i = 0; i < nodes.length; i++) {
for (var anc = nodes[i].parentNode; anc != null; anc = anc.parentNode) {
if (anc._type == HTML_LI)
putDirectInlineChildrenInParagraphs(anc);
}
recurse(nodes[i]);
}
var remove = new NodeSet();
for (var i = 0; i < array.length; i++) {
for (var anc = array[i].parentNode; anc != null; anc = anc.parentNode)
remove.add(anc);
}
var modified = new Array();
for (var i = 0; i < array.length; i++) {
if (!remove.contains(array[i]))
modified.push(array[i]);
}
return modified;
function recurse(node)
{
if (node._type == HTML_LI)
putDirectInlineChildrenInParagraphs(node);
if (node.firstChild == null) {
// Leaf node
for (var anc = node; anc != null; anc = anc.parentNode)
if (isParagraphNode(anc)) {
add(anc);
}
}
else {
for (var child = node.firstChild; child != null; child = child.nextSibling)
recurse(child);
}
}
function add(node)
{
if (!set.contains(node)) {
array.push(node);
set.add(node);
}
}
}
// private
function setParagraphStyle(paragraph,selector)
{
var wasHeading = isHeadingNode(paragraph);
DOM_removeAttribute(paragraph,"class");
if (selector == "") {
if (paragraph._type != HTML_P)
paragraph = DOM_replaceElement(paragraph,"P");
}
else {
var elementClassRegex = /^([a-zA-Z0-9]+)?(\.(.+))?$/;
var result = elementClassRegex.exec(selector);
if ((result != null) && (result.length == 4)) {
var elementName = result[1];
var className = result[3];
if (elementName == null)
elementName = "P";
else
elementName = elementName.toUpperCase();
var elementType = ElementTypes[elementName];
if (!PARAGRAPH_ELEMENTS[elementType])
return; // better than throwing an exception
if (paragraph._type != elementType)
paragraph = DOM_replaceElement(paragraph,elementName);
if (className != null)
DOM_setAttribute(paragraph,"class",className);
else
DOM_removeAttribute(paragraph,"class");
}
}
// FIXME: this will need to change when we add Word/ODF support, because the ids serve
// a purpose other than simply being targets for references
var isHeading = isHeadingNode(paragraph);
if (wasHeading && !isHeading)
DOM_removeAttribute(paragraph,"id");
}
// public
Formatting_pushDownInlineProperties = function(outermost)
{
for (var i = 0; i < outermost.length; i++)
outermost[i] = pushDownInlinePropertiesSingle(outermost[i]);
}
// private
function pushDownInlinePropertiesSingle(target)
{
recurse(target.parentNode);
return target;
function recurse(node)
{
if (node.nodeType == Node.DOCUMENT_NODE)
return;
if (node.parentNode != null)
recurse(node.parentNode);
var inlineProperties = new Object();
var nodeProperties = getStyleProperties(node);
for (var name in nodeProperties) {
if (isInlineProperty(name)) {
inlineProperties[name] = nodeProperties[name];
}
}
var remove = new Object();
for (var name in inlineProperties)
remove[name] = null;
DOM_setStyleProperties(node,remove);
var type = node._type;
switch (type) {
case HTML_B:
inlineProperties["font-weight"] = "bold";
break;
case HTML_I:
inlineProperties["font-style"] = "italic";
break;
case HTML_U:
if (inlineProperties["text-decoration"] != null)
inlineProperties["text-decoration"] += " underline";
else
inlineProperties["text-decoration"] = "underline";
break;
}
var special = extractSpecial(inlineProperties);
var count = Object.getOwnPropertyNames(inlineProperties).length;
if ((count > 0) || special.bold || special.italic || special.underline) {
var next;
for (var child = node.firstChild; child != null; child = next) {
next = child.nextSibling;
if (isWhitespaceTextNode(child))
continue;
var replacement = applyInlineFormatting(child,inlineProperties,special);
if (target == child)
target = replacement;
}
}
if (node.hasAttribute("style") && (node.style.length == 0))
DOM_removeAttribute(node,"style");
switch (type) {
case HTML_B:
case HTML_I:
case HTML_U:
DOM_removeNodeButKeepChildren(node);
break;
}
}
}
// private
function wrapInline(node,elementName)
{
if (!isInlineNode(node) || isAbstractSpan(node)) {
var next;
for (var child = node.firstChild; child != null; child = next) {
next = child.nextSibling;
wrapInline(child,elementName);
}
return node;
}
else {
return DOM_wrapNode(node,elementName);
}
}
// private
function applyInlineFormatting(target,inlineProperties,special,applyToWhitespace)
{
if (!applyToWhitespace && isWhitespaceTextNode(target))
return;
if (special.underline)
target = wrapInline(target,"U");
if (special.italic)
target = wrapInline(target,"I");
if (special.bold)
target = wrapInline(target,"B");
var isbiu = false;
switch (target._type) {
case HTML_B:
case HTML_I:
case HTML_U:
isbiu = true;
break;
}
if ((Object.getOwnPropertyNames(inlineProperties).length > 0) &&
((target.nodeType != Node.ELEMENT_NODE) ||
isbiu || isSpecialSpan(target))) {
target = wrapInline(target,"SPAN");
}
var propertiesToSet = new Object();
for (var name in inlineProperties) {
var existing = target.style.getPropertyValue(name);
if ((existing == null) || (existing == ""))
propertiesToSet[name] = inlineProperties[name];
}
DOM_setStyleProperties(target,propertiesToSet);
return target;
}
// private
function extractSpecial(properties)
{
var special = { bold: null, italic: null, underline: null };
var fontWeight = properties["font-weight"];
var fontStyle = properties["font-style"];
var textDecoration = properties["text-decoration"];
if (typeof(fontWeight) != "undefined") {
special.bold = false;
if ((fontWeight != null) &&
(fontWeight.toLowerCase() == "bold")) {
special.bold = true;
delete properties["font-weight"];
}
}
if (typeof(fontStyle) != "undefined") {
special.italic = false;
if ((fontStyle != null) &&
(fontStyle.toLowerCase() == "italic")) {
special.italic = true;
delete properties["font-style"];
}
}
if (typeof(textDecoration) != "undefined") {
special.underline = false;
if (textDecoration != null) {
var values = textDecoration.toLowerCase().split(/\s+/);
var index;
while ((index = values.indexOf("underline")) >= 0) {
values.splice(index,1);
special.underline = true;
}
if (values.length == 0)
delete properties["text-decoration"];
else
properties["text-decoration"] = values.join(" ");
}
}
return special;
}
// private
function removeProperties(outermost,properties)
{
properties = clone(properties);
var special = extractSpecial(properties);
var remaining = new Array();
for (var i = 0; i < outermost.length; i++) {
removePropertiesSingle(outermost[i],properties,special,remaining);
}
return remaining;
}
// private
function getOutermostParagraphs(paragraphs)
{
var all = new NodeSet();
for (var i = 0; i < paragraphs.length; i++)
all.add(paragraphs[i]);
var result = new Array();
for (var i = 0; i < paragraphs.length; i++) {
var haveAncestor = false;
for (var p = paragraphs[i].parentNode; p != null; p = p.parentNode) {
if (all.contains(p)) {
haveAncestor = true;
break;
}
}
if (!haveAncestor)
result.push(paragraphs[i]);
}
return result;
}
// private
function removePropertiesSingle(node,properties,special,remaining)
{
if ((node.nodeType == Node.ELEMENT_NODE) && (node.hasAttribute("style"))) {
var remove = new Object();
for (var name in properties)
remove[name] = null;
DOM_setStyleProperties(node,remove);
}
var willRemove = false;
switch (node._type) {
case HTML_B:
willRemove = (special.bold != null);
break;
case HTML_I:
willRemove = (special.italic != null);
break;
case HTML_U:
willRemove = (special.underline != null);
break;
case HTML_SPAN:
willRemove = (!node.hasAttribute("style") && !isSpecialSpan(node));
break;
}
var childRemaining = willRemove ? remaining : null;
var next;
for (var child = node.firstChild; child != null; child = next) {
next = child.nextSibling;
removePropertiesSingle(child,properties,special,childRemaining);
}
if (willRemove)
DOM_removeNodeButKeepChildren(node);
else if (remaining != null)
remaining.push(node);
}
function isSpecialSpan(span)
{
if (span._type == HTML_SPAN) {
if (span.hasAttribute(Keys.ABSTRACT_ELEMENT))
return true;
var className = DOM_getStringAttribute(span,"class");
if (className.indexOf(Keys.UXWRITE_PREFIX) == 0)
return true;
if ((className == "footnote") || (className == "endnote"))
return true;
}
return false;
}
// private
function containsOnlyWhitespace(ancestor)
{
for (child = ancestor.firstChild; child != null; child = child.nextSibling) {
if (!isWhitespaceTextNode(child))
return false;
}
return true;
}
// public
Formatting_applyFormattingChanges = function(style,properties)
{
debug("JS: applyFormattingChanges: style = "+JSON.stringify(style));
if (properties != null) {
var names = Object.getOwnPropertyNames(properties).sort();
for (var i = 0; i < names.length; i++) {
debug(" "+names[i]+" = "+properties[names[i]]);
}
}
UndoManager_newGroup("Apply formatting changes");
if (properties == null)
properties = new Object();
if (style == Keys.NONE_STYLE)
style = null;
var paragraphProperties = new Object();
var inlineProperties = new Object();
for (var name in properties) {
if (isParagraphProperty(name))
paragraphProperties[name] = properties[name];
else if (isInlineProperty(name))
inlineProperties[name] = properties[name];
}
var selectionRange = Selection_get();
if (selectionRange == null)
return;
// If we're applying formatting properties to an empty selection, and the node of the
// selection start & end is an element, add an empty text node so that we have something
// to apply the formatting to.
if (Range_isEmpty(selectionRange) &&
(selectionRange.start.node.nodeType == Node.ELEMENT_NODE)) {
var node = selectionRange.start.node;
var offset = selectionRange.start.offset;
var text = DOM_createTextNode(document,"");
DOM_insertBefore(node,text,node.childNodes[offset]);
Selection_set(text,0,text,0);
selectionRange = Selection_get();
}
// If the cursor is in a container (such as BODY OR FIGCAPTION), and not inside a paragraph,
// put it in one so we can set a paragraph style
if ((style != null) && Range_isEmpty(selectionRange)) {
var node = Range_singleNode(selectionRange);
while (isInlineNode(node))
node = node.parentNode;
if (isContainerNode(node) && containsOnlyInlineChildren(node)) {
var p = DOM_createElement(document,"P");
DOM_appendChild(node,p);
while (node.firstChild != p)
DOM_appendChild(p,node.firstChild);
Cursor_updateBRAtEndOfParagraph(p);
}
}
var range = new Range(selectionRange.start.node,selectionRange.start.offset,
selectionRange.end.node,selectionRange.end.offset);
var positions = [selectionRange.start,selectionRange.end,
range.start,range.end];
var allowDirectInline = (style == null);
Position_trackWhileExecuting(positions,function() {
Formatting_splitAroundSelection(range,allowDirectInline);
Range_expand(range);
if (!allowDirectInline)
Range_ensureInlineNodesInParagraph(range);
Range_ensureValidHierarchy(range);
Range_expand(range);
var outermost = Range_getOutermostNodes(range);
var target = null;
var paragraphs;
if (outermost.length > 0)
paragraphs = getParagraphs(outermost);
else
paragraphs = getParagraphs([Range_singleNode(range)]);
// Push down inline properties
Formatting_pushDownInlineProperties(outermost);
outermost = removeProperties(outermost,inlineProperties);
// Set properties on inline nodes
for (var i = 0; i < outermost.length; i++) {
var existing = Formatting_getAllNodeProperties(outermost[i]);
var toSet = new Object();
for (var name in inlineProperties) {
if ((inlineProperties[name] != null) &&
(existing[name] != inlineProperties[name])) {
toSet[name] = inlineProperties[name];
}
}
var special = extractSpecial(toSet);
var applyToWhitespace = (outermost.length == 1);
applyInlineFormatting(outermost[i],toSet,special,applyToWhitespace);
}
// Remove properties from paragraph nodes
paragraphs = removeProperties(paragraphs,paragraphProperties,{});
// Set properties on paragraph nodes
var paragraphPropertiesToSet = new Object();
for (var name in paragraphProperties) {
if (paragraphProperties[name] != null)
paragraphPropertiesToSet[name] = paragraphProperties[name];
}
var outermostParagraphs = getOutermostParagraphs(paragraphs);
for (var i = 0; i < outermostParagraphs.length; i++)
DOM_setStyleProperties(outermostParagraphs[i],paragraphPropertiesToSet);
// Set style on paragraph nodes
if (style != null) {
for (var i = 0; i < paragraphs.length; i++) {
setParagraphStyle(paragraphs[i],style);
}
}
mergeRange(range,Formatting_MERGEABLE_INLINE);
if (target != null) {
for (var p = target; p != null; p = next) {
next = p.parentNode;
Formatting_mergeWithNeighbours(p,Formatting_MERGEABLE_INLINE);
}
}
});
// The current cursor position may no longer be valid, e.g. if a heading span was inserted
// and the cursor is at a position that is now immediately before the span.
var start = Position_closestMatchForwards(selectionRange.start,Position_okForInsertion);
var end = Position_closestMatchBackwards(selectionRange.end,Position_okForInsertion);
var tempRange = new Range(start.node,start.offset,end.node,end.offset);
tempRange = Range_forwards(tempRange);
Range_ensureValidHierarchy(tempRange);
start = tempRange.start;
end = tempRange.end;
Selection_set(start.node,start.offset,end.node,end.offset);
function containsOnlyInlineChildren(node)
{
for (var child = node.firstChild; child != null; child = child.nextSibling) {
if (!isInlineNode(child))
return false;
}
return true;
}
}
Formatting_formatInlineNode = function(node,properties)
{
properties = clone(properties);
var special = extractSpecial(properties);
return applyInlineFormatting(node,properties,special,true);
}
Formatting_MERGEABLE_INLINE = new Array(HTML_COUNT);
Formatting_MERGEABLE_INLINE[HTML_TEXT] = true;
Formatting_MERGEABLE_INLINE[HTML_SPAN] = true;
Formatting_MERGEABLE_INLINE[HTML_A] = true;
Formatting_MERGEABLE_INLINE[HTML_Q] = true;
// HTML 4.01 Section 9.2.1: Phrase elements
Formatting_MERGEABLE_INLINE[HTML_EM] = true;
Formatting_MERGEABLE_INLINE[HTML_STRONG] = true;
Formatting_MERGEABLE_INLINE[HTML_DFN] = true;
Formatting_MERGEABLE_INLINE[HTML_CODE] = true;
Formatting_MERGEABLE_INLINE[HTML_SAMP] = true;
Formatting_MERGEABLE_INLINE[HTML_KBD] = true;
Formatting_MERGEABLE_INLINE[HTML_VAR] = true;
Formatting_MERGEABLE_INLINE[HTML_CITE] = true;
Formatting_MERGEABLE_INLINE[HTML_ABBR] = true;
// HTML 4.01 Section 9.2.3: Subscripts and superscripts
Formatting_MERGEABLE_INLINE[HTML_SUB] = true;
Formatting_MERGEABLE_INLINE[HTML_SUP] = true;
// HTML 4.01 Section 15.2.1: Font style elements
Formatting_MERGEABLE_INLINE[HTML_I] = true;
Formatting_MERGEABLE_INLINE[HTML_B] = true;
Formatting_MERGEABLE_INLINE[HTML_SMALL] = true;
Formatting_MERGEABLE_INLINE[HTML_S] = true;
Formatting_MERGEABLE_INLINE[HTML_U] = true;
Formatting_MERGEABLE_BLOCK = new Array(HTML_COUNT);
Formatting_MERGEABLE_BLOCK[HTML_P] = true;
Formatting_MERGEABLE_BLOCK[HTML_H1] = true;
Formatting_MERGEABLE_BLOCK[HTML_H2] = true;
Formatting_MERGEABLE_BLOCK[HTML_H3] = true;
Formatting_MERGEABLE_BLOCK[HTML_H4] = true;
Formatting_MERGEABLE_BLOCK[HTML_H5] = true;
Formatting_MERGEABLE_BLOCK[HTML_H6] = true;
Formatting_MERGEABLE_BLOCK[HTML_DIV] = true;
Formatting_MERGEABLE_BLOCK[HTML_PRE] = true;
Formatting_MERGEABLE_BLOCK[HTML_BLOCKQUOTE] = true;
Formatting_MERGEABLE_BLOCK[HTML_UL] = true;
Formatting_MERGEABLE_BLOCK[HTML_OL] = true;
Formatting_MERGEABLE_BLOCK[HTML_LI] = true;
Formatting_MERGEABLE_BLOCK_AND_INLINE = new Array(HTML_COUNT);
for (var i = 0; i < HTML_COUNT; i++) {
if (Formatting_MERGEABLE_INLINE[i] || Formatting_MERGEABLE_BLOCK[i])
Formatting_MERGEABLE_BLOCK_AND_INLINE[i] = true;
Formatting_MERGEABLE_BLOCK_AND_INLINE["force"] = true;
}
})();