diff --git editors/js/tinymce-3.js editors/js/tinymce-3.js
index b04a4ce..b827b83 100644
--- editors/js/tinymce-3.js
+++ editors/js/tinymce-3.js
@@ -200,26 +200,27 @@ Drupal.wysiwyg.editor.instance.tinymce = {
var specialProperties = {
img: { 'class': 'mceItem' }
};
- var $content = $('
' + content + '
'); // No .outerHTML() in jQuery :(
- // Find all placeholder/replacement content of Drupal plugins.
- $content.find('.drupal-content').each(function() {
- // Recursively process DOM elements below this element to apply special
- // properties.
- var $drupalContent = $(this);
- $.each(specialProperties, function(element, properties) {
- $drupalContent.find(element).andSelf().each(function() {
- for (var property in properties) {
- if (property == 'class') {
- $(this).addClass(properties[property]);
+ return Drupal.wysiwyg.utilities.modifyAsDom(content, function (dom) {
+ var $content = $(dom);
+ // Find all placeholder/replacement content of Drupal plugins.
+ $content.find('.drupal-content').each(function() {
+ // Recursively process DOM elements below this element to apply special
+ // properties.
+ var $drupalContent = $(this);
+ $.each(specialProperties, function(element, properties) {
+ $drupalContent.find(element).andSelf().each(function() {
+ for (var property in properties) {
+ if (property == 'class') {
+ $(this).addClass(properties[property]);
+ }
+ else {
+ $(this).attr(property, properties[property]);
+ }
}
- else {
- $(this).attr(property, properties[property]);
- }
- }
+ });
});
});
});
- return $content.html();
},
insert: function(content) {
diff --git plugins/break/break.js plugins/break/break.js
index 54aac4c..2c13a82 100644
--- plugins/break/break.js
+++ plugins/break/break.js
@@ -46,15 +46,16 @@ Drupal.wysiwyg.plugins['break'] = {
* Replace images with tags in content upon detaching editor.
*/
detach: function(content, settings, instanceId) {
- var $content = $('' + content + '
'); // No .outerHTML() in jQuery :(
- // #404532: document.createComment() required or IE will strip the comment.
- // #474908: IE 8 breaks when using jQuery methods to replace the elements.
- // @todo Add a generic implementation for all Drupal plugins for this.
- $.each($('img.wysiwyg-break', $content), function (i, elem) {
- elem.parentNode.insertBefore(document.createComment('break'), elem);
- elem.parentNode.removeChild(elem);
+ return Drupal.wysiwyg.utilities.modifyAsDom(content, function (dom) {
+ var $content = $(dom);
+ // #404532: document.createComment() required or IE will strip the comment.
+ // #474908: IE 8 breaks when using jQuery methods to replace the elements.
+ // @todo Add a generic implementation for all Drupal plugins for this.
+ $.each($('img.wysiwyg-break', $content), function (i, elem) {
+ elem.parentNode.insertBefore(document.createComment('break'), elem);
+ elem.parentNode.removeChild(elem);
+ });
});
- return $content.html();
},
/**
diff --git wysiwyg.js wysiwyg.js
index 72ca156..97a8ebb 100644
--- wysiwyg.js
+++ wysiwyg.js
@@ -230,6 +230,259 @@ Drupal.wysiwyg.getParams = function(element, params) {
};
/**
+ * Serialize a DOM node and its children to an XHTML string.
+ *
+ * Makes sure source formatting is preserved across all major browsers.
+ *
+ * @param node
+ * A DOM node, will not be modified.
+ *
+ * @returns
+ * A string containing the XHTML representation of the node, empty
+ * if the node could not be serialized.
+ */
+function serialize(node) {
+ // Inspired by Steve Tucker's innerXHTML, http://www.stevetucker.co.uk.
+ if (!node || (typeof node.nodeType == 'undefined' && typeof node.length == 'undefined' )) {
+ return '';
+ }
+ var xhtmlContent = '', nodeType = node.nodeType, nodeName = (node.nodeName ? node.nodeName.toLowerCase() : '');
+ if (typeof nodeType == 'undefined') {
+ for (var i = 0; i < node.length; i++) {
+ xhtmlContent += serialize(node[i]);
+ }
+ return xhtmlContent;
+ }
+ else if (nodeType == 3) {
+ // Text node.
+ return node.nodeValue.replace(//g, '>');
+ }
+ else if (nodeType == 8) {
+ // Comment node.
+ return '';
+ }
+ else if (nodeType == 1) {
+ // Element node.
+ xhtmlContent += '<' + nodeName;
+ var attributes = node.attributes;
+ for (var j=0; j < attributes.length; j++) {
+ var attrib = attributes[j], attName = attrib.nodeName.toLowerCase(), attValue = attrib.nodeValue;
+ if ((attValue == 1 && (attName == 'colspan' || attName == 'rowspan' || attName == 'start' || attName == 'loop'))
+ || (attName == 'start' && attValue == 'fileopen') ) {
+ // IE compatibility mode always sets these, despite being defaults.
+ continue;
+ }
+ if (/^data-wysiwyg-masked-/.test(attName)) {
+ // Ignore these temporary attributes, see below.
+ continue;
+ }
+ if ((attName == 'name' || attName == 'src' || attName == 'href') && attributes['data-wysiwyg-masked-' + attName]) {
+ // Browsers often turn relative URLs into absolute in these attributes.
+ attValue = attributes['data-wysiwyg-masked-' + attName].nodeValue || attValue;
+ }
+ if (attName == 'style' && node.style.cssText) {
+ // IE uppercases style attributes, values must be kept intact.
+ var styles = node.style.cssText.replace(/(^|;)([^\:]+)/g, function (match) {
+ return match.toLowerCase();
+ });
+ xhtmlContent += ' style="' + styles + '"';
+ }
+ else if (attValue && attName != 'contenteditable') {
+ xhtmlContent += ' ' + attName + '="' + attValue + '"';
+ }
+ }
+ // Clone the node and get its outerHTML to test if it was self-closed.
+ var elemClone = node.cloneNode(false), container = document.createElement('div');
+ container.appendChild(elemClone);
+ var selfClosed = !new RegExp('' + nodeName + '>\s*$', 'i').test(container.innerHTML);
+ delete container;
+ }
+ // IE doesn't set nodeValue for script tags.
+
+ if (nodeName == 'script' && node.nodeValue == '') {
+ xhtmlContent += node.text;
+ }
+ else {
+ // Process children for types that can have them.
+ var children = node.childNodes;
+ var innerContent = '';
+ for (var i = 0; i < children.length; i++) {
+ var child = children[i];
+ innerContent += serialize(child);
+ }
+ if (nodeType == 1) {
+ if (selfClosed) {
+ xhtmlContent += ' />' + innerContent;
+ }
+ else {
+ xhtmlContent += '>' + innerContent + '' + nodeName + '>';
+ }
+ }
+ else {
+ xhtmlContent += innerContent;
+ }
+ }
+ return xhtmlContent;
+}
+
+/**
+ * Unserialize an XHTML string to one or more DOM nodes.
+ *
+ * @param content
+ * A valid XHTML string.
+ *
+ * @returns
+ * One or more DOM nodes wrapped by a DocumentFragment node.
+ */
+function unserialize(content) {
+ // Use a pre element to preserve formatting (#text) nodes in IE.
+ var $pre = $('' + content + '
');
+ var pre = $pre[0];
+ return pre.childNodes;
+ var dom = document.createDocumentFragment();
+ while (pre.firstChild) {
+ dom.appendChild(pre.firstChild);
+ }
+ pre.parentNode.removeChild(pre);
+ delete pre;
+ return dom;
+}
+
+/**
+ * Masks tags in a markup string as divs.
+ *
+ * All ocurrances of tags are changed to divs and given an extra
+ * "data-masked" attribute to identify the old tag name.
+ *
+ * @see unmaskTags().
+ *
+ * @param content
+ * A valid XHTML markup string.
+ *
+ * @param tags
+ * An array of tag names to be replaced by divs.
+ *
+ * @returns
+ * An XHTML markup string with all instances of tags replaced by divs.
+ */
+function maskTags(content, tags) {
+ var replaced = content.replace(new RegExp('<(' + tags.join('|') + ')', 'gi'), '', 'gi'), '
');
+ // Borrowed from CKEditor to prevent relative URLsi from becoming absolute.
+ var protectAttributeRegex = /<((?:a|area|img|input)\b[\s\S]*?\s)((href|src|name)\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|(?:[^ "'>]+)))([^>]*)>/gi;
+ replaced = replaced.replace(protectAttributeRegex, function(tag, beginning, fullAttr, attrName, end) {
+ return '<' + beginning + fullAttr + ' data-wysiwyg-masked-' + fullAttr + end + '>';
+ });
+ // Escape entities since the innerHTML operation in unserialize mangles them.
+ replaced = replaced.replace(/\&(\w+|#\d+);/g, "$1");
+ return replaced;
+}
+
+/**
+ * Unmasks tags previously nasked as divs.
+ *
+ * Recursively looks for nodes having a "data-masked" attribute and creates
+ * new element nodes of the corresponding type. Other nodes are just cloned.
+ *
+ * @see maskTags().
+ *
+ * @param node
+ * A DOM node to create a unmasked clone of.
+ *
+ * @returns
+ * A clone of the given DOM tree, after unmasking placeholders.
+ */
+function unmaskTags(node) {
+ var unmaskedTag = null;
+ if (node && typeof node.nodeType == 'undefined' && typeof node.length != 'undefined') {
+ var list = [];
+ for (var i = 0; i < node.length; i++) {
+ list[i] = unmaskTags(node[i]);
+ }
+ return list;
+ }
+ else if (node.getAttribute) {
+ if (node.getAttribute('data-masked')) {
+ // Create the new element and transfer attributes from the placeholder.
+ unmaskedTag = node.ownerDocument.createElement(node.getAttribute('data-masked'));
+ for (var i = 0; i < node.attributes.length; i++) {
+ var attribute = node.attributes[i];
+ if (attribute.specified && attribute.name.toLowerCase() != 'data-masked') {
+ unmaskedTag.setAttribute(attribute.name, attribute.value);
+ }
+ }
+ }
+ else if (node.getAttribute('data-wysiwyg-protected-entity')) {
+ // Recreate the entity as a text node, will be merged with siblings later.
+ // Text nodes can't have children so return right away.
+ return document.createTextNode('&' + node.innerText + ';');
+ }
+ else {
+ // The node was not masked, just clone it.
+ unmaskedTag = node.cloneNode(false);
+ }
+ }
+ else if (node.nodeType == 3 && node.nodeValue == '') {
+ // Skip empty text nodes.
+ return null;
+ }
+ else {
+ // The node doesn't support attributes, just clone it.
+ unmaskedTag = node.cloneNode(false);
+ }
+ for (var i = 0; i < node.childNodes.length; i++) {
+ var clonedChild = unmaskTags(node.childNodes[i]);
+ if (clonedChild) {
+ unmaskedTag.appendChild(clonedChild);
+ }
+ }
+ // Merge text nodes.
+ if (unmaskedTag.normalize) {
+ unmaskedTag.normalize();
+ }
+ return unmaskedTag;
+}
+
+/**
+ * Utility functions provided by Wysiwyg.
+ */
+Drupal.wysiwyg.utilities = {
+
+ /**
+ * Temporarily convert XHTML to a DOM and back again via a callback.
+ *
+ * Provides a convenient way to modify a valid XHTML string as DOM nodes.
+ * Preserves source indentation and whitespaces as #text nodes in all major
+ * browsers, otherwise not possible using .innerHTML in IE.
+ *
+ * @param content
+ * A valid XHTML string.
+ *
+ * @param callback
+ * A callback function to be called when DOM nodes have been generated.
+ * The callback may modify the DOM nodes in any way needed. If assigning an
+ * element's src or href attribute and it's important for URIs to be
+ * relative, also assign the same value to the corresponding
+ * data-wysiwyg-masked-[attribute name] attribute. It keeps the browser from
+ * making the URI absolute. The same rule applies when reading these
+ * attributes. Instead of element.getAttribute('src'), do
+ * element.getAttribute('data-wysiwyg-protected-src') etc.
+ *
+ * The callback's return value is ignored.
+ *
+ * @returns
+ * An XHTML string representing the DOM nodes as left by the callback.
+ */
+ modifyAsDom : function (content, callback) {
+ var tags = ['table', 'caption', 'colgroup', 'col', 'thead', 'tbody', 'tr', 'th', 'td', 'tfoot'];
+ var dom = unmaskTags(unserialize(maskTags(content, tags)));
+ callback(dom);
+ var clone = serialize(dom);
+ return clone;
+ }
+}
+
+/**
* Allow certain editor libraries to initialize before the DOM is loaded.
*/
Drupal.wysiwygInit();