/**
* Node.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class is a minimalistic implementation of a DOM like node used by the DomParser class.
*
* @example
* var node = new tinymce.html.Node('strong', 1);
* someRoot.append(node);
*
* @class tinymce.html.Node
* @version 3.4
*/
define(
'tinymce.core.html.Node',
[
],
function () {
var whiteSpaceRegExp = /^[ \t\r\n]*$/;
var typeLookup = {
'#text': 3,
'#comment': 8,
'#cdata': 4,
'#pi': 7,
'#doctype': 10,
'#document-fragment': 11
};
// Walks the tree left/right
var walk = function (node, rootNode, prev) {
var sibling, parent, startName = prev ? 'lastChild' : 'firstChild', siblingName = prev ? 'prev' : 'next';
// Walk into nodes if it has a start
if (node[startName]) {
return node[startName];
}
// Return the sibling if it has one
if (node !== rootNode) {
sibling = node[siblingName];
if (sibling) {
return sibling;
}
// Walk up the parents to look for siblings
for (parent = node.parent; parent && parent !== rootNode; parent = parent.parent) {
sibling = parent[siblingName];
if (sibling) {
return sibling;
}
}
}
};
/**
* Constructs a new Node instance.
*
* @constructor
* @method Node
* @param {String} name Name of the node type.
* @param {Number} type Numeric type representing the node.
*/
var Node = function (name, type) {
this.name = name;
this.type = type;
if (type === 1) {
this.attributes = [];
this.attributes.map = {};
}
};
Node.prototype = {
/**
* Replaces the current node with the specified one.
*
* @example
* someNode.replace(someNewNode);
*
* @method replace
* @param {tinymce.html.Node} node Node to replace the current node with.
* @return {tinymce.html.Node} The old node that got replaced.
*/
replace: function (node) {
var self = this;
if (node.parent) {
node.remove();
}
self.insert(node, self);
self.remove();
return self;
},
/**
* Gets/sets or removes an attribute by name.
*
* @example
* someNode.attr("name", "value"); // Sets an attribute
* console.log(someNode.attr("name")); // Gets an attribute
* someNode.attr("name", null); // Removes an attribute
*
* @method attr
* @param {String} name Attribute name to set or get.
* @param {String} value Optional value to set.
* @return {String/tinymce.html.Node} String or undefined on a get operation or the current node on a set operation.
*/
attr: function (name, value) {
var self = this, attrs, i, undef;
if (typeof name !== "string") {
for (i in name) {
self.attr(i, name[i]);
}
return self;
}
if ((attrs = self.attributes)) {
if (value !== undef) {
// Remove attribute
if (value === null) {
if (name in attrs.map) {
delete attrs.map[name];
i = attrs.length;
while (i--) {
if (attrs[i].name === name) {
attrs = attrs.splice(i, 1);
return self;
}
}
}
return self;
}
// Set attribute
if (name in attrs.map) {
// Set attribute
i = attrs.length;
while (i--) {
if (attrs[i].name === name) {
attrs[i].value = value;
break;
}
}
} else {
attrs.push({ name: name, value: value });
}
attrs.map[name] = value;
return self;
}
return attrs.map[name];
}
},
/**
* Does a shallow clones the node into a new node. It will also exclude id attributes since
* there should only be one id per document.
*
* @example
* var clonedNode = node.clone();
*
* @method clone
* @return {tinymce.html.Node} New copy of the original node.
*/
clone: function () {
var self = this, clone = new Node(self.name, self.type), i, l, selfAttrs, selfAttr, cloneAttrs;
// Clone element attributes
if ((selfAttrs = self.attributes)) {
cloneAttrs = [];
cloneAttrs.map = {};
for (i = 0, l = selfAttrs.length; i < l; i++) {
selfAttr = selfAttrs[i];
// Clone everything except id
if (selfAttr.name !== 'id') {
cloneAttrs[cloneAttrs.length] = { name: selfAttr.name, value: selfAttr.value };
cloneAttrs.map[selfAttr.name] = selfAttr.value;
}
}
clone.attributes = cloneAttrs;
}
clone.value = self.value;
clone.shortEnded = self.shortEnded;
return clone;
},
/**
* Wraps the node in in another node.
*
* @example
* node.wrap(wrapperNode);
*
* @method wrap
*/
wrap: function (wrapper) {
var self = this;
self.parent.insert(wrapper, self);
wrapper.append(self);
return self;
},
/**
* Unwraps the node in other words it removes the node but keeps the children.
*
* @example
* node.unwrap();
*
* @method unwrap
*/
unwrap: function () {
var self = this, node, next;
for (node = self.firstChild; node;) {
next = node.next;
self.insert(node, self, true);
node = next;
}
self.remove();
},
/**
* Removes the node from it's parent.
*
* @example
* node.remove();
*
* @method remove
* @return {tinymce.html.Node} Current node that got removed.
*/
remove: function () {
var self = this, parent = self.parent, next = self.next, prev = self.prev;
if (parent) {
if (parent.firstChild === self) {
parent.firstChild = next;
if (next) {
next.prev = null;
}
} else {
prev.next = next;
}
if (parent.lastChild === self) {
parent.lastChild = prev;
if (prev) {
prev.next = null;
}
} else {
next.prev = prev;
}
self.parent = self.next = self.prev = null;
}
return self;
},
/**
* Appends a new node as a child of the current node.
*
* @example
* node.append(someNode);
*
* @method append
* @param {tinymce.html.Node} node Node to append as a child of the current one.
* @return {tinymce.html.Node} The node that got appended.
*/
append: function (node) {
var self = this, last;
if (node.parent) {
node.remove();
}
last = self.lastChild;
if (last) {
last.next = node;
node.prev = last;
self.lastChild = node;
} else {
self.lastChild = self.firstChild = node;
}
node.parent = self;
return node;
},
/**
* Inserts a node at a specific position as a child of the current node.
*
* @example
* parentNode.insert(newChildNode, oldChildNode);
*
* @method insert
* @param {tinymce.html.Node} node Node to insert as a child of the current node.
* @param {tinymce.html.Node} refNode Reference node to set node before/after.
* @param {Boolean} before Optional state to insert the node before the reference node.
* @return {tinymce.html.Node} The node that got inserted.
*/
insert: function (node, refNode, before) {
var parent;
if (node.parent) {
node.remove();
}
parent = refNode.parent || this;
if (before) {
if (refNode === parent.firstChild) {
parent.firstChild = node;
} else {
refNode.prev.next = node;
}
node.prev = refNode.prev;
node.next = refNode;
refNode.prev = node;
} else {
if (refNode === parent.lastChild) {
parent.lastChild = node;
} else {
refNode.next.prev = node;
}
node.next = refNode.next;
node.prev = refNode;
refNode.next = node;
}
node.parent = parent;
return node;
},
/**
* Get all children by name.
*
* @method getAll
* @param {String} name Name of the child nodes to collect.
* @return {Array} Array with child nodes matchin the specified name.
*/
getAll: function (name) {
var self = this, node, collection = [];
for (node = self.firstChild; node; node = walk(node, self)) {
if (node.name === name) {
collection.push(node);
}
}
return collection;
},
/**
* Removes all children of the current node.
*
* @method empty
* @return {tinymce.html.Node} The current node that got cleared.
*/
empty: function () {
var self = this, nodes, i, node;
// Remove all children
if (self.firstChild) {
nodes = [];
// Collect the children
for (node = self.firstChild; node; node = walk(node, self)) {
nodes.push(node);
}
// Remove the children
i = nodes.length;
while (i--) {
node = nodes[i];
node.parent = node.firstChild = node.lastChild = node.next = node.prev = null;
}
}
self.firstChild = self.lastChild = null;
return self;
},
/**
* Returns true/false if the node is to be considered empty or not.
*
* @example
* node.isEmpty({img: true});
* @method isEmpty
* @param {Object} elements Name/value object with elements that are automatically treated as non empty elements.
* @param {Object} whitespace Name/value object with elements that are automatically treated whitespace preservables.
* @param {function} predicate Optional predicate that gets called after the other rules determine that the node is empty. Should return true if the node is a content node.
* @return {Boolean} true/false if the node is empty or not.
*/
isEmpty: function (elements, whitespace, predicate) {
var self = this, node = self.firstChild, i, name;
whitespace = whitespace || {};
if (node) {
do {
if (node.type === 1) {
// Ignore bogus elements
if (node.attributes.map['data-mce-bogus']) {
continue;
}
// Keep empty elements like <img />
if (elements[node.name]) {
return false;
}
// Keep bookmark nodes and name attribute like <a name="1"></a>
i = node.attributes.length;
while (i--) {
name = node.attributes[i].name;
if (name === "name" || name.indexOf('data-mce-bookmark') === 0) {
return false;
}
}
}
// Keep comments
if (node.type === 8) {
return false;
}
// Keep non whitespace text nodes
if (node.type === 3 && !whiteSpaceRegExp.test(node.value)) {
return false;
}
// Keep whitespace preserve elements
if (node.type === 3 && node.parent && whitespace[node.parent.name] && whiteSpaceRegExp.test(node.value)) {
return false;
}
// Predicate tells that the node is contents
if (predicate && predicate(node)) {
return false;
}
} while ((node = walk(node, self)));
}
return true;
},
/**
* Walks to the next or previous node and returns that node or null if it wasn't found.
*
* @method walk
* @param {Boolean} prev Optional previous node state defaults to false.
* @return {tinymce.html.Node} Node that is next to or previous of the current node.
*/
walk: function (prev) {
return walk(this, null, prev);
}
};
/**
* Creates a node of a specific type.
*
* @static
* @method create
* @param {String} name Name of the node type to create for example "b" or "#text".
* @param {Object} attrs Name/value collection of attributes that will be applied to elements.
*/
Node.create = function (name, attrs) {
var node, attrName;
// Create node
node = new Node(name, typeLookup[name] || 1);
// Add attributes if needed
if (attrs) {
for (attrName in attrs) {
node.attr(attrName, attrs[attrName]);
}
}
return node;
};
return Node;
}
);
|