1 |
efrain |
1 |
YUI.add('editor-selection', function (Y, NAME) {
|
|
|
2 |
|
|
|
3 |
/**
|
|
|
4 |
* Wraps some common Selection/Range functionality into a simple object
|
|
|
5 |
* @class EditorSelection
|
|
|
6 |
* @constructor
|
|
|
7 |
* @module editor
|
|
|
8 |
* @submodule selection
|
|
|
9 |
*/
|
|
|
10 |
|
|
|
11 |
//TODO This shouldn't be there, Y.Node doesn't normalize getting textnode content.
|
|
|
12 |
var textContent = 'textContent',
|
|
|
13 |
INNER_HTML = 'innerHTML',
|
|
|
14 |
FONT_FAMILY = 'fontFamily';
|
|
|
15 |
|
|
|
16 |
if (Y.UA.ie && Y.UA.ie < 11) {
|
|
|
17 |
textContent = 'nodeValue';
|
|
|
18 |
}
|
|
|
19 |
|
|
|
20 |
Y.EditorSelection = function(domEvent) {
|
|
|
21 |
var sel, par, ieNode, nodes, rng, i,
|
|
|
22 |
comp, moved = 0, n, id, root = Y.EditorSelection.ROOT;
|
|
|
23 |
|
|
|
24 |
|
|
|
25 |
if (Y.config.win.getSelection && (!Y.UA.ie || Y.UA.ie < 9 || Y.UA.ie > 10)) {
|
|
|
26 |
sel = Y.config.win.getSelection();
|
|
|
27 |
} else if (Y.config.doc.selection) {
|
|
|
28 |
sel = Y.config.doc.selection.createRange();
|
|
|
29 |
}
|
|
|
30 |
this._selection = sel;
|
|
|
31 |
|
|
|
32 |
if (!sel) {
|
|
|
33 |
return false;
|
|
|
34 |
}
|
|
|
35 |
|
|
|
36 |
if (sel.pasteHTML) {
|
|
|
37 |
this.isCollapsed = (sel.compareEndPoints('StartToEnd', sel)) ? false : true;
|
|
|
38 |
if (this.isCollapsed) {
|
|
|
39 |
this.anchorNode = this.focusNode = Y.one(sel.parentElement());
|
|
|
40 |
|
|
|
41 |
if (domEvent) {
|
|
|
42 |
ieNode = Y.config.doc.elementFromPoint(domEvent.clientX, domEvent.clientY);
|
|
|
43 |
}
|
|
|
44 |
rng = sel.duplicate();
|
|
|
45 |
if (!ieNode) {
|
|
|
46 |
par = sel.parentElement();
|
|
|
47 |
nodes = par.childNodes;
|
|
|
48 |
|
|
|
49 |
for (i = 0; i < nodes.length; i++) {
|
|
|
50 |
//This causes IE to not allow a selection on a doubleclick
|
|
|
51 |
//rng.select(nodes[i]);
|
|
|
52 |
if (rng.inRange(sel)) {
|
|
|
53 |
if (!ieNode) {
|
|
|
54 |
ieNode = nodes[i];
|
|
|
55 |
}
|
|
|
56 |
}
|
|
|
57 |
}
|
|
|
58 |
}
|
|
|
59 |
|
|
|
60 |
this.ieNode = ieNode;
|
|
|
61 |
|
|
|
62 |
if (ieNode) {
|
|
|
63 |
if (ieNode.nodeType !== 3) {
|
|
|
64 |
if (ieNode.firstChild) {
|
|
|
65 |
ieNode = ieNode.firstChild;
|
|
|
66 |
}
|
|
|
67 |
if (root.compareTo(ieNode)) {
|
|
|
68 |
if (ieNode.firstChild) {
|
|
|
69 |
ieNode = ieNode.firstChild;
|
|
|
70 |
}
|
|
|
71 |
}
|
|
|
72 |
}
|
|
|
73 |
this.anchorNode = this.focusNode = Y.EditorSelection.resolve(ieNode);
|
|
|
74 |
|
|
|
75 |
rng.moveToElementText(sel.parentElement());
|
|
|
76 |
comp = sel.compareEndPoints('StartToStart', rng);
|
|
|
77 |
if (comp) {
|
|
|
78 |
//We are not at the beginning of the selection.
|
|
|
79 |
//Setting the move to something large, may need to increase it later
|
|
|
80 |
moved = this.getEditorOffset(root);
|
|
|
81 |
sel.move('character', -(moved));
|
|
|
82 |
}
|
|
|
83 |
|
|
|
84 |
this.anchorOffset = this.focusOffset = moved;
|
|
|
85 |
|
|
|
86 |
this.anchorTextNode = this.focusTextNode = Y.one(ieNode);
|
|
|
87 |
}
|
|
|
88 |
|
|
|
89 |
|
|
|
90 |
} else {
|
|
|
91 |
//This helps IE deal with a selection and nodeChange events
|
|
|
92 |
if (sel.htmlText && sel.htmlText !== '') {
|
|
|
93 |
n = Y.Node.create(sel.htmlText);
|
|
|
94 |
if (n && n.get('id')) {
|
|
|
95 |
id = n.get('id');
|
|
|
96 |
this.anchorNode = this.focusNode = Y.one('#' + id);
|
|
|
97 |
} else if (n) {
|
|
|
98 |
n = n.get('childNodes');
|
|
|
99 |
this.anchorNode = this.focusNode = n.item(0);
|
|
|
100 |
}
|
|
|
101 |
}
|
|
|
102 |
}
|
|
|
103 |
|
|
|
104 |
//var self = this;
|
|
|
105 |
//debugger;
|
|
|
106 |
} else {
|
|
|
107 |
this.isCollapsed = sel.isCollapsed;
|
|
|
108 |
this.anchorNode = Y.EditorSelection.resolve(sel.anchorNode);
|
|
|
109 |
this.focusNode = Y.EditorSelection.resolve(sel.focusNode);
|
|
|
110 |
this.anchorOffset = sel.anchorOffset;
|
|
|
111 |
this.focusOffset = sel.focusOffset;
|
|
|
112 |
|
|
|
113 |
this.anchorTextNode = Y.one(sel.anchorNode || this.anchorNode);
|
|
|
114 |
this.focusTextNode = Y.one(sel.focusNode || this.focusNode);
|
|
|
115 |
}
|
|
|
116 |
if (Y.Lang.isString(sel.text)) {
|
|
|
117 |
this.text = sel.text;
|
|
|
118 |
} else {
|
|
|
119 |
if (sel.toString) {
|
|
|
120 |
this.text = sel.toString();
|
|
|
121 |
} else {
|
|
|
122 |
this.text = '';
|
|
|
123 |
}
|
|
|
124 |
}
|
|
|
125 |
};
|
|
|
126 |
|
|
|
127 |
/**
|
|
|
128 |
* Utility method to remove dead font-family styles from an element.
|
|
|
129 |
* @static
|
|
|
130 |
* @method removeFontFamily
|
|
|
131 |
*/
|
|
|
132 |
Y.EditorSelection.removeFontFamily = function(n) {
|
|
|
133 |
n.removeAttribute('face');
|
|
|
134 |
var s = n.getAttribute('style').toLowerCase();
|
|
|
135 |
if (s === '' || (s === 'font-family: ')) {
|
|
|
136 |
n.removeAttribute('style');
|
|
|
137 |
}
|
|
|
138 |
if (s.match(Y.EditorSelection.REG_FONTFAMILY)) {
|
|
|
139 |
s = s.replace(Y.EditorSelection.REG_FONTFAMILY, '');
|
|
|
140 |
n.setAttribute('style', s);
|
|
|
141 |
}
|
|
|
142 |
};
|
|
|
143 |
|
|
|
144 |
/**
|
|
|
145 |
* Performs a prefilter on all nodes in the editor. Looks for nodes with a style: fontFamily or font face
|
|
|
146 |
* It then creates a dynamic class assigns it and removed the property. This is so that we don't lose
|
|
|
147 |
* the fontFamily when selecting nodes.
|
|
|
148 |
* @static
|
|
|
149 |
* @method filter
|
|
|
150 |
*/
|
|
|
151 |
Y.EditorSelection.filter = function(blocks) {
|
|
|
152 |
Y.log('Filtering nodes', 'info', 'editor-selection');
|
|
|
153 |
|
|
|
154 |
var startTime = (new Date()).getTime(),
|
|
|
155 |
editorSelection = Y.EditorSelection,
|
|
|
156 |
root = editorSelection.ROOT,
|
|
|
157 |
endTime,
|
|
|
158 |
nodes = root.all(editorSelection.ALL),
|
|
|
159 |
baseNodes = root.all('strong,em'),
|
|
|
160 |
doc = Y.config.doc, hrs,
|
|
|
161 |
classNames = {}, cssString = '',
|
|
|
162 |
ls, startTime1 = (new Date()).getTime(),
|
|
|
163 |
endTime1;
|
|
|
164 |
|
|
|
165 |
nodes.each(function(n) {
|
|
|
166 |
var raw = Y.Node.getDOMNode(n);
|
|
|
167 |
if (raw.style[FONT_FAMILY]) {
|
|
|
168 |
classNames['.' + n._yuid] = raw.style[FONT_FAMILY];
|
|
|
169 |
n.addClass(n._yuid);
|
|
|
170 |
|
|
|
171 |
editorSelection.removeFontFamily(raw);
|
|
|
172 |
}
|
|
|
173 |
});
|
|
|
174 |
endTime1 = (new Date()).getTime();
|
|
|
175 |
Y.log('Node Filter Timer: ' + (endTime1 - startTime1) + 'ms', 'info', 'editor-selection');
|
|
|
176 |
|
|
|
177 |
root.all('.hr').addClass('yui-skip').addClass('yui-non');
|
|
|
178 |
|
|
|
179 |
if (Y.UA.ie) {
|
|
|
180 |
hrs = Y.Node.getDOMNode(root).getElementsByTagName('hr');
|
|
|
181 |
Y.each(hrs, function(hr) {
|
|
|
182 |
var el = doc.createElement('div'),
|
|
|
183 |
s = el.style;
|
|
|
184 |
|
|
|
185 |
el.className = 'hr yui-non yui-skip';
|
|
|
186 |
|
|
|
187 |
el.setAttribute('readonly', true);
|
|
|
188 |
el.setAttribute('contenteditable', false); //Keep it from being Edited
|
|
|
189 |
if (hr.parentNode) {
|
|
|
190 |
hr.parentNode.replaceChild(el, hr);
|
|
|
191 |
}
|
|
|
192 |
//Had to move to inline style. writes for ie's < 8. They don't render el.setAttribute('style');
|
|
|
193 |
s.border = '1px solid #ccc';
|
|
|
194 |
s.lineHeight = '0';
|
|
|
195 |
s.height = '0';
|
|
|
196 |
s.fontSize = '0';
|
|
|
197 |
s.marginTop = '5px';
|
|
|
198 |
s.marginBottom = '5px';
|
|
|
199 |
s.marginLeft = '0px';
|
|
|
200 |
s.marginRight = '0px';
|
|
|
201 |
s.padding = '0';
|
|
|
202 |
});
|
|
|
203 |
}
|
|
|
204 |
|
|
|
205 |
|
|
|
206 |
Y.each(classNames, function(v, k) {
|
|
|
207 |
cssString += k + ' { font-family: ' + v.replace(/"/gi, '') + '; }';
|
|
|
208 |
});
|
|
|
209 |
Y.StyleSheet(cssString, 'editor');
|
|
|
210 |
|
|
|
211 |
|
|
|
212 |
//Not sure about this one?
|
|
|
213 |
baseNodes.each(function(n, k) {
|
|
|
214 |
var t = n.get('tagName').toLowerCase(),
|
|
|
215 |
newTag = 'i';
|
|
|
216 |
if (t === 'strong') {
|
|
|
217 |
newTag = 'b';
|
|
|
218 |
}
|
|
|
219 |
editorSelection.prototype._swap(baseNodes.item(k), newTag);
|
|
|
220 |
});
|
|
|
221 |
|
|
|
222 |
//Filter out all the empty UL/OL's
|
|
|
223 |
ls = root.all('ol,ul');
|
|
|
224 |
ls.each(function(v) {
|
|
|
225 |
var lis = v.all('li');
|
|
|
226 |
if (!lis.size()) {
|
|
|
227 |
v.remove();
|
|
|
228 |
}
|
|
|
229 |
});
|
|
|
230 |
|
|
|
231 |
if (blocks) {
|
|
|
232 |
editorSelection.filterBlocks();
|
|
|
233 |
}
|
|
|
234 |
endTime = (new Date()).getTime();
|
|
|
235 |
Y.log('Filter Timer: ' + (endTime - startTime) + 'ms', 'info', 'editor-selection');
|
|
|
236 |
};
|
|
|
237 |
|
|
|
238 |
/**
|
|
|
239 |
* Method attempts to replace all "orphined" text nodes in the main body by wrapping them with a <p>. Called from filter.
|
|
|
240 |
* @static
|
|
|
241 |
* @method filterBlocks
|
|
|
242 |
*/
|
|
|
243 |
Y.EditorSelection.filterBlocks = function() {
|
|
|
244 |
Y.log('RAW filter blocks', 'info', 'editor-selection');
|
|
|
245 |
var startTime = (new Date()).getTime(), endTime,
|
|
|
246 |
childs = Y.Node.getDOMNode(Y.EditorSelection.ROOT).childNodes, i, node, wrapped = false, doit = true,
|
|
|
247 |
sel, single, br, c, s, html;
|
|
|
248 |
|
|
|
249 |
if (childs) {
|
|
|
250 |
for (i = 0; i < childs.length; i++) {
|
|
|
251 |
node = Y.one(childs[i]);
|
|
|
252 |
if (!node.test(Y.EditorSelection.BLOCKS)) {
|
|
|
253 |
doit = true;
|
|
|
254 |
if (childs[i].nodeType === 3) {
|
|
|
255 |
c = childs[i][textContent].match(Y.EditorSelection.REG_CHAR);
|
|
|
256 |
s = childs[i][textContent].match(Y.EditorSelection.REG_NON);
|
|
|
257 |
if (c === null && s) {
|
|
|
258 |
doit = false;
|
|
|
259 |
|
|
|
260 |
}
|
|
|
261 |
}
|
|
|
262 |
if (doit) {
|
|
|
263 |
if (!wrapped) {
|
|
|
264 |
wrapped = [];
|
|
|
265 |
}
|
|
|
266 |
wrapped.push(childs[i]);
|
|
|
267 |
}
|
|
|
268 |
} else {
|
|
|
269 |
wrapped = Y.EditorSelection._wrapBlock(wrapped);
|
|
|
270 |
}
|
|
|
271 |
}
|
|
|
272 |
wrapped = Y.EditorSelection._wrapBlock(wrapped);
|
|
|
273 |
}
|
|
|
274 |
|
|
|
275 |
single = Y.all(Y.EditorSelection.DEFAULT_BLOCK_TAG);
|
|
|
276 |
if (single.size() === 1) {
|
|
|
277 |
Y.log('Only One default block tag (' + Y.EditorSelection.DEFAULT_BLOCK_TAG + '), focus it..', 'info', 'editor-selection');
|
|
|
278 |
br = single.item(0).all('br');
|
|
|
279 |
if (br.size() === 1) {
|
|
|
280 |
if (!br.item(0).test('.yui-cursor')) {
|
|
|
281 |
br.item(0).remove();
|
|
|
282 |
}
|
|
|
283 |
html = single.item(0).get('innerHTML');
|
|
|
284 |
if (html === '' || html === ' ') {
|
|
|
285 |
Y.log('Paragraph empty, focusing cursor', 'info', 'editor-selection');
|
|
|
286 |
single.set('innerHTML', Y.EditorSelection.CURSOR);
|
|
|
287 |
sel = new Y.EditorSelection();
|
|
|
288 |
sel.focusCursor(true, true);
|
|
|
289 |
}
|
|
|
290 |
if (br.item(0).test('.yui-cursor') && Y.UA.ie) {
|
|
|
291 |
br.item(0).remove();
|
|
|
292 |
}
|
|
|
293 |
}
|
|
|
294 |
} else {
|
|
|
295 |
single.each(function(p) {
|
|
|
296 |
var html = p.get('innerHTML');
|
|
|
297 |
if (html === '') {
|
|
|
298 |
Y.log('Empty Paragraph Tag Found, Removing It', 'info', 'editor-selection');
|
|
|
299 |
p.remove();
|
|
|
300 |
}
|
|
|
301 |
});
|
|
|
302 |
}
|
|
|
303 |
|
|
|
304 |
endTime = (new Date()).getTime();
|
|
|
305 |
Y.log('FilterBlocks Timer: ' + (endTime - startTime) + 'ms', 'info', 'editor-selection');
|
|
|
306 |
};
|
|
|
307 |
|
|
|
308 |
/**
|
|
|
309 |
* Regular Expression used to find dead font-family styles
|
|
|
310 |
* @static
|
|
|
311 |
* @property REG_FONTFAMILY
|
|
|
312 |
*/
|
|
|
313 |
Y.EditorSelection.REG_FONTFAMILY = /font-family:\s*;/;
|
|
|
314 |
|
|
|
315 |
/**
|
|
|
316 |
* Regular Expression to determine if a string has a character in it
|
|
|
317 |
* @static
|
|
|
318 |
* @property REG_CHAR
|
|
|
319 |
*/
|
|
|
320 |
Y.EditorSelection.REG_CHAR = /[a-zA-Z-0-9_!@#\$%\^&*\(\)-=_+\[\]\\{}|;':",.\/<>\?]/gi;
|
|
|
321 |
|
|
|
322 |
/**
|
|
|
323 |
* Regular Expression to determine if a string has a non-character in it
|
|
|
324 |
* @static
|
|
|
325 |
* @property REG_NON
|
|
|
326 |
*/
|
|
|
327 |
Y.EditorSelection.REG_NON = /[\s|\n|\t]/gi;
|
|
|
328 |
|
|
|
329 |
/**
|
|
|
330 |
* Regular Expression to remove all HTML from a string
|
|
|
331 |
* @static
|
|
|
332 |
* @property REG_NOHTML
|
|
|
333 |
*/
|
|
|
334 |
Y.EditorSelection.REG_NOHTML = /<\S[^><]*>/g;
|
|
|
335 |
|
|
|
336 |
|
|
|
337 |
/**
|
|
|
338 |
* Wraps an array of elements in a Block level tag
|
|
|
339 |
* @static
|
|
|
340 |
* @private
|
|
|
341 |
* @method _wrapBlock
|
|
|
342 |
*/
|
|
|
343 |
Y.EditorSelection._wrapBlock = function(wrapped) {
|
|
|
344 |
if (wrapped) {
|
|
|
345 |
var newChild = Y.Node.create('<' + Y.EditorSelection.DEFAULT_BLOCK_TAG + '></' + Y.EditorSelection.DEFAULT_BLOCK_TAG + '>'),
|
|
|
346 |
firstChild = Y.one(wrapped[0]), i;
|
|
|
347 |
|
|
|
348 |
for (i = 1; i < wrapped.length; i++) {
|
|
|
349 |
newChild.append(wrapped[i]);
|
|
|
350 |
}
|
|
|
351 |
firstChild.replace(newChild);
|
|
|
352 |
newChild.prepend(firstChild);
|
|
|
353 |
}
|
|
|
354 |
return false;
|
|
|
355 |
};
|
|
|
356 |
|
|
|
357 |
/**
|
|
|
358 |
* Undoes what filter does enough to return the HTML from the Editor, then re-applies the filter.
|
|
|
359 |
* @static
|
|
|
360 |
* @method unfilter
|
|
|
361 |
* @return {String} The filtered HTML
|
|
|
362 |
*/
|
|
|
363 |
Y.EditorSelection.unfilter = function() {
|
|
|
364 |
var root = Y.EditorSelection.ROOT,
|
|
|
365 |
nodes = root.all('[class]'),
|
|
|
366 |
html = '', nons, ids,
|
|
|
367 |
body = root;
|
|
|
368 |
|
|
|
369 |
Y.log('UnFiltering nodes', 'info', 'editor-selection');
|
|
|
370 |
|
|
|
371 |
nodes.each(function(n) {
|
|
|
372 |
if (n.hasClass(n._yuid)) {
|
|
|
373 |
//One of ours
|
|
|
374 |
n.setStyle(FONT_FAMILY, n.getStyle(FONT_FAMILY));
|
|
|
375 |
n.removeClass(n._yuid);
|
|
|
376 |
if (n.getAttribute('class') === '') {
|
|
|
377 |
n.removeAttribute('class');
|
|
|
378 |
}
|
|
|
379 |
}
|
|
|
380 |
});
|
|
|
381 |
|
|
|
382 |
nons = root.all('.yui-non');
|
|
|
383 |
nons.each(function(n) {
|
|
|
384 |
if (!n.hasClass('yui-skip') && n.get('innerHTML') === '') {
|
|
|
385 |
n.remove();
|
|
|
386 |
} else {
|
|
|
387 |
n.removeClass('yui-non').removeClass('yui-skip');
|
|
|
388 |
}
|
|
|
389 |
});
|
|
|
390 |
|
|
|
391 |
ids = root.all('[id]');
|
|
|
392 |
ids.each(function(n) {
|
|
|
393 |
if (n.get('id').indexOf('yui_3_') === 0) {
|
|
|
394 |
n.removeAttribute('id');
|
|
|
395 |
n.removeAttribute('_yuid');
|
|
|
396 |
}
|
|
|
397 |
});
|
|
|
398 |
|
|
|
399 |
if (body) {
|
|
|
400 |
html = body.get('innerHTML');
|
|
|
401 |
}
|
|
|
402 |
|
|
|
403 |
root.all('.hr').addClass('yui-skip').addClass('yui-non');
|
|
|
404 |
|
|
|
405 |
/*
|
|
|
406 |
nodes.each(function(n) {
|
|
|
407 |
n.addClass(n._yuid);
|
|
|
408 |
n.setStyle(FONT_FAMILY, '');
|
|
|
409 |
if (n.getAttribute('style') === '') {
|
|
|
410 |
n.removeAttribute('style');
|
|
|
411 |
}
|
|
|
412 |
});
|
|
|
413 |
*/
|
|
|
414 |
|
|
|
415 |
return html;
|
|
|
416 |
};
|
|
|
417 |
/**
|
|
|
418 |
* Resolve a node from the selection object and return a Node instance
|
|
|
419 |
* @static
|
|
|
420 |
* @method resolve
|
|
|
421 |
* @param {HTMLElement} n The HTMLElement to resolve. Might be a TextNode, gives parentNode.
|
|
|
422 |
* @return {Node} The Resolved node
|
|
|
423 |
*/
|
|
|
424 |
Y.EditorSelection.resolve = function(n) {
|
|
|
425 |
if (!n) {
|
|
|
426 |
return Y.EditorSelection.ROOT;
|
|
|
427 |
}
|
|
|
428 |
|
|
|
429 |
if (n && n.nodeType === 3) {
|
|
|
430 |
//Adding a try/catch here because in rare occasions IE will
|
|
|
431 |
//Throw a error accessing the parentNode of a stranded text node.
|
|
|
432 |
//In the case of Ctrl+Z (Undo)
|
|
|
433 |
try {
|
|
|
434 |
n = n.parentNode;
|
|
|
435 |
} catch (re) {
|
|
|
436 |
n = Y.EditorSelection.ROOT;
|
|
|
437 |
}
|
|
|
438 |
}
|
|
|
439 |
return Y.one(n);
|
|
|
440 |
};
|
|
|
441 |
|
|
|
442 |
/**
|
|
|
443 |
* Returns the innerHTML of a node with all HTML tags removed.
|
|
|
444 |
* @static
|
|
|
445 |
* @method getText
|
|
|
446 |
* @param {Node} node The Node instance to remove the HTML from
|
|
|
447 |
* @return {String} The string of text
|
|
|
448 |
*/
|
|
|
449 |
Y.EditorSelection.getText = function(node) {
|
|
|
450 |
var txt = node.get('innerHTML').replace(Y.EditorSelection.REG_NOHTML, '');
|
|
|
451 |
//Clean out the cursor subs to see if the Node is empty
|
|
|
452 |
txt = txt.replace('<span><br></span>', '').replace('<br>', '');
|
|
|
453 |
return txt;
|
|
|
454 |
};
|
|
|
455 |
|
|
|
456 |
//Y.EditorSelection.DEFAULT_BLOCK_TAG = 'div';
|
|
|
457 |
Y.EditorSelection.DEFAULT_BLOCK_TAG = 'p';
|
|
|
458 |
|
|
|
459 |
/**
|
|
|
460 |
* The selector to use when looking for Nodes to cache the value of: [style],font[face]
|
|
|
461 |
* @static
|
|
|
462 |
* @property ALL
|
|
|
463 |
*/
|
|
|
464 |
Y.EditorSelection.ALL = '[style],font[face]';
|
|
|
465 |
|
|
|
466 |
/**
|
|
|
467 |
* The selector to use when looking for block level items.
|
|
|
468 |
* @static
|
|
|
469 |
* @property BLOCKS
|
|
|
470 |
*/
|
|
|
471 |
Y.EditorSelection.BLOCKS = 'p,div,ul,ol,table,style';
|
|
|
472 |
/**
|
|
|
473 |
* The temporary fontname applied to a selection to retrieve their values: yui-tmp
|
|
|
474 |
* @static
|
|
|
475 |
* @property TMP
|
|
|
476 |
*/
|
|
|
477 |
Y.EditorSelection.TMP = 'yui-tmp';
|
|
|
478 |
/**
|
|
|
479 |
* The default tag to use when creating elements: span
|
|
|
480 |
* @static
|
|
|
481 |
* @property DEFAULT_TAG
|
|
|
482 |
*/
|
|
|
483 |
Y.EditorSelection.DEFAULT_TAG = 'span';
|
|
|
484 |
|
|
|
485 |
/**
|
|
|
486 |
* The id of the outer cursor wrapper
|
|
|
487 |
* @static
|
|
|
488 |
* @property CURID
|
|
|
489 |
*/
|
|
|
490 |
Y.EditorSelection.CURID = 'yui-cursor';
|
|
|
491 |
|
|
|
492 |
/**
|
|
|
493 |
* The id used to wrap the inner space of the cursor position
|
|
|
494 |
* @static
|
|
|
495 |
* @property CUR_WRAPID
|
|
|
496 |
*/
|
|
|
497 |
Y.EditorSelection.CUR_WRAPID = 'yui-cursor-wrapper';
|
|
|
498 |
|
|
|
499 |
/**
|
|
|
500 |
* The default HTML used to focus the cursor..
|
|
|
501 |
* @static
|
|
|
502 |
* @property CURSOR
|
|
|
503 |
*/
|
|
|
504 |
Y.EditorSelection.CURSOR = '<span><br class="yui-cursor"></span>';
|
|
|
505 |
|
|
|
506 |
/**
|
|
|
507 |
* The default HTML element from which data will be retrieved. Default: body
|
|
|
508 |
* @static
|
|
|
509 |
* @property ROOT
|
|
|
510 |
*/
|
|
|
511 |
Y.EditorSelection.ROOT = Y.one('body');
|
|
|
512 |
|
|
|
513 |
Y.EditorSelection.hasCursor = function() {
|
|
|
514 |
var cur = Y.all('#' + Y.EditorSelection.CUR_WRAPID);
|
|
|
515 |
Y.log('Has Cursor: ' + cur.size(), 'info', 'editor-selection');
|
|
|
516 |
return cur.size();
|
|
|
517 |
};
|
|
|
518 |
|
|
|
519 |
/**
|
|
|
520 |
* Called from Editor keydown to remove the "extra" space before the cursor.
|
|
|
521 |
* @static
|
|
|
522 |
* @method cleanCursor
|
|
|
523 |
*/
|
|
|
524 |
Y.EditorSelection.cleanCursor = function() {
|
|
|
525 |
//Y.log('Cleaning Cursor', 'info', 'Selection');
|
|
|
526 |
var cur, sel = 'br.yui-cursor';
|
|
|
527 |
cur = Y.all(sel);
|
|
|
528 |
if (cur.size()) {
|
|
|
529 |
cur.each(function(b) {
|
|
|
530 |
var c = b.get('parentNode.parentNode.childNodes'), html;
|
|
|
531 |
if (c.size()) {
|
|
|
532 |
b.remove();
|
|
|
533 |
} else {
|
|
|
534 |
html = Y.EditorSelection.getText(c.item(0));
|
|
|
535 |
if (html !== '') {
|
|
|
536 |
b.remove();
|
|
|
537 |
}
|
|
|
538 |
}
|
|
|
539 |
});
|
|
|
540 |
}
|
|
|
541 |
/*
|
|
|
542 |
var cur = Y.all('#' + Y.EditorSelection.CUR_WRAPID);
|
|
|
543 |
if (cur.size()) {
|
|
|
544 |
cur.each(function(c) {
|
|
|
545 |
var html = c.get('innerHTML');
|
|
|
546 |
if (html == ' ' || html == '<br>') {
|
|
|
547 |
if (c.previous() || c.next()) {
|
|
|
548 |
c.remove();
|
|
|
549 |
}
|
|
|
550 |
}
|
|
|
551 |
});
|
|
|
552 |
}
|
|
|
553 |
*/
|
|
|
554 |
};
|
|
|
555 |
|
|
|
556 |
Y.EditorSelection.prototype = {
|
|
|
557 |
/**
|
|
|
558 |
* Range text value
|
|
|
559 |
* @property text
|
|
|
560 |
* @type String
|
|
|
561 |
*/
|
|
|
562 |
text: null,
|
|
|
563 |
/**
|
|
|
564 |
* Flag to show if the range is collapsed or not
|
|
|
565 |
* @property isCollapsed
|
|
|
566 |
* @type Boolean
|
|
|
567 |
*/
|
|
|
568 |
isCollapsed: null,
|
|
|
569 |
/**
|
|
|
570 |
* A Node instance of the parentNode of the anchorNode of the range
|
|
|
571 |
* @property anchorNode
|
|
|
572 |
* @type Node
|
|
|
573 |
*/
|
|
|
574 |
anchorNode: null,
|
|
|
575 |
/**
|
|
|
576 |
* The offset from the range object
|
|
|
577 |
* @property anchorOffset
|
|
|
578 |
* @type Number
|
|
|
579 |
*/
|
|
|
580 |
anchorOffset: null,
|
|
|
581 |
/**
|
|
|
582 |
* A Node instance of the actual textNode of the range.
|
|
|
583 |
* @property anchorTextNode
|
|
|
584 |
* @type Node
|
|
|
585 |
*/
|
|
|
586 |
anchorTextNode: null,
|
|
|
587 |
/**
|
|
|
588 |
* A Node instance of the parentNode of the focusNode of the range
|
|
|
589 |
* @property focusNode
|
|
|
590 |
* @type Node
|
|
|
591 |
*/
|
|
|
592 |
focusNode: null,
|
|
|
593 |
/**
|
|
|
594 |
* The offset from the range object
|
|
|
595 |
* @property focusOffset
|
|
|
596 |
* @type Number
|
|
|
597 |
*/
|
|
|
598 |
focusOffset: null,
|
|
|
599 |
/**
|
|
|
600 |
* A Node instance of the actual textNode of the range.
|
|
|
601 |
* @property focusTextNode
|
|
|
602 |
* @type Node
|
|
|
603 |
*/
|
|
|
604 |
focusTextNode: null,
|
|
|
605 |
/**
|
|
|
606 |
* The actual Selection/Range object
|
|
|
607 |
* @property _selection
|
|
|
608 |
* @private
|
|
|
609 |
*/
|
|
|
610 |
_selection: null,
|
|
|
611 |
/**
|
|
|
612 |
* Wrap an element, with another element
|
|
|
613 |
* @private
|
|
|
614 |
* @method _wrap
|
|
|
615 |
* @param {HTMLElement} n The node to wrap
|
|
|
616 |
* @param {String} tag The tag to use when creating the new element.
|
|
|
617 |
* @return {HTMLElement} The wrapped node
|
|
|
618 |
*/
|
|
|
619 |
_wrap: function(n, tag) {
|
|
|
620 |
var tmp = Y.Node.create('<' + tag + '></' + tag + '>');
|
|
|
621 |
tmp.set(INNER_HTML, n.get(INNER_HTML));
|
|
|
622 |
n.set(INNER_HTML, '');
|
|
|
623 |
n.append(tmp);
|
|
|
624 |
return Y.Node.getDOMNode(tmp);
|
|
|
625 |
},
|
|
|
626 |
/**
|
|
|
627 |
* Swap an element, with another element
|
|
|
628 |
* @private
|
|
|
629 |
* @method _swap
|
|
|
630 |
* @param {HTMLElement} n The node to swap
|
|
|
631 |
* @param {String} tag The tag to use when creating the new element.
|
|
|
632 |
* @return {HTMLElement} The new node
|
|
|
633 |
*/
|
|
|
634 |
_swap: function(n, tag) {
|
|
|
635 |
var tmp = Y.Node.create('<' + tag + '></' + tag + '>');
|
|
|
636 |
tmp.set(INNER_HTML, n.get(INNER_HTML));
|
|
|
637 |
n.replace(tmp, n);
|
|
|
638 |
return Y.Node.getDOMNode(tmp);
|
|
|
639 |
},
|
|
|
640 |
/**
|
|
|
641 |
* Get all the nodes in the current selection. This method will actually perform a filter first.
|
|
|
642 |
* Then it calls doc.execCommand('fontname', null, 'yui-tmp') to touch all nodes in the selection.
|
|
|
643 |
* The it compiles a list of all nodes affected by the execCommand and builds a NodeList to return.
|
|
|
644 |
* @method getSelected
|
|
|
645 |
* @return {NodeList} A NodeList of all items in the selection.
|
|
|
646 |
*/
|
|
|
647 |
getSelected: function() {
|
|
|
648 |
var editorSelection = Y.EditorSelection,
|
|
|
649 |
root = editorSelection.ROOT,
|
|
|
650 |
nodes,
|
|
|
651 |
items = [];
|
|
|
652 |
|
|
|
653 |
editorSelection.filter();
|
|
|
654 |
Y.config.doc.execCommand('fontname', null, editorSelection.TMP);
|
|
|
655 |
nodes = root.all(editorSelection.ALL);
|
|
|
656 |
|
|
|
657 |
nodes.each(function(n, k) {
|
|
|
658 |
if (n.getStyle(FONT_FAMILY) === editorSelection.TMP) {
|
|
|
659 |
n.setStyle(FONT_FAMILY, '');
|
|
|
660 |
editorSelection.removeFontFamily(n);
|
|
|
661 |
if (!n.compareTo(root)) {
|
|
|
662 |
items.push(Y.Node.getDOMNode(nodes.item(k)));
|
|
|
663 |
}
|
|
|
664 |
}
|
|
|
665 |
});
|
|
|
666 |
return Y.all(items);
|
|
|
667 |
},
|
|
|
668 |
/**
|
|
|
669 |
* Insert HTML at the current cursor position and return a Node instance of the newly inserted element.
|
|
|
670 |
* @method insertContent
|
|
|
671 |
* @param {String} html The HTML to insert.
|
|
|
672 |
* @return {Node} The inserted Node.
|
|
|
673 |
*/
|
|
|
674 |
insertContent: function(html) {
|
|
|
675 |
return this.insertAtCursor(html, this.anchorTextNode, this.anchorOffset, true);
|
|
|
676 |
},
|
|
|
677 |
/**
|
|
|
678 |
* 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.
|
|
|
679 |
* @method insertAtCursor
|
|
|
680 |
* @param {String} html The HTML to insert.
|
|
|
681 |
* @param {Node} node The text node to break when inserting.
|
|
|
682 |
* @param {Number} offset The left offset of the text node to break and insert the new content.
|
|
|
683 |
* @param {Boolean} collapse Should the range be collapsed after insertion. default: false
|
|
|
684 |
* @return {Node} The inserted Node.
|
|
|
685 |
*/
|
|
|
686 |
insertAtCursor: function(html, node, offset, collapse) {
|
|
|
687 |
var cur = Y.Node.create('<' + Y.EditorSelection.DEFAULT_TAG + ' class="yui-non"></' + Y.EditorSelection.DEFAULT_TAG + '>'),
|
|
|
688 |
inHTML, txt, txt2, newNode, range = this.createRange(), b, root = Y.EditorSelection.ROOT;
|
|
|
689 |
|
|
|
690 |
if (root.compareTo(node)) {
|
|
|
691 |
b = Y.Node.create('<span></span>');
|
|
|
692 |
node.append(b);
|
|
|
693 |
node = b;
|
|
|
694 |
}
|
|
|
695 |
|
|
|
696 |
|
|
|
697 |
if (range.pasteHTML) {
|
|
|
698 |
if (offset === 0 && node && !node.previous() && node.get('nodeType') === 3) {
|
|
|
699 |
/*
|
|
|
700 |
* For some strange reason, range.pasteHTML fails if the node is a textNode and
|
|
|
701 |
* the offset is 0. (The cursor is at the beginning of the line)
|
|
|
702 |
* It will always insert the new content at position 1 instead of
|
|
|
703 |
* position 0. Here we test for that case and do it the hard way.
|
|
|
704 |
*/
|
|
|
705 |
node.insert(html, 'before');
|
|
|
706 |
if (range.moveToElementText) {
|
|
|
707 |
range.moveToElementText(Y.Node.getDOMNode(node.previous()));
|
|
|
708 |
}
|
|
|
709 |
//Move the cursor after the new node
|
|
|
710 |
range.collapse(false);
|
|
|
711 |
range.select();
|
|
|
712 |
return node.previous();
|
|
|
713 |
} else {
|
|
|
714 |
newNode = Y.Node.create(html);
|
|
|
715 |
try {
|
|
|
716 |
range.pasteHTML('<span id="rte-insert"></span>');
|
|
|
717 |
} catch (e) {}
|
|
|
718 |
inHTML = root.one('#rte-insert');
|
|
|
719 |
if (inHTML) {
|
|
|
720 |
inHTML.set('id', '');
|
|
|
721 |
inHTML.replace(newNode);
|
|
|
722 |
if (range.moveToElementText) {
|
|
|
723 |
range.moveToElementText(Y.Node.getDOMNode(newNode));
|
|
|
724 |
}
|
|
|
725 |
range.collapse(false);
|
|
|
726 |
range.select();
|
|
|
727 |
return newNode;
|
|
|
728 |
} else {
|
|
|
729 |
Y.on('available', function() {
|
|
|
730 |
inHTML.set('id', '');
|
|
|
731 |
inHTML.replace(newNode);
|
|
|
732 |
if (range.moveToElementText) {
|
|
|
733 |
range.moveToElementText(Y.Node.getDOMNode(newNode));
|
|
|
734 |
}
|
|
|
735 |
range.collapse(false);
|
|
|
736 |
range.select();
|
|
|
737 |
}, '#rte-insert');
|
|
|
738 |
}
|
|
|
739 |
}
|
|
|
740 |
} else {
|
|
|
741 |
//TODO using Y.Node.create here throws warnings & strips first white space character
|
|
|
742 |
//txt = Y.one(Y.Node.create(inHTML.substr(0, offset)));
|
|
|
743 |
//txt2 = Y.one(Y.Node.create(inHTML.substr(offset)));
|
|
|
744 |
if (offset > 0) {
|
|
|
745 |
inHTML = node.get(textContent);
|
|
|
746 |
|
|
|
747 |
txt = Y.one(Y.config.doc.createTextNode(inHTML.substr(0, offset)));
|
|
|
748 |
txt2 = Y.one(Y.config.doc.createTextNode(inHTML.substr(offset)));
|
|
|
749 |
|
|
|
750 |
node.replace(txt, node);
|
|
|
751 |
newNode = Y.Node.create(html);
|
|
|
752 |
if (newNode.get('nodeType') === 11) {
|
|
|
753 |
b = Y.Node.create('<span></span>');
|
|
|
754 |
b.append(newNode);
|
|
|
755 |
newNode = b;
|
|
|
756 |
}
|
|
|
757 |
txt.insert(newNode, 'after');
|
|
|
758 |
//if (txt2 && txt2.get('length')) {
|
|
|
759 |
if (txt2) {
|
|
|
760 |
newNode.insert(cur, 'after');
|
|
|
761 |
cur.insert(txt2, 'after');
|
|
|
762 |
this.selectNode(cur, collapse);
|
|
|
763 |
}
|
|
|
764 |
} else {
|
|
|
765 |
if (node.get('nodeType') === 3) {
|
|
|
766 |
node = node.get('parentNode') || root;
|
|
|
767 |
}
|
|
|
768 |
newNode = Y.Node.create(html);
|
|
|
769 |
html = node.get('innerHTML').replace(/\n/gi, '');
|
|
|
770 |
if (html === '' || html === '<br>') {
|
|
|
771 |
node.append(newNode);
|
|
|
772 |
} else {
|
|
|
773 |
if (newNode.get('parentNode')) {
|
|
|
774 |
node.insert(newNode, 'before');
|
|
|
775 |
} else {
|
|
|
776 |
root.prepend(newNode);
|
|
|
777 |
}
|
|
|
778 |
}
|
|
|
779 |
if (node.get('firstChild').test('br')) {
|
|
|
780 |
node.get('firstChild').remove();
|
|
|
781 |
}
|
|
|
782 |
}
|
|
|
783 |
}
|
|
|
784 |
return newNode;
|
|
|
785 |
},
|
|
|
786 |
/**
|
|
|
787 |
* Get all elements inside a selection and wrap them with a new element and return a NodeList of all elements touched.
|
|
|
788 |
* @method wrapContent
|
|
|
789 |
* @param {String} tag The tag to wrap all selected items with.
|
|
|
790 |
* @return {NodeList} A NodeList of all items in the selection.
|
|
|
791 |
*/
|
|
|
792 |
wrapContent: function(tag) {
|
|
|
793 |
tag = (tag) ? tag : Y.EditorSelection.DEFAULT_TAG;
|
|
|
794 |
|
|
|
795 |
if (!this.isCollapsed) {
|
|
|
796 |
Y.log('Wrapping selection with: ' + tag, 'info', 'editor-selection');
|
|
|
797 |
var items = this.getSelected(),
|
|
|
798 |
changed = [], range, last, first, range2;
|
|
|
799 |
|
|
|
800 |
items.each(function(n, k) {
|
|
|
801 |
var t = n.get('tagName').toLowerCase();
|
|
|
802 |
if (t === 'font') {
|
|
|
803 |
changed.push(this._swap(items.item(k), tag));
|
|
|
804 |
} else {
|
|
|
805 |
changed.push(this._wrap(items.item(k), tag));
|
|
|
806 |
}
|
|
|
807 |
}, this);
|
|
|
808 |
|
|
|
809 |
range = this.createRange();
|
|
|
810 |
first = changed[0];
|
|
|
811 |
last = changed[changed.length - 1];
|
|
|
812 |
if (this._selection.removeAllRanges) {
|
|
|
813 |
range.setStart(changed[0], 0);
|
|
|
814 |
range.setEnd(last, last.childNodes.length);
|
|
|
815 |
this._selection.removeAllRanges();
|
|
|
816 |
this._selection.addRange(range);
|
|
|
817 |
} else {
|
|
|
818 |
if (range.moveToElementText) {
|
|
|
819 |
range.moveToElementText(Y.Node.getDOMNode(first));
|
|
|
820 |
range2 = this.createRange();
|
|
|
821 |
range2.moveToElementText(Y.Node.getDOMNode(last));
|
|
|
822 |
range.setEndPoint('EndToEnd', range2);
|
|
|
823 |
}
|
|
|
824 |
range.select();
|
|
|
825 |
}
|
|
|
826 |
|
|
|
827 |
changed = Y.all(changed);
|
|
|
828 |
Y.log('Returning NodeList with (' + changed.size() + ') item(s)' , 'info', 'editor-selection');
|
|
|
829 |
return changed;
|
|
|
830 |
|
|
|
831 |
|
|
|
832 |
} else {
|
|
|
833 |
Y.log('Can not wrap a collapsed selection, use insertContent', 'error', 'editor-selection');
|
|
|
834 |
return Y.all([]);
|
|
|
835 |
}
|
|
|
836 |
},
|
|
|
837 |
/**
|
|
|
838 |
* Find and replace a string inside a text node and replace it with HTML focusing the node after
|
|
|
839 |
* to allow you to continue to type.
|
|
|
840 |
* @method replace
|
|
|
841 |
* @param {String} se The string to search for.
|
|
|
842 |
* @param {String} re The string of HTML to replace it with.
|
|
|
843 |
* @return {Node} The node inserted.
|
|
|
844 |
*/
|
|
|
845 |
replace: function(se,re) {
|
|
|
846 |
Y.log('replacing (' + se + ') with (' + re + ')');
|
|
|
847 |
var range = this.createRange(), node, txt, index, newNode;
|
|
|
848 |
|
|
|
849 |
if (range.getBookmark) {
|
|
|
850 |
index = range.getBookmark();
|
|
|
851 |
txt = this.anchorNode.get('innerHTML').replace(se, re);
|
|
|
852 |
this.anchorNode.set('innerHTML', txt);
|
|
|
853 |
range.moveToBookmark(index);
|
|
|
854 |
newNode = Y.one(range.parentElement());
|
|
|
855 |
} else {
|
|
|
856 |
node = this.anchorTextNode;
|
|
|
857 |
txt = node.get(textContent);
|
|
|
858 |
index = txt.indexOf(se);
|
|
|
859 |
|
|
|
860 |
txt = txt.replace(se, '');
|
|
|
861 |
node.set(textContent, txt);
|
|
|
862 |
newNode = this.insertAtCursor(re, node, index, true);
|
|
|
863 |
}
|
|
|
864 |
return newNode;
|
|
|
865 |
},
|
|
|
866 |
/**
|
|
|
867 |
* Destroy the range.
|
|
|
868 |
* @method remove
|
|
|
869 |
* @chainable
|
|
|
870 |
* @return {EditorSelection}
|
|
|
871 |
*/
|
|
|
872 |
remove: function() {
|
|
|
873 |
if (this._selection && this._selection.removeAllRanges) {
|
|
|
874 |
this._selection.removeAllRanges();
|
|
|
875 |
}
|
|
|
876 |
return this;
|
|
|
877 |
},
|
|
|
878 |
/**
|
|
|
879 |
* Wrapper for the different range creation methods.
|
|
|
880 |
* @method createRange
|
|
|
881 |
* @return {Range}
|
|
|
882 |
*/
|
|
|
883 |
createRange: function() {
|
|
|
884 |
if (Y.config.doc.selection) {
|
|
|
885 |
return Y.config.doc.selection.createRange();
|
|
|
886 |
} else {
|
|
|
887 |
return Y.config.doc.createRange();
|
|
|
888 |
}
|
|
|
889 |
},
|
|
|
890 |
/**
|
|
|
891 |
* Select a Node (hilighting it).
|
|
|
892 |
* @method selectNode
|
|
|
893 |
* @param {Node} node The node to select
|
|
|
894 |
* @param {Boolean} collapse Should the range be collapsed after insertion. default: false
|
|
|
895 |
* @chainable
|
|
|
896 |
* @return {EditorSelection}
|
|
|
897 |
*/
|
|
|
898 |
selectNode: function(node, collapse, end) {
|
|
|
899 |
if (!node) {
|
|
|
900 |
Y.log('Node passed to selectNode is null', 'error', 'editor-selection');
|
|
|
901 |
return;
|
|
|
902 |
}
|
|
|
903 |
end = end || 0;
|
|
|
904 |
node = Y.Node.getDOMNode(node);
|
|
|
905 |
var range = this.createRange();
|
|
|
906 |
if (range.selectNode) {
|
|
|
907 |
try {
|
|
|
908 |
range.selectNode(node);
|
|
|
909 |
} catch (err) {
|
|
|
910 |
// Ignore selection errors like INVALID_NODE_TYPE_ERR
|
|
|
911 |
}
|
|
|
912 |
this._selection.removeAllRanges();
|
|
|
913 |
this._selection.addRange(range);
|
|
|
914 |
if (collapse) {
|
|
|
915 |
try {
|
|
|
916 |
this._selection.collapse(node, end);
|
|
|
917 |
} catch (err) {
|
|
|
918 |
this._selection.collapse(node, 0);
|
|
|
919 |
}
|
|
|
920 |
}
|
|
|
921 |
} else {
|
|
|
922 |
if (node.nodeType === 3) {
|
|
|
923 |
node = node.parentNode;
|
|
|
924 |
}
|
|
|
925 |
try {
|
|
|
926 |
range.moveToElementText(node);
|
|
|
927 |
} catch(e) {}
|
|
|
928 |
if (collapse) {
|
|
|
929 |
range.collapse(((end) ? false : true));
|
|
|
930 |
}
|
|
|
931 |
range.select();
|
|
|
932 |
}
|
|
|
933 |
return this;
|
|
|
934 |
},
|
|
|
935 |
/**
|
|
|
936 |
* Put a placeholder in the DOM at the current cursor position.
|
|
|
937 |
* @method setCursor
|
|
|
938 |
* @return {Node}
|
|
|
939 |
*/
|
|
|
940 |
setCursor: function() {
|
|
|
941 |
this.removeCursor(false);
|
|
|
942 |
return this.insertContent(Y.EditorSelection.CURSOR);
|
|
|
943 |
},
|
|
|
944 |
/**
|
|
|
945 |
* Get the placeholder in the DOM at the current cursor position.
|
|
|
946 |
* @method getCursor
|
|
|
947 |
* @return {Node}
|
|
|
948 |
*/
|
|
|
949 |
getCursor: function() {
|
|
|
950 |
return Y.EditorSelection.ROOT.all('.' + Y.EditorSelection.CURID).get('parentNode');
|
|
|
951 |
},
|
|
|
952 |
/**
|
|
|
953 |
* Remove the cursor placeholder from the DOM.
|
|
|
954 |
* @method removeCursor
|
|
|
955 |
* @param {Boolean} keep Setting this to true will keep the node, but remove the unique parts that make it the cursor.
|
|
|
956 |
* @return {Node}
|
|
|
957 |
*/
|
|
|
958 |
removeCursor: function(keep) {
|
|
|
959 |
var cur = this.getCursor();
|
|
|
960 |
if (cur && cur.remove) {
|
|
|
961 |
if (keep) {
|
|
|
962 |
cur.set('innerHTML', '<br class="yui-cursor">');
|
|
|
963 |
} else {
|
|
|
964 |
cur.remove();
|
|
|
965 |
}
|
|
|
966 |
}
|
|
|
967 |
return cur;
|
|
|
968 |
},
|
|
|
969 |
/**
|
|
|
970 |
* Gets a stored cursor and focuses it for editing, must be called sometime after setCursor
|
|
|
971 |
* @method focusCursor
|
|
|
972 |
* @return {Node}
|
|
|
973 |
*/
|
|
|
974 |
focusCursor: function(collapse, end) {
|
|
|
975 |
if (collapse !== false) {
|
|
|
976 |
collapse = true;
|
|
|
977 |
}
|
|
|
978 |
if (end !== false) {
|
|
|
979 |
end = true;
|
|
|
980 |
}
|
|
|
981 |
var cur = this.removeCursor(true);
|
|
|
982 |
if (cur) {
|
|
|
983 |
cur.each(function(c) {
|
|
|
984 |
this.selectNode(c, collapse, end);
|
|
|
985 |
}, this);
|
|
|
986 |
}
|
|
|
987 |
},
|
|
|
988 |
/**
|
|
|
989 |
* Generic toString for logging.
|
|
|
990 |
* @method toString
|
|
|
991 |
* @return {String}
|
|
|
992 |
*/
|
|
|
993 |
toString: function() {
|
|
|
994 |
return 'EditorSelection Object';
|
|
|
995 |
},
|
|
|
996 |
|
|
|
997 |
/**
|
|
|
998 |
Gets the offset of the selection for the selection within the current
|
|
|
999 |
editor
|
|
|
1000 |
@public
|
|
|
1001 |
@method getEditorOffset
|
|
|
1002 |
@param {Y.Node} [node] Element used to measure the offset to
|
|
|
1003 |
@return Number Number of characters the selection is from the beginning
|
|
|
1004 |
@since 3.13.0
|
|
|
1005 |
*/
|
|
|
1006 |
getEditorOffset: function(node) {
|
|
|
1007 |
var container = (node || Y.EditorSelection.ROOT).getDOMNode(),
|
|
|
1008 |
caretOffset = 0,
|
|
|
1009 |
doc = Y.config.doc,
|
|
|
1010 |
win = Y.config.win,
|
|
|
1011 |
sel,
|
|
|
1012 |
range,
|
|
|
1013 |
preCaretRange;
|
|
|
1014 |
|
|
|
1015 |
if (typeof win.getSelection !== "undefined") {
|
|
|
1016 |
range = win.getSelection().getRangeAt(0);
|
|
|
1017 |
preCaretRange = range.cloneRange();
|
|
|
1018 |
preCaretRange.selectNodeContents(container);
|
|
|
1019 |
preCaretRange.setEnd(range.endContainer, range.endOffset);
|
|
|
1020 |
caretOffset = preCaretRange.toString().length;
|
|
|
1021 |
} else {
|
|
|
1022 |
sel = doc.selection;
|
|
|
1023 |
|
|
|
1024 |
if ( sel && sel.type !== "Control") {
|
|
|
1025 |
range = sel.createRange();
|
|
|
1026 |
preCaretRange = doc.body.createTextRange();
|
|
|
1027 |
preCaretRange.moveToElementText(container);
|
|
|
1028 |
preCaretRange.setEndPoint("EndToEnd", range);
|
|
|
1029 |
caretOffset = preCaretRange.text.length;
|
|
|
1030 |
}
|
|
|
1031 |
}
|
|
|
1032 |
|
|
|
1033 |
return caretOffset;
|
|
|
1034 |
}
|
|
|
1035 |
};
|
|
|
1036 |
|
|
|
1037 |
//TODO Remove this alias in 3.6.0
|
|
|
1038 |
Y.Selection = Y.EditorSelection;
|
|
|
1039 |
|
|
|
1040 |
|
|
|
1041 |
|
|
|
1042 |
}, '3.18.1', {"requires": ["node"]});
|