/* Copyright (c) 2011, Geert Bergman (geert@scrivo.nl)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of "Scrivo" nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* $Id: PasteData.js 738 2013-07-12 18:38:39Z geert $
*/
"use strict";
SUI.control.PasteData = SUI.defineClass(
/** @lends SUI.control.PasteData.prototype */{
/**
* @private
*
* @class
* <p>The SUI.control.PasteData is a part of the HTML editor that deals
* with pasting data. We want to offer different ways of pasting text:
* plain text, filtered (just a limited set of tags and attributes) or
* with all markup.
* </p>
* <p>The latter case is obviously very simple: that is how normal pasting
* works, but is not what we prefer as standard actions. We want to remove
* all useless and interfering markup that editors as Word use.
* </p>
* <p>Since we're not allowed to access the paste buffer using JavaScript
* we fool the browser here. When pasting we direct the focus to an div
* element that we create for the purpose of intercepting the paste
* action. We use the 'onbeforepaste' (IE) and 'onpaste' (others) event
* handlers to do that.
* </p>
* <p>We also monitor the key strokes and if we discover that there is data
* to paste we finish the paste action by cleaning the pasted data using
* the requested method and inserting it into the content.
* </p>
*
* @description
* The SUI.control.PasteData implements the pasting logic for the HTML
* editor. It is a fundamental part of the HTML editor and therefore both
* are able to access each other's private data members.
*
* @constructs
* @param {SUI.control._HTMLEditControl} arg.editor A reference to an
* instance of the SUI.control.HTMLEditControl.
*/
initializer: function(arg) {
this._editor = arg.editor;
},
/**
* The listener method to use on the 'keyup' event handler of the HTML
* editor to finish an intercepted the paste actions. If it is detected
* that there is data to be pasted the data will we cleaned using the
* currently set paste method and inserted into the content.
*/
finishPaste: function() {
// If we're currently intercepting then finish that
if (this._intercepting) {
// Make sure we're not executing this method twice
if (!this._finishing) {
this._finishing = true;
/*if (SUI.browser.isGecko) {
//this._finishPaste(); < generates exception FF ??
this._div.blur();
} else */ if (SUI.browser.isIE) {
this._finishPasteIE();
} else {
this._finishPaste();
}
// free the locks
this._finishing = this._intercepting = false;
}
}
},
/**
* The listener method to use on the 'onbeforepaste' (IE) and 'onpaste'
* (others) event handlers to intercept the paste actions. If the paste
* method was set to 'html' the method returns true so that pasting will
* continue as default. In the case of 'text' or 'filtered' the data
* will be pasted to an offscreen div to process it by finishPaste later.
*/
interceptPaste: function() {
if (this._pasteMethod !== "html") {
// make sure we're not executing while not have completed
// _finishedPaste
if (!this._intercepting) {
this._intercepting = true;
if (SUI.browser.isIE) {
this._interceptPasteIE();
} else {
this._interceptPaste();
}
}
// prevent event propagation
return false;
} else {
// when used in scrivo event handling: true means propagate (i.e.
// behave normally)
return true;
}
},
/**
* Get or set the paste method to use when pasting.
* @param {String} [p] The desired method for pasting text: 'text',
* 'filtered' or 'html' (or none to use this method as a getter).
* @return {String} The current paste method setting (or null if this
* method was used as a setter).
*/
pasteMethod: function(p) {
return p !== undefined
? (this._pasteMethod = p) && null : this._pasteMethod;
},
/**
* The offscreen div to paste the data to.
* @type HTMLElementNode
* @private
*/
_div: null,
/**
* A reference to the editor object.
* @type SUI.control._HTMLEditControl
* @private
*/
_editor: null,
/**
* Flag to indicate that we're currently are busy with finishing a paste
* action.
* @type boolean
* @private
*/
_finishing: false,
/**
* Flag to indicate that we're currently are busy with intercepting a paste
* action and that there is data ready to be inserted.
* @type boolean
* @private
*/
_intercepting: false,
/**
* The paste method to use: 'text', 'filtered' or 'html'
* @type String
* @private
*/
_pasteMethod: "filtered",
/**
* The stored range (or bookmark in IE) to be able to restore the cursor
* position after the editor focus was moved to the offscreen div.
* @type Object
* @private
*/
_saveRange: null,
/**
* One of the scroll tops to save (and restore) if the document scroll
* offset changes when the helper div is created and removed.
* @type int
* @private
*/
_scrollTop1: 0,
/**
* One of the scroll tops to save (and restore) if the document scroll
* offset changes when the helper div is created and removed.
* @type int
* @private
*/
_scrollTop2: 0,
/**
* Set of allowed tags an allowed attributes for tag filtering.
* @type Object
* @private
*/
_tagFilter: {
"P": [],
"H1": [],
"H2": [],
"H3": [],
"H4": [],
"H5": [],
"H6": [],
"BR": [],
"HR": [],
"IMG": ["src", "align", "title", "alt"],
"A": ["href", "title"],
"B": [],
"U": [],
"I": [],
"SPAN": ["lang"],
"STRONG": [],
"EM": [],
"SMALL": [],
"BIG": [],
"CODE": [],
"PRE": [],
"TABLE": ["summary", "cellspacing"],
"TD": ["id", "headers", "axis"],
"TH": ["id", "headers", "axis"],
"TR": [],
"THEAD": [],
"TBODY": [],
"TFOOT": [],
"CAPTION": [],
"LI": [],
"UL": [],
"OL": [],
"DD": [],
"DT": [],
"DL": []
},
/**
* Set of tag that should be terminated with a line break when converted
* to plain text.
* @type Object
* @private
*/
_blockTags: {
"P": 1,
"H1": 1,
"H2": 1,
"H3": 1,
"H4": 1,
"H5": 1,
"H6": 1,
"BR": 1,
"HR": 1,
"PRE": 1,
"TABLE": 1,
"TR": 1,
"CAPTION": 1,
"LI": 1,
"DD": 1,
"DT": 1
},
/**
* Create an offscreen div as target for the paste actions so that we can
* access and process the data.
* @private
*/
_createPasteDiv: function() {
// create an offscreen contenteditable div
this._div = SUI.browser.createElement();
SUI.style.removeClass(this._div, "no-select");
SUI.style.setRect(this._div, 10, -110, 100, 100);
this._div.style.overflow = "hidden";
// If you do something, always do it with style ;)
this._div.style.backgroundColor = "#FFFFBB";
this._div.contentEditable = true;
//this._editor._editDoc.body.appendChild(this._div);
this._editor._editWin.frames.parent.document.body.appendChild(this._div);
},
/**
* Filter all unwanted tags and attributes from an HTML text string.
* @private
*/
_filter: function(txt, method) {
// Setup the filter DIV's ...
var a = document.createElement("DIV");
var b = document.createElement("DIV");
a.innerHTML = txt;
// ... and filter the tags.
this._filterTags(a, b);
// If "filtered" was selected, return the filtered result.
if (method == "filtered") {
return b.innerHTML;
}
// Else create plain text from the filtered HTML.
var res = {res: ""};
this._toText(b, res, false);
var makePs = true;
// Replace line end (with trailing line ends and spaces) ...
var txt = res.res.replace(/[\r\n][\s\r\n]*/g, makePs ? "<P>" : "");
// ... clean spaces around P tags ...
txt = txt.replace(/\s*<P>\s*/g, makePs ? "<P>" : "");
// ... remove double spaces.
return txt.replace(/\s+/g, " ");
},
/**
* When filtering (actually transfering) CSS class names keep the
* scrivo system styles.
* @param {HTMLElementNode} nd The target node.
* @param {String} cls The class name to strip the non scrivo class
* selectors from.
* @private
*/
_filterClass: function(nd, cls) {
if (!cls) {
return;
}
var c = cls.split(" ");
for (var i=0; i<c.length; i++) {
if (c[i]) {
if (c[i].substr(0, 4) == "sys_"
|| c[i].substr(0, 6) == "scrivo") {
SUI.style.addClass(nd, c[i]);
}
}
}
},
/**
* Copy an HTML DOM sub tree from a HTML element to an other empty
* element node. And while doing so forget all unwanted tags and
* attributes, thus in effect filter the tree.
* @param {HTMLElementNode} src An source node containing a an HTML
* fragment to filter.
* @param {HTMLElementNode} dest An empty target node.
* @private
*/
_filterTags: function(src, dest) {
var ignore = {"SCRIPT":true, "HEAD":true, "LINK":true, "STYLE":true,
"META": true, "TITLE": true};
// first check if src is not an element node ...
if(src.nodeType != 1 /*Element*/) {
// ... and if it is an text node ...
if (src.nodeType == 3 /*Text*/) {
// ... append the text to the destiny node.
dest.appendChild(document.createTextNode(src.nodeValue));
}
// ... else we're done (only possibility are comment nodes here)
return;
}
// ... node type is element node: get the tag name ...
var tg = this._tagFilter[src.tagName];
// ... and check if is allowed and copy it to dest ...
if(tg) {
/* TODO need to fix this, not very important, but a bug however
if (src.tagName == 'TABLE') {
replaceTableIds(edtWin.document, src);
}
*/
if (src.tagName == 'A' && src.name != "") {
// anchor is a special case in IE ...
if (SUI.browser.isIE && SUI.browser.version <= 8) {
var nwnd =
document.createElement("<A name='"+src.name+"'></A>");
} else {
var nwnd = document.createElement(src.tagName);
nwnd.name = src.name;
}
SUI.style.addClass(nwnd, "sys_anchor");
} else {
// ... else just create the tag.
var nwnd = document.createElement(src.tagName);
}
// now copy the attributes from the source to the destiny node
for (var i=0; i<tg.length; i++) {
var attr = src.getAttribute(tg[i]);
if (attr && (attr != "" || tg[i] == "alt")) {
nwnd.setAttribute(tg[i], attr);
}
}
// now copy scrivo system styles to the destiny node
this._filterClass(nwnd, src.className);
// append the newly created node to the destiny tree
if (nwnd.tagName == "IMG") {
// TODO what was the need for this clause
//if (nwnd.src.indexOf("baseUrl") != -1) {
dest.appendChild(nwnd);
dest = nwnd;
//}
} else {
dest.appendChild(nwnd);
dest = nwnd;
}
}
// recurse into the tree for all child nodes
for(var i=0; i<src.childNodes.length; i++) {
if (ignore[src.childNodes[i].tagName]) {
continue;
}
this._filterTags(src.childNodes[i], dest);
}
},
/**
* Convert HTML that was filtered using the _filterTags method to plain
* text.
* @param {HTMLElementNode} src An source node containing a an HTML
* fragment to filter.
* @param {object} dest An object containing a 'res' data member field.
* @param {bool} prev Value to indicate that the line break should be
* appended before the new text will be added.
* @private
*/
_toText: function(src, dest, prev) {
if (prev) {
dest.res += "\n";
}
// first check if src is not an element node ...
if(src.nodeType != 1 /*Element*/) {
// ... and if it is an text node ...
if (src.nodeType == 3 /*Text*/) {
// ... append the text to the destiny node.
dest.res += src.nodeValue.replace(/\r?\n/g, " ");
}
return;
}
// Recurse into the tree for all child nodes
for(var i=0; i<src.childNodes.length; i++) {
this._toText(src.childNodes[i], dest,
this._blockTags[src.childNodes[i].tagName]);
}
},
/**
* Finish a pending pasting action: filter out unwanted tags from the
* pasted data, remove the helper div and insert the cleaned data in
* the correct location of the document in the editor.
* @private
*/
_finishPaste: function() {
var html = this._filter(this._div.innerHTML, this._pasteMethod);
// ... remove the helper div ...
// this._div.contentEditable = false;
// this._editor._editDoc.body.removeChild(this._div);
this._editor._editWin.frames.parent.document.body.removeChild(this._div);
this._div = null;
if (SUI.browser.isGecko) {
// set the editor back to contenteditable ...
this._editor._editDoc.body.contentEditable = false;
this._editor._editDoc.body.contentEditable = true;
// ... and restore the selection
var s = this._editor._getSelection();
s.removeAllRanges();
s.addRange(this._saveRange);
this._saveRange = null;
}
// restore document scroll offsets
// this._editor._editDoc.body.scrollTop = this._scrollTop1;
// this._editor._editDoc.body.parentNode.scrollTop = this._scrollTop2;
// Now we can try to insert the cleaned html
try {
this._editor._execCommand("insertHtml", html);
} catch (e) {
alert(e.message);
}
this._editor._editDoc.body.focus();
},
/**
* Finish a pending pasting action: filter out unwanted tags from the
* pasted data, remove the helper div and insert the cleaned data in
* the correct location of the document in the editor. This method is
* of course specific to IE.
* @private
*/
_finishPasteIE: function() {
// ... get the data from the helper div using the selected
// cleaning method ...
var html = this._filter(this._div.innerHTML, this._pasteMethod);
// ... remove the helper div ...
//this._editor._editDoc.body.removeChild(this._div);
this._editor._editWin.frames.parent.document.body.removeChild(this._div);
this._div = null;
// ... and restore the selection
var rng = this._editor._ieRestoreBookmark(this._saveRange);
this._saveRange = null;
if (rng) {
rng.pasteHTML(html);
}
},
/**
* In order to manipulate the paste buffer we need to intercept the paste
* action. Therefore we create an offscreen contenteditable div that gets
* the focus over the editor's contenteditable. On a later moment we use
* 'finishPaste' to manipulate the pasted data and insert it into the
* editor.
* @private
*/
_interceptPaste: function() {
// save document scroll offsets
// this._scrollTop1 = this._editor._editDoc.body.scrollTop;
// this._scrollTop2 = this._editor._editDoc.body.parentNode.scrollTop;
// save the current selection
this._saveRange = this._editor._getCurrentRange();
this._saveRange =
this._editor._editWin.getSelection().getRangeAt(0);
// create an offscreen contenteditable div
this._createPasteDiv();
// this works on FF and chrome linux. On chrome window's I find that
// not the div but the document body receives the paste event. So
// for chrome you'll find an onpaste handler also.
var that = this;
SUI.browser.addEventListener(
this._div, "paste",
function () {
setTimeout(
function() {
that.finishPaste();
}
);
}
);
// ... and set the focus to the offscreen div, so that the data
// will be pasted into this div
this._div.focus();
var range = this._editor._editWin.frames.parent.document.createRange();
range.selectNode(this._div);
/*
// We'll raise an blur event on the paste event. This way we schedule
// the finishPaste excecution after the content was inserted into the
// hidden div.
var that = this;
SUI.browser.addEventListener(this._div, "blur",
function(e) {
setTimeout(
function() {
///that.finishPaste();
}
);
}
);
*/
},
/**
* In order to manipulate the paste buffer we need to intercept the paste
* action. Therefore we create an offscreen contenteditable div that gets
* the focus over the editor's contenteditable. On a later moment we use
* 'finishPaste' to manipulate the pasted data and insert it into the
* editor. This method is of course specific to IE.
* @private
*/
_interceptPasteIE: function() {
this._saveRange = this._editor._ieSaveBookmark();
// create an offscreen contenteditable div
this._createPasteDiv();
var that = this;
SUI.browser.addEventListener(this._div, "blur",
function(e) {
that.finishPaste();
}
);
var that = this;
SUI.browser.addEventListener(this._div, "paste",
function(e) {
setTimeout(
function() {
that._editor._editDoc.body.focus();
}
);
}
);
// ... and set the focus to the offscreen div, so that the data
// will be pasted into this div
this._div.focus();
//var r3 = this._editor._editDoc.body.createTextRange();
var r3 = this._editor._editWin.frames.parent.document.body.createTextRange();
r3.moveToElementText(this._div);
r3.select();
}
});