2 Copyright (c) 2010, Yahoo! Inc. All rights reserved.
3 Code licensed under the BSD License:
4 http://developer.yahoo.com/yui/license.html
8 YUI.add('editor-para', function(Y) {
13 * Plugin for Editor to paragraph auto wrapping and correction.
15 * @submodule editor-para
18 * Plugin for Editor to paragraph auto wrapping and correction.
19 * @class Plugin.EditorPara
25 var EditorPara = function() {
26 EditorPara.superclass.constructor.apply(this, arguments);
27 }, HOST = 'host', BODY = 'body', NODE_CHANGE = 'nodeChange', PARENT_NODE = 'parentNode',
28 FIRST_P = BODY + ' > p', P = 'p', BR = '<br>', FC = 'firstChild', LI = 'li';
31 Y.extend(EditorPara, Y.Base, {
33 * Utility method to create an empty paragraph when the document is empty.
35 * @method _fixFirstPara
37 _fixFirstPara: function() {
38 var host = this.get(HOST), inst = host.getInstance(), sel;
39 inst.one('body').set('innerHTML', '<' + P + '>' + inst.Selection.CURSOR + '</' + P + '>');
40 var n = inst.one(FIRST_P);
41 sel = new inst.Selection();
42 sel.selectNode(n, true, false);
46 * nodeChange handler to handle fixing an empty document.
48 * @method _onNodeChange
50 _onNodeChange: function(e) {
51 var host = this.get(HOST), inst = host.getInstance(),
52 html, txt, par , d, sel, btag = inst.Selection.DEFAULT_BLOCK_TAG,
53 inHTML, txt2, childs, aNode, index, node2, top, n, sib,
54 ps, br, item, p, imgs, t, LAST_CHILD = ':last-child';
56 switch (e.changedType) {
58 var para = ((this._lastPara) ? this._lastPara : e.changedNode),
59 b = para.one('br.yui-cursor');
62 delete this._lastPara;
66 if (b.previous() || b.next()) {
70 if (!para.test(btag)) {
71 var para2 = para.ancestor(btag);
77 if (para.test(btag)) {
78 var prev = para.previous(), lc, lc2, found = false;
80 lc = prev.one(LAST_CHILD);
83 lc2 = lc.one(LAST_CHILD);
94 host.copyStyles(lc, para);
101 //Webkit doesn't support shift+enter as a BR, this fixes that.
102 if (e.changedEvent.shiftKey) {
103 host.execCommand('insertbr');
104 e.changedEvent.preventDefault();
107 //TODO Move this to a GECKO MODULE - Can't for the moment, requires no change to metadata (YMAIL)
108 if (Y.UA.gecko && host.get('defaultblock') !== 'p') {
111 if (!par.test(LI) && !par.ancestor(LI)) {
112 if (!par.test(btag)) {
113 par = par.ancestor(btag);
115 d = inst.Node.create('<' + btag + '></' + btag + '>');
116 par.insert(d, 'after');
117 sel = new inst.Selection();
118 if (sel.anchorOffset) {
119 inHTML = sel.anchorNode.get('textContent');
121 txt = inst.one(inst.config.doc.createTextNode(inHTML.substr(0, sel.anchorOffset)));
122 txt2 = inst.one(inst.config.doc.createTextNode(inHTML.substr(sel.anchorOffset)));
124 aNode = sel.anchorNode;
125 aNode.setContent(''); //I
126 node2 = aNode.cloneNode(); //I
127 node2.append(txt2); //text
131 sib = sib.get(PARENT_NODE); //B
132 if (sib && !sib.test(btag)) {
134 n.set('innerHTML', '');
138 childs = sib.get('childNodes');
140 childs.each(function(c) {
149 aNode = sib; //Top sibling
156 sel.anchorNode.append(txt);
165 d.prepend(inst.Selection.CURSOR);
166 sel.focusCursor(true, true);
167 html = inst.Selection.getText(d);
169 inst.Selection.cleanCursor();
171 e.changedEvent.preventDefault();
176 if (inst.config.doc.childNodes.length < 2) {
177 var cont = inst.config.doc.body.innerHTML;
178 if (cont && cont.length < 5 && cont.toLowerCase() == BR) {
179 this._fixFirstPara();
184 case 'backspace-down':
187 ps = inst.all(FIRST_P);
188 item = inst.one(BODY);
194 br.removeAttribute('id');
195 br.removeAttribute('class');
198 txt = inst.Selection.getText(item);
199 txt = txt.replace(/ /g, '').replace(/\n/g, '');
200 imgs = item.all('img');
202 if (txt.length === 0 && !imgs.size()) {
203 //God this is horrible..
205 this._fixFirstPara();
208 if (e.changedNode && e.changedNode.test(P)) {
211 if (!p && host._lastPara && host._lastPara.inDoc()) {
214 if (p && !p.test(P)) {
218 if (!p.previous() && p.get(PARENT_NODE) && p.get(PARENT_NODE).test(BODY)) {
219 e.changedEvent.frameEvent.halt();
225 item = e.changedNode;
226 if (item.test('li') && (!item.previous() && !item.next())) {
227 html = item.get('innerHTML').replace(BR, '');
229 if (item.get(PARENT_NODE)) {
230 item.get(PARENT_NODE).replace(inst.Node.create(BR));
231 e.changedEvent.frameEvent.halt();
233 inst.Selection.filterBlocks();
242 * This forced FF to redraw the content on backspace.
243 * On some occasions FF will leave a cursor residue after content has been deleted.
244 * Dropping in the empty textnode and then removing it causes FF to redraw and
245 * remove the "ghost cursors"
248 t = inst.config.doc.createTextNode(' ');
255 if (e.changedNode && !e.changedNode.test(btag)) {
256 var p = e.changedNode.ancestor(btag);
265 * Performs a block element filter when the Editor is first ready
267 * @method _afterEditorReady
269 _afterEditorReady: function() {
270 var host = this.get(HOST), inst = host.getInstance(), btag;
272 inst.Selection.filterBlocks();
273 btag = inst.Selection.DEFAULT_BLOCK_TAG;
274 FIRST_P = BODY + ' > ' + btag;
279 * Performs a block element filter when the Editor after an content change
281 * @method _afterContentChange
283 _afterContentChange: function() {
284 var host = this.get(HOST), inst = host.getInstance();
285 if (inst && inst.Selection) {
286 inst.Selection.filterBlocks();
290 * Performs block/paste filtering after paste.
292 * @method _afterPaste
294 _afterPaste: function() {
295 var host = this.get(HOST), inst = host.getInstance(),
296 sel = new inst.Selection();
298 Y.later(50, host, function() {
299 inst.Selection.filterBlocks();
303 initializer: function() {
304 var host = this.get(HOST);
306 Y.error('Can not plug EditorPara and EditorBR at the same time.');
310 host.on(NODE_CHANGE, Y.bind(this._onNodeChange, this));
311 host.after('ready', Y.bind(this._afterEditorReady, this));
312 host.after('contentChange', Y.bind(this._afterContentChange, this));
314 host.after('dom:paste', Y.bind(this._afterPaste, this));
337 Y.namespace('Plugin');
339 Y.Plugin.EditorPara = EditorPara;
343 }, '3.3.0' ,{requires:['node'], skinnable:false});