You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1034 lines
35 KiB
1034 lines
35 KiB
/*
|
|
YUI 3.17.2 (build 9c3c78e)
|
|
Copyright 2014 Yahoo! Inc. All rights reserved.
|
|
Licensed under the BSD License.
|
|
http://yuilibrary.com/license/
|
|
*/
|
|
|
|
YUI.add('editor-selection', function (Y, NAME) {
|
|
|
|
/**
|
|
* Wraps some common Selection/Range functionality into a simple object
|
|
* @class EditorSelection
|
|
* @constructor
|
|
* @module editor
|
|
* @submodule selection
|
|
*/
|
|
|
|
//TODO This shouldn't be there, Y.Node doesn't normalize getting textnode content.
|
|
var textContent = 'textContent',
|
|
INNER_HTML = 'innerHTML',
|
|
FONT_FAMILY = 'fontFamily';
|
|
|
|
if (Y.UA.ie && Y.UA.ie < 11) {
|
|
textContent = 'nodeValue';
|
|
}
|
|
|
|
Y.EditorSelection = function(domEvent) {
|
|
var sel, par, ieNode, nodes, rng, i,
|
|
comp, moved = 0, n, id, root = Y.EditorSelection.ROOT;
|
|
|
|
|
|
if (Y.config.win.getSelection && (!Y.UA.ie || Y.UA.ie < 9 || Y.UA.ie > 10)) {
|
|
sel = Y.config.win.getSelection();
|
|
} else if (Y.config.doc.selection) {
|
|
sel = Y.config.doc.selection.createRange();
|
|
}
|
|
this._selection = sel;
|
|
|
|
if (!sel) {
|
|
return false;
|
|
}
|
|
|
|
if (sel.pasteHTML) {
|
|
this.isCollapsed = (sel.compareEndPoints('StartToEnd', sel)) ? false : true;
|
|
if (this.isCollapsed) {
|
|
this.anchorNode = this.focusNode = Y.one(sel.parentElement());
|
|
|
|
if (domEvent) {
|
|
ieNode = Y.config.doc.elementFromPoint(domEvent.clientX, domEvent.clientY);
|
|
}
|
|
rng = sel.duplicate();
|
|
if (!ieNode) {
|
|
par = sel.parentElement();
|
|
nodes = par.childNodes;
|
|
|
|
for (i = 0; i < nodes.length; i++) {
|
|
//This causes IE to not allow a selection on a doubleclick
|
|
//rng.select(nodes[i]);
|
|
if (rng.inRange(sel)) {
|
|
if (!ieNode) {
|
|
ieNode = nodes[i];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
this.ieNode = ieNode;
|
|
|
|
if (ieNode) {
|
|
if (ieNode.nodeType !== 3) {
|
|
if (ieNode.firstChild) {
|
|
ieNode = ieNode.firstChild;
|
|
}
|
|
if (root.compareTo(ieNode)) {
|
|
if (ieNode.firstChild) {
|
|
ieNode = ieNode.firstChild;
|
|
}
|
|
}
|
|
}
|
|
this.anchorNode = this.focusNode = Y.EditorSelection.resolve(ieNode);
|
|
|
|
rng.moveToElementText(sel.parentElement());
|
|
comp = sel.compareEndPoints('StartToStart', rng);
|
|
if (comp) {
|
|
//We are not at the beginning of the selection.
|
|
//Setting the move to something large, may need to increase it later
|
|
moved = this.getEditorOffset(root);
|
|
sel.move('character', -(moved));
|
|
}
|
|
|
|
this.anchorOffset = this.focusOffset = moved;
|
|
|
|
this.anchorTextNode = this.focusTextNode = Y.one(ieNode);
|
|
}
|
|
|
|
|
|
} else {
|
|
//This helps IE deal with a selection and nodeChange events
|
|
if (sel.htmlText && sel.htmlText !== '') {
|
|
n = Y.Node.create(sel.htmlText);
|
|
if (n && n.get('id')) {
|
|
id = n.get('id');
|
|
this.anchorNode = this.focusNode = Y.one('#' + id);
|
|
} else if (n) {
|
|
n = n.get('childNodes');
|
|
this.anchorNode = this.focusNode = n.item(0);
|
|
}
|
|
}
|
|
}
|
|
|
|
//var self = this;
|
|
//debugger;
|
|
} else {
|
|
this.isCollapsed = sel.isCollapsed;
|
|
this.anchorNode = Y.EditorSelection.resolve(sel.anchorNode);
|
|
this.focusNode = Y.EditorSelection.resolve(sel.focusNode);
|
|
this.anchorOffset = sel.anchorOffset;
|
|
this.focusOffset = sel.focusOffset;
|
|
|
|
this.anchorTextNode = Y.one(sel.anchorNode || this.anchorNode);
|
|
this.focusTextNode = Y.one(sel.focusNode || this.focusNode);
|
|
}
|
|
if (Y.Lang.isString(sel.text)) {
|
|
this.text = sel.text;
|
|
} else {
|
|
if (sel.toString) {
|
|
this.text = sel.toString();
|
|
} else {
|
|
this.text = '';
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Utility method to remove dead font-family styles from an element.
|
|
* @static
|
|
* @method removeFontFamily
|
|
*/
|
|
Y.EditorSelection.removeFontFamily = function(n) {
|
|
n.removeAttribute('face');
|
|
var s = n.getAttribute('style').toLowerCase();
|
|
if (s === '' || (s === 'font-family: ')) {
|
|
n.removeAttribute('style');
|
|
}
|
|
if (s.match(Y.EditorSelection.REG_FONTFAMILY)) {
|
|
s = s.replace(Y.EditorSelection.REG_FONTFAMILY, '');
|
|
n.setAttribute('style', s);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Performs a prefilter on all nodes in the editor. Looks for nodes with a style: fontFamily or font face
|
|
* It then creates a dynamic class assigns it and removed the property. This is so that we don't lose
|
|
* the fontFamily when selecting nodes.
|
|
* @static
|
|
* @method filter
|
|
*/
|
|
Y.EditorSelection.filter = function(blocks) {
|
|
|
|
var startTime = (new Date()).getTime(),
|
|
editorSelection = Y.EditorSelection,
|
|
root = editorSelection.ROOT,
|
|
endTime,
|
|
nodes = root.all(editorSelection.ALL),
|
|
baseNodes = root.all('strong,em'),
|
|
doc = Y.config.doc, hrs,
|
|
classNames = {}, cssString = '',
|
|
ls, startTime1 = (new Date()).getTime(),
|
|
endTime1;
|
|
|
|
nodes.each(function(n) {
|
|
var raw = Y.Node.getDOMNode(n);
|
|
if (raw.style[FONT_FAMILY]) {
|
|
classNames['.' + n._yuid] = raw.style[FONT_FAMILY];
|
|
n.addClass(n._yuid);
|
|
|
|
editorSelection.removeFontFamily(raw);
|
|
}
|
|
});
|
|
endTime1 = (new Date()).getTime();
|
|
|
|
root.all('.hr').addClass('yui-skip').addClass('yui-non');
|
|
|
|
if (Y.UA.ie) {
|
|
hrs = Y.Node.getDOMNode(root).getElementsByTagName('hr');
|
|
Y.each(hrs, function(hr) {
|
|
var el = doc.createElement('div'),
|
|
s = el.style;
|
|
|
|
el.className = 'hr yui-non yui-skip';
|
|
|
|
el.setAttribute('readonly', true);
|
|
el.setAttribute('contenteditable', false); //Keep it from being Edited
|
|
if (hr.parentNode) {
|
|
hr.parentNode.replaceChild(el, hr);
|
|
}
|
|
//Had to move to inline style. writes for ie's < 8. They don't render el.setAttribute('style');
|
|
s.border = '1px solid #ccc';
|
|
s.lineHeight = '0';
|
|
s.height = '0';
|
|
s.fontSize = '0';
|
|
s.marginTop = '5px';
|
|
s.marginBottom = '5px';
|
|
s.marginLeft = '0px';
|
|
s.marginRight = '0px';
|
|
s.padding = '0';
|
|
});
|
|
}
|
|
|
|
|
|
Y.each(classNames, function(v, k) {
|
|
cssString += k + ' { font-family: ' + v.replace(/"/gi, '') + '; }';
|
|
});
|
|
Y.StyleSheet(cssString, 'editor');
|
|
|
|
|
|
//Not sure about this one?
|
|
baseNodes.each(function(n, k) {
|
|
var t = n.get('tagName').toLowerCase(),
|
|
newTag = 'i';
|
|
if (t === 'strong') {
|
|
newTag = 'b';
|
|
}
|
|
editorSelection.prototype._swap(baseNodes.item(k), newTag);
|
|
});
|
|
|
|
//Filter out all the empty UL/OL's
|
|
ls = root.all('ol,ul');
|
|
ls.each(function(v) {
|
|
var lis = v.all('li');
|
|
if (!lis.size()) {
|
|
v.remove();
|
|
}
|
|
});
|
|
|
|
if (blocks) {
|
|
editorSelection.filterBlocks();
|
|
}
|
|
endTime = (new Date()).getTime();
|
|
};
|
|
|
|
/**
|
|
* Method attempts to replace all "orphined" text nodes in the main body by wrapping them with a <p>. Called from filter.
|
|
* @static
|
|
* @method filterBlocks
|
|
*/
|
|
Y.EditorSelection.filterBlocks = function() {
|
|
var startTime = (new Date()).getTime(), endTime,
|
|
childs = Y.Node.getDOMNode(Y.EditorSelection.ROOT).childNodes, i, node, wrapped = false, doit = true,
|
|
sel, single, br, c, s, html;
|
|
|
|
if (childs) {
|
|
for (i = 0; i < childs.length; i++) {
|
|
node = Y.one(childs[i]);
|
|
if (!node.test(Y.EditorSelection.BLOCKS)) {
|
|
doit = true;
|
|
if (childs[i].nodeType === 3) {
|
|
c = childs[i][textContent].match(Y.EditorSelection.REG_CHAR);
|
|
s = childs[i][textContent].match(Y.EditorSelection.REG_NON);
|
|
if (c === null && s) {
|
|
doit = false;
|
|
|
|
}
|
|
}
|
|
if (doit) {
|
|
if (!wrapped) {
|
|
wrapped = [];
|
|
}
|
|
wrapped.push(childs[i]);
|
|
}
|
|
} else {
|
|
wrapped = Y.EditorSelection._wrapBlock(wrapped);
|
|
}
|
|
}
|
|
wrapped = Y.EditorSelection._wrapBlock(wrapped);
|
|
}
|
|
|
|
single = Y.all(Y.EditorSelection.DEFAULT_BLOCK_TAG);
|
|
if (single.size() === 1) {
|
|
br = single.item(0).all('br');
|
|
if (br.size() === 1) {
|
|
if (!br.item(0).test('.yui-cursor')) {
|
|
br.item(0).remove();
|
|
}
|
|
html = single.item(0).get('innerHTML');
|
|
if (html === '' || html === ' ') {
|
|
single.set('innerHTML', Y.EditorSelection.CURSOR);
|
|
sel = new Y.EditorSelection();
|
|
sel.focusCursor(true, true);
|
|
}
|
|
if (br.item(0).test('.yui-cursor') && Y.UA.ie) {
|
|
br.item(0).remove();
|
|
}
|
|
}
|
|
} else {
|
|
single.each(function(p) {
|
|
var html = p.get('innerHTML');
|
|
if (html === '') {
|
|
p.remove();
|
|
}
|
|
});
|
|
}
|
|
|
|
endTime = (new Date()).getTime();
|
|
};
|
|
|
|
/**
|
|
* Regular Expression used to find dead font-family styles
|
|
* @static
|
|
* @property REG_FONTFAMILY
|
|
*/
|
|
Y.EditorSelection.REG_FONTFAMILY = /font-family:\s*;/;
|
|
|
|
/**
|
|
* Regular Expression to determine if a string has a character in it
|
|
* @static
|
|
* @property REG_CHAR
|
|
*/
|
|
Y.EditorSelection.REG_CHAR = /[a-zA-Z-0-9_!@#\$%\^&*\(\)-=_+\[\]\\{}|;':",.\/<>\?]/gi;
|
|
|
|
/**
|
|
* Regular Expression to determine if a string has a non-character in it
|
|
* @static
|
|
* @property REG_NON
|
|
*/
|
|
Y.EditorSelection.REG_NON = /[\s|\n|\t]/gi;
|
|
|
|
/**
|
|
* Regular Expression to remove all HTML from a string
|
|
* @static
|
|
* @property REG_NOHTML
|
|
*/
|
|
Y.EditorSelection.REG_NOHTML = /<\S[^><]*>/g;
|
|
|
|
|
|
/**
|
|
* Wraps an array of elements in a Block level tag
|
|
* @static
|
|
* @private
|
|
* @method _wrapBlock
|
|
*/
|
|
Y.EditorSelection._wrapBlock = function(wrapped) {
|
|
if (wrapped) {
|
|
var newChild = Y.Node.create('<' + Y.EditorSelection.DEFAULT_BLOCK_TAG + '></' + Y.EditorSelection.DEFAULT_BLOCK_TAG + '>'),
|
|
firstChild = Y.one(wrapped[0]), i;
|
|
|
|
for (i = 1; i < wrapped.length; i++) {
|
|
newChild.append(wrapped[i]);
|
|
}
|
|
firstChild.replace(newChild);
|
|
newChild.prepend(firstChild);
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Undoes what filter does enough to return the HTML from the Editor, then re-applies the filter.
|
|
* @static
|
|
* @method unfilter
|
|
* @return {String} The filtered HTML
|
|
*/
|
|
Y.EditorSelection.unfilter = function() {
|
|
var root = Y.EditorSelection.ROOT,
|
|
nodes = root.all('[class]'),
|
|
html = '', nons, ids,
|
|
body = root;
|
|
|
|
|
|
nodes.each(function(n) {
|
|
if (n.hasClass(n._yuid)) {
|
|
//One of ours
|
|
n.setStyle(FONT_FAMILY, n.getStyle(FONT_FAMILY));
|
|
n.removeClass(n._yuid);
|
|
if (n.getAttribute('class') === '') {
|
|
n.removeAttribute('class');
|
|
}
|
|
}
|
|
});
|
|
|
|
nons = root.all('.yui-non');
|
|
nons.each(function(n) {
|
|
if (!n.hasClass('yui-skip') && n.get('innerHTML') === '') {
|
|
n.remove();
|
|
} else {
|
|
n.removeClass('yui-non').removeClass('yui-skip');
|
|
}
|
|
});
|
|
|
|
ids = root.all('[id]');
|
|
ids.each(function(n) {
|
|
if (n.get('id').indexOf('yui_3_') === 0) {
|
|
n.removeAttribute('id');
|
|
n.removeAttribute('_yuid');
|
|
}
|
|
});
|
|
|
|
if (body) {
|
|
html = body.get('innerHTML');
|
|
}
|
|
|
|
root.all('.hr').addClass('yui-skip').addClass('yui-non');
|
|
|
|
/*
|
|
nodes.each(function(n) {
|
|
n.addClass(n._yuid);
|
|
n.setStyle(FONT_FAMILY, '');
|
|
if (n.getAttribute('style') === '') {
|
|
n.removeAttribute('style');
|
|
}
|
|
});
|
|
*/
|
|
|
|
return html;
|
|
};
|
|
/**
|
|
* Resolve a node from the selection object and return a Node instance
|
|
* @static
|
|
* @method resolve
|
|
* @param {HTMLElement} n The HTMLElement to resolve. Might be a TextNode, gives parentNode.
|
|
* @return {Node} The Resolved node
|
|
*/
|
|
Y.EditorSelection.resolve = function(n) {
|
|
if (!n) {
|
|
return Y.EditorSelection.ROOT;
|
|
}
|
|
|
|
if (n && n.nodeType === 3) {
|
|
//Adding a try/catch here because in rare occasions IE will
|
|
//Throw a error accessing the parentNode of a stranded text node.
|
|
//In the case of Ctrl+Z (Undo)
|
|
try {
|
|
n = n.parentNode;
|
|
} catch (re) {
|
|
n = Y.EditorSelection.ROOT;
|
|
}
|
|
}
|
|
return Y.one(n);
|
|
};
|
|
|
|
/**
|
|
* Returns the innerHTML of a node with all HTML tags removed.
|
|
* @static
|
|
* @method getText
|
|
* @param {Node} node The Node instance to remove the HTML from
|
|
* @return {String} The string of text
|
|
*/
|
|
Y.EditorSelection.getText = function(node) {
|
|
var txt = node.get('innerHTML').replace(Y.EditorSelection.REG_NOHTML, '');
|
|
//Clean out the cursor subs to see if the Node is empty
|
|
txt = txt.replace('<span><br></span>', '').replace('<br>', '');
|
|
return txt;
|
|
};
|
|
|
|
//Y.EditorSelection.DEFAULT_BLOCK_TAG = 'div';
|
|
Y.EditorSelection.DEFAULT_BLOCK_TAG = 'p';
|
|
|
|
/**
|
|
* The selector to use when looking for Nodes to cache the value of: [style],font[face]
|
|
* @static
|
|
* @property ALL
|
|
*/
|
|
Y.EditorSelection.ALL = '[style],font[face]';
|
|
|
|
/**
|
|
* The selector to use when looking for block level items.
|
|
* @static
|
|
* @property BLOCKS
|
|
*/
|
|
Y.EditorSelection.BLOCKS = 'p,div,ul,ol,table,style';
|
|
/**
|
|
* The temporary fontname applied to a selection to retrieve their values: yui-tmp
|
|
* @static
|
|
* @property TMP
|
|
*/
|
|
Y.EditorSelection.TMP = 'yui-tmp';
|
|
/**
|
|
* The default tag to use when creating elements: span
|
|
* @static
|
|
* @property DEFAULT_TAG
|
|
*/
|
|
Y.EditorSelection.DEFAULT_TAG = 'span';
|
|
|
|
/**
|
|
* The id of the outer cursor wrapper
|
|
* @static
|
|
* @property CURID
|
|
*/
|
|
Y.EditorSelection.CURID = 'yui-cursor';
|
|
|
|
/**
|
|
* The id used to wrap the inner space of the cursor position
|
|
* @static
|
|
* @property CUR_WRAPID
|
|
*/
|
|
Y.EditorSelection.CUR_WRAPID = 'yui-cursor-wrapper';
|
|
|
|
/**
|
|
* The default HTML used to focus the cursor..
|
|
* @static
|
|
* @property CURSOR
|
|
*/
|
|
Y.EditorSelection.CURSOR = '<span><br class="yui-cursor"></span>';
|
|
|
|
/**
|
|
* The default HTML element from which data will be retrieved. Default: body
|
|
* @static
|
|
* @property ROOT
|
|
*/
|
|
Y.EditorSelection.ROOT = Y.one('body');
|
|
|
|
Y.EditorSelection.hasCursor = function() {
|
|
var cur = Y.all('#' + Y.EditorSelection.CUR_WRAPID);
|
|
return cur.size();
|
|
};
|
|
|
|
/**
|
|
* Called from Editor keydown to remove the "extra" space before the cursor.
|
|
* @static
|
|
* @method cleanCursor
|
|
*/
|
|
Y.EditorSelection.cleanCursor = function() {
|
|
var cur, sel = 'br.yui-cursor';
|
|
cur = Y.all(sel);
|
|
if (cur.size()) {
|
|
cur.each(function(b) {
|
|
var c = b.get('parentNode.parentNode.childNodes'), html;
|
|
if (c.size()) {
|
|
b.remove();
|
|
} else {
|
|
html = Y.EditorSelection.getText(c.item(0));
|
|
if (html !== '') {
|
|
b.remove();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
/*
|
|
var cur = Y.all('#' + Y.EditorSelection.CUR_WRAPID);
|
|
if (cur.size()) {
|
|
cur.each(function(c) {
|
|
var html = c.get('innerHTML');
|
|
if (html == ' ' || html == '<br>') {
|
|
if (c.previous() || c.next()) {
|
|
c.remove();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
*/
|
|
};
|
|
|
|
Y.EditorSelection.prototype = {
|
|
/**
|
|
* Range text value
|
|
* @property text
|
|
* @type String
|
|
*/
|
|
text: null,
|
|
/**
|
|
* Flag to show if the range is collapsed or not
|
|
* @property isCollapsed
|
|
* @type Boolean
|
|
*/
|
|
isCollapsed: null,
|
|
/**
|
|
* A Node instance of the parentNode of the anchorNode of the range
|
|
* @property anchorNode
|
|
* @type Node
|
|
*/
|
|
anchorNode: null,
|
|
/**
|
|
* The offset from the range object
|
|
* @property anchorOffset
|
|
* @type Number
|
|
*/
|
|
anchorOffset: null,
|
|
/**
|
|
* A Node instance of the actual textNode of the range.
|
|
* @property anchorTextNode
|
|
* @type Node
|
|
*/
|
|
anchorTextNode: null,
|
|
/**
|
|
* A Node instance of the parentNode of the focusNode of the range
|
|
* @property focusNode
|
|
* @type Node
|
|
*/
|
|
focusNode: null,
|
|
/**
|
|
* The offset from the range object
|
|
* @property focusOffset
|
|
* @type Number
|
|
*/
|
|
focusOffset: null,
|
|
/**
|
|
* A Node instance of the actual textNode of the range.
|
|
* @property focusTextNode
|
|
* @type Node
|
|
*/
|
|
focusTextNode: null,
|
|
/**
|
|
* The actual Selection/Range object
|
|
* @property _selection
|
|
* @private
|
|
*/
|
|
_selection: null,
|
|
/**
|
|
* Wrap an element, with another element
|
|
* @private
|
|
* @method _wrap
|
|
* @param {HTMLElement} n The node to wrap
|
|
* @param {String} tag The tag to use when creating the new element.
|
|
* @return {HTMLElement} The wrapped node
|
|
*/
|
|
_wrap: function(n, tag) {
|
|
var tmp = Y.Node.create('<' + tag + '></' + tag + '>');
|
|
tmp.set(INNER_HTML, n.get(INNER_HTML));
|
|
n.set(INNER_HTML, '');
|
|
n.append(tmp);
|
|
return Y.Node.getDOMNode(tmp);
|
|
},
|
|
/**
|
|
* Swap an element, with another element
|
|
* @private
|
|
* @method _swap
|
|
* @param {HTMLElement} n The node to swap
|
|
* @param {String} tag The tag to use when creating the new element.
|
|
* @return {HTMLElement} The new node
|
|
*/
|
|
_swap: function(n, tag) {
|
|
var tmp = Y.Node.create('<' + tag + '></' + tag + '>');
|
|
tmp.set(INNER_HTML, n.get(INNER_HTML));
|
|
n.replace(tmp, n);
|
|
return Y.Node.getDOMNode(tmp);
|
|
},
|
|
/**
|
|
* Get all the nodes in the current selection. This method will actually perform a filter first.
|
|
* Then it calls doc.execCommand('fontname', null, 'yui-tmp') to touch all nodes in the selection.
|
|
* The it compiles a list of all nodes affected by the execCommand and builds a NodeList to return.
|
|
* @method getSelected
|
|
* @return {NodeList} A NodeList of all items in the selection.
|
|
*/
|
|
getSelected: function() {
|
|
var editorSelection = Y.EditorSelection,
|
|
root = editorSelection.ROOT,
|
|
nodes,
|
|
items = [];
|
|
|
|
editorSelection.filter();
|
|
Y.config.doc.execCommand('fontname', null, editorSelection.TMP);
|
|
nodes = root.all(editorSelection.ALL);
|
|
|
|
nodes.each(function(n, k) {
|
|
if (n.getStyle(FONT_FAMILY) === editorSelection.TMP) {
|
|
n.setStyle(FONT_FAMILY, '');
|
|
editorSelection.removeFontFamily(n);
|
|
if (!n.compareTo(root)) {
|
|
items.push(Y.Node.getDOMNode(nodes.item(k)));
|
|
}
|
|
}
|
|
});
|
|
return Y.all(items);
|
|
},
|
|
/**
|
|
* Insert HTML at the current cursor position and return a Node instance of the newly inserted element.
|
|
* @method insertContent
|
|
* @param {String} html The HTML to insert.
|
|
* @return {Node} The inserted Node.
|
|
*/
|
|
insertContent: function(html) {
|
|
return this.insertAtCursor(html, this.anchorTextNode, this.anchorOffset, true);
|
|
},
|
|
/**
|
|
* Insert HTML at the current cursor position, this method gives you control over the text node to insert into and the offset where to put it.
|
|
* @method insertAtCursor
|
|
* @param {String} html The HTML to insert.
|
|
* @param {Node} node The text node to break when inserting.
|
|
* @param {Number} offset The left offset of the text node to break and insert the new content.
|
|
* @param {Boolean} collapse Should the range be collapsed after insertion. default: false
|
|
* @return {Node} The inserted Node.
|
|
*/
|
|
insertAtCursor: function(html, node, offset, collapse) {
|
|
var cur = Y.Node.create('<' + Y.EditorSelection.DEFAULT_TAG + ' class="yui-non"></' + Y.EditorSelection.DEFAULT_TAG + '>'),
|
|
inHTML, txt, txt2, newNode, range = this.createRange(), b, root = Y.EditorSelection.ROOT;
|
|
|
|
if (root.compareTo(node)) {
|
|
b = Y.Node.create('<span></span>');
|
|
node.append(b);
|
|
node = b;
|
|
}
|
|
|
|
|
|
if (range.pasteHTML) {
|
|
if (offset === 0 && node && !node.previous() && node.get('nodeType') === 3) {
|
|
/*
|
|
* For some strange reason, range.pasteHTML fails if the node is a textNode and
|
|
* the offset is 0. (The cursor is at the beginning of the line)
|
|
* It will always insert the new content at position 1 instead of
|
|
* position 0. Here we test for that case and do it the hard way.
|
|
*/
|
|
node.insert(html, 'before');
|
|
if (range.moveToElementText) {
|
|
range.moveToElementText(Y.Node.getDOMNode(node.previous()));
|
|
}
|
|
//Move the cursor after the new node
|
|
range.collapse(false);
|
|
range.select();
|
|
return node.previous();
|
|
} else {
|
|
newNode = Y.Node.create(html);
|
|
try {
|
|
range.pasteHTML('<span id="rte-insert"></span>');
|
|
} catch (e) {}
|
|
inHTML = root.one('#rte-insert');
|
|
if (inHTML) {
|
|
inHTML.set('id', '');
|
|
inHTML.replace(newNode);
|
|
if (range.moveToElementText) {
|
|
range.moveToElementText(Y.Node.getDOMNode(newNode));
|
|
}
|
|
range.collapse(false);
|
|
range.select();
|
|
return newNode;
|
|
} else {
|
|
Y.on('available', function() {
|
|
inHTML.set('id', '');
|
|
inHTML.replace(newNode);
|
|
if (range.moveToElementText) {
|
|
range.moveToElementText(Y.Node.getDOMNode(newNode));
|
|
}
|
|
range.collapse(false);
|
|
range.select();
|
|
}, '#rte-insert');
|
|
}
|
|
}
|
|
} else {
|
|
//TODO using Y.Node.create here throws warnings & strips first white space character
|
|
//txt = Y.one(Y.Node.create(inHTML.substr(0, offset)));
|
|
//txt2 = Y.one(Y.Node.create(inHTML.substr(offset)));
|
|
if (offset > 0) {
|
|
inHTML = node.get(textContent);
|
|
|
|
txt = Y.one(Y.config.doc.createTextNode(inHTML.substr(0, offset)));
|
|
txt2 = Y.one(Y.config.doc.createTextNode(inHTML.substr(offset)));
|
|
|
|
node.replace(txt, node);
|
|
newNode = Y.Node.create(html);
|
|
if (newNode.get('nodeType') === 11) {
|
|
b = Y.Node.create('<span></span>');
|
|
b.append(newNode);
|
|
newNode = b;
|
|
}
|
|
txt.insert(newNode, 'after');
|
|
//if (txt2 && txt2.get('length')) {
|
|
if (txt2) {
|
|
newNode.insert(cur, 'after');
|
|
cur.insert(txt2, 'after');
|
|
this.selectNode(cur, collapse);
|
|
}
|
|
} else {
|
|
if (node.get('nodeType') === 3) {
|
|
node = node.get('parentNode') || root;
|
|
}
|
|
newNode = Y.Node.create(html);
|
|
html = node.get('innerHTML').replace(/\n/gi, '');
|
|
if (html === '' || html === '<br>') {
|
|
node.append(newNode);
|
|
} else {
|
|
if (newNode.get('parentNode')) {
|
|
node.insert(newNode, 'before');
|
|
} else {
|
|
root.prepend(newNode);
|
|
}
|
|
}
|
|
if (node.get('firstChild').test('br')) {
|
|
node.get('firstChild').remove();
|
|
}
|
|
}
|
|
}
|
|
return newNode;
|
|
},
|
|
/**
|
|
* Get all elements inside a selection and wrap them with a new element and return a NodeList of all elements touched.
|
|
* @method wrapContent
|
|
* @param {String} tag The tag to wrap all selected items with.
|
|
* @return {NodeList} A NodeList of all items in the selection.
|
|
*/
|
|
wrapContent: function(tag) {
|
|
tag = (tag) ? tag : Y.EditorSelection.DEFAULT_TAG;
|
|
|
|
if (!this.isCollapsed) {
|
|
var items = this.getSelected(),
|
|
changed = [], range, last, first, range2;
|
|
|
|
items.each(function(n, k) {
|
|
var t = n.get('tagName').toLowerCase();
|
|
if (t === 'font') {
|
|
changed.push(this._swap(items.item(k), tag));
|
|
} else {
|
|
changed.push(this._wrap(items.item(k), tag));
|
|
}
|
|
}, this);
|
|
|
|
range = this.createRange();
|
|
first = changed[0];
|
|
last = changed[changed.length - 1];
|
|
if (this._selection.removeAllRanges) {
|
|
range.setStart(changed[0], 0);
|
|
range.setEnd(last, last.childNodes.length);
|
|
this._selection.removeAllRanges();
|
|
this._selection.addRange(range);
|
|
} else {
|
|
if (range.moveToElementText) {
|
|
range.moveToElementText(Y.Node.getDOMNode(first));
|
|
range2 = this.createRange();
|
|
range2.moveToElementText(Y.Node.getDOMNode(last));
|
|
range.setEndPoint('EndToEnd', range2);
|
|
}
|
|
range.select();
|
|
}
|
|
|
|
changed = Y.all(changed);
|
|
return changed;
|
|
|
|
|
|
} else {
|
|
return Y.all([]);
|
|
}
|
|
},
|
|
/**
|
|
* Find and replace a string inside a text node and replace it with HTML focusing the node after
|
|
* to allow you to continue to type.
|
|
* @method replace
|
|
* @param {String} se The string to search for.
|
|
* @param {String} re The string of HTML to replace it with.
|
|
* @return {Node} The node inserted.
|
|
*/
|
|
replace: function(se,re) {
|
|
var range = this.createRange(), node, txt, index, newNode;
|
|
|
|
if (range.getBookmark) {
|
|
index = range.getBookmark();
|
|
txt = this.anchorNode.get('innerHTML').replace(se, re);
|
|
this.anchorNode.set('innerHTML', txt);
|
|
range.moveToBookmark(index);
|
|
newNode = Y.one(range.parentElement());
|
|
} else {
|
|
node = this.anchorTextNode;
|
|
txt = node.get(textContent);
|
|
index = txt.indexOf(se);
|
|
|
|
txt = txt.replace(se, '');
|
|
node.set(textContent, txt);
|
|
newNode = this.insertAtCursor(re, node, index, true);
|
|
}
|
|
return newNode;
|
|
},
|
|
/**
|
|
* Destroy the range.
|
|
* @method remove
|
|
* @chainable
|
|
* @return {EditorSelection}
|
|
*/
|
|
remove: function() {
|
|
if (this._selection && this._selection.removeAllRanges) {
|
|
this._selection.removeAllRanges();
|
|
}
|
|
return this;
|
|
},
|
|
/**
|
|
* Wrapper for the different range creation methods.
|
|
* @method createRange
|
|
* @return {Range}
|
|
*/
|
|
createRange: function() {
|
|
if (Y.config.doc.selection) {
|
|
return Y.config.doc.selection.createRange();
|
|
} else {
|
|
return Y.config.doc.createRange();
|
|
}
|
|
},
|
|
/**
|
|
* Select a Node (hilighting it).
|
|
* @method selectNode
|
|
* @param {Node} node The node to select
|
|
* @param {Boolean} collapse Should the range be collapsed after insertion. default: false
|
|
* @chainable
|
|
* @return {EditorSelection}
|
|
*/
|
|
selectNode: function(node, collapse, end) {
|
|
if (!node) {
|
|
return;
|
|
}
|
|
end = end || 0;
|
|
node = Y.Node.getDOMNode(node);
|
|
var range = this.createRange();
|
|
if (range.selectNode) {
|
|
try {
|
|
range.selectNode(node);
|
|
} catch (err) {
|
|
// Ignore selection errors like INVALID_NODE_TYPE_ERR
|
|
}
|
|
this._selection.removeAllRanges();
|
|
this._selection.addRange(range);
|
|
if (collapse) {
|
|
try {
|
|
this._selection.collapse(node, end);
|
|
} catch (err) {
|
|
this._selection.collapse(node, 0);
|
|
}
|
|
}
|
|
} else {
|
|
if (node.nodeType === 3) {
|
|
node = node.parentNode;
|
|
}
|
|
try {
|
|
range.moveToElementText(node);
|
|
} catch(e) {}
|
|
if (collapse) {
|
|
range.collapse(((end) ? false : true));
|
|
}
|
|
range.select();
|
|
}
|
|
return this;
|
|
},
|
|
/**
|
|
* Put a placeholder in the DOM at the current cursor position.
|
|
* @method setCursor
|
|
* @return {Node}
|
|
*/
|
|
setCursor: function() {
|
|
this.removeCursor(false);
|
|
return this.insertContent(Y.EditorSelection.CURSOR);
|
|
},
|
|
/**
|
|
* Get the placeholder in the DOM at the current cursor position.
|
|
* @method getCursor
|
|
* @return {Node}
|
|
*/
|
|
getCursor: function() {
|
|
return Y.EditorSelection.ROOT.all('.' + Y.EditorSelection.CURID);
|
|
},
|
|
/**
|
|
* Remove the cursor placeholder from the DOM.
|
|
* @method removeCursor
|
|
* @param {Boolean} keep Setting this to true will keep the node, but remove the unique parts that make it the cursor.
|
|
* @return {Node}
|
|
*/
|
|
removeCursor: function(keep) {
|
|
var cur = this.getCursor();
|
|
if (cur) {
|
|
if (keep) {
|
|
cur.removeAttribute('id');
|
|
cur.set('innerHTML', '<br class="yui-cursor">');
|
|
} else {
|
|
cur.remove();
|
|
}
|
|
}
|
|
return cur;
|
|
},
|
|
/**
|
|
* Gets a stored cursor and focuses it for editing, must be called sometime after setCursor
|
|
* @method focusCursor
|
|
* @return {Node}
|
|
*/
|
|
focusCursor: function(collapse, end) {
|
|
if (collapse !== false) {
|
|
collapse = true;
|
|
}
|
|
if (end !== false) {
|
|
end = true;
|
|
}
|
|
var cur = this.removeCursor(true);
|
|
if (cur) {
|
|
cur.each(function(c) {
|
|
this.selectNode(c, collapse, end);
|
|
}, this);
|
|
}
|
|
},
|
|
/**
|
|
* Generic toString for logging.
|
|
* @method toString
|
|
* @return {String}
|
|
*/
|
|
toString: function() {
|
|
return 'EditorSelection Object';
|
|
},
|
|
|
|
/**
|
|
Gets the offset of the selection for the selection within the current
|
|
editor
|
|
@public
|
|
@method getEditorOffset
|
|
@param {Y.Node} [node] Element used to measure the offset to
|
|
@return Number Number of characters the selection is from the beginning
|
|
@since 3.13.0
|
|
*/
|
|
getEditorOffset: function(node) {
|
|
var container = (node || Y.EditorSelection.ROOT).getDOMNode(),
|
|
caretOffset = 0,
|
|
doc = Y.config.doc,
|
|
win = Y.config.win,
|
|
sel,
|
|
range,
|
|
preCaretRange;
|
|
|
|
if (typeof win.getSelection !== "undefined") {
|
|
range = win.getSelection().getRangeAt(0);
|
|
preCaretRange = range.cloneRange();
|
|
preCaretRange.selectNodeContents(container);
|
|
preCaretRange.setEnd(range.endContainer, range.endOffset);
|
|
caretOffset = preCaretRange.toString().length;
|
|
} else {
|
|
sel = doc.selection;
|
|
|
|
if ( sel && sel.type !== "Control") {
|
|
range = sel.createRange();
|
|
preCaretRange = doc.body.createTextRange();
|
|
preCaretRange.moveToElementText(container);
|
|
preCaretRange.setEndPoint("EndToEnd", range);
|
|
caretOffset = preCaretRange.text.length;
|
|
}
|
|
}
|
|
|
|
return caretOffset;
|
|
}
|
|
};
|
|
|
|
//TODO Remove this alias in 3.6.0
|
|
Y.Selection = Y.EditorSelection;
|
|
|
|
|
|
|
|
}, '3.17.2', {"requires": ["node"]});
|
|
|