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('frame', function(Y) {
11
* Creates a wrapper around an iframe. It loads the content either from a local
12
* file or from script and creates a local YUI instance bound to that new window and document.
17
* Creates a wrapper around an iframe. It loads the content either from a local
18
* file or from script and creates a local YUI instance bound to that new window and document.
25
var Frame = function() {
26
Frame.superclass.constructor.apply(this, arguments);
29
Y.extend(Frame, Y.Base, {
33
* @description Internal reference set when the content is ready.
40
* @description Internal reference set when render is called.
47
* @description Internal Node reference to the iFrame or the window
54
* @description Internal reference to the YUI instance bound to the iFrame or window
61
* @description Create the iframe or Window and get references to the Document & Window
62
* @return {Object} Hash table containing references to the new Document & Window
64
_create: function(cb) {
65
var win, doc, res, node;
67
this._iframe = Y.Node.create(Frame.HTML);
68
this._iframe.setStyle('visibility', 'hidden');
69
this._iframe.set('src', this.get('src'));
70
this.get('container').append(this._iframe);
74
extra_css = ((this.get('extracss')) ? '<style id="extra_css">' + this.get('extracss') + '</style>' : '');
76
Y.log('Creating the document from javascript', 'info', 'frame');
77
html = Y.substitute(Frame.PAGE_HTML, {
79
LANG: this.get('lang'),
80
TITLE: this.get('title'),
82
CONTENT: this.get('content'),
83
BASE_HREF: this.get('basehref'),
84
DEFAULT_CSS: Frame.DEFAULT_CSS,
87
if (Y.config.doc.compatMode != 'BackCompat') {
88
Y.log('Adding Doctype to frame', 'info', 'frame');
89
html = Frame.DOC_TYPE + "\n" + html;
91
Y.log('DocType skipped because we are in BackCompat Mode.', 'warn', 'frame');
94
Y.log('Injecting content into iframe', 'info', 'frame');
97
res = this._resolveWinDoc();
102
if (this.get('designMode')) {
103
res.doc.designMode = 'on';
106
if (!res.doc.documentElement) {
107
Y.log('document.documentElement was not found, running timer', 'warn', 'frame');
108
var timer = Y.later(1, this, function() {
109
if (res.doc && res.doc.documentElement) {
110
Y.log('document.documentElement found inside timer', 'info', 'frame');
116
Y.log('document.documentElement found', 'info', 'frame');
123
* @method _resolveWinDoc
124
* @description Resolves the document and window from an iframe or window instance
125
* @param {Object} c The YUI Config to add the window and document to
126
* @return {Object} Object hash of window and document references, if a YUI config was passed, it is returned.
128
_resolveWinDoc: function(c) {
129
var config = (c) ? c : {};
130
config.win = Y.Node.getDOMNode(this._iframe.get('contentWindow'));
131
config.doc = Y.Node.getDOMNode(this._iframe.get('contentWindow.document'));
133
config.doc = Y.config.doc;
136
config.win = Y.config.win;
142
* @method _onDomEvent
143
* @description Generic handler for all DOM events fired by the iframe or window. This handler
144
* takes the current EventFacade and augments it to fire on the Frame host. It adds two new properties
145
* to the EventFacade called frameX and frameY which adds the scroll and xy position of the iframe
146
* to the original pageX and pageY of the event so external nodes can be positioned over the frame.
147
* @param {Event.Facade} e
149
_onDomEvent: function(e) {
152
//Y.log('onDOMEvent: ' + e.type, 'info', 'frame');
153
e.frameX = e.frameY = 0;
155
if (e.pageX > 0 || e.pageY > 0) {
156
if (e.type.substring(0, 3) !== 'key') {
157
node = this._instance.one('win');
158
xy = this._iframe.getXY()
159
e.frameX = xy[0] + e.pageX - node.get('scrollLeft');
160
e.frameY = xy[1] + e.pageY - node.get('scrollTop');
164
e.frameTarget = e.target;
165
e.frameCurrentTarget = e.currentTarget;
168
this.fire('dom:' + e.type, e);
170
initializer: function() {
171
this.publish('ready', {
173
defaultFn: this._defReadyFn
176
destructor: function() {
177
var inst = this.getInstance();
179
inst.one('doc').detachAll();
181
this._iframe.remove();
186
* @description Simple pass thru handler for the paste event so we can do content cleanup
187
* @param {Event.Facade} e
189
_DOMPaste: function(e) {
190
var inst = this.getInstance(),
191
data = '', win = inst.config.win;
193
if (e._event.originalTarget) {
194
data = e._event.originalTarget;
196
if (e._event.clipboardData) {
197
data = e._event.clipboardData.getData('Text');
200
if (win.clipboardData) {
201
data = win.clipboardData.getData('Text');
202
if (data == '') { // Could be empty, or failed
204
if (!win.clipboardData.setData('Text', data)) {
211
e.frameTarget = e.target;
212
e.frameCurrentTarget = e.currentTarget;
218
getData: function() {
223
Y.log('Failed to collect clipboard data', 'warn', 'frame');
224
e.clipboardData = null;
227
this.fire('dom:paste', e);
231
* @method _defReadyFn
232
* @description Binds DOM events, sets the iframe to visible and fires the ready event
234
_defReadyFn: function() {
235
var inst = this.getInstance(),
236
fn = Y.bind(this._onDomEvent, this),
237
kfn = ((Y.UA.ie) ? Y.throttle(fn, 200) : fn);
239
inst.Node.DOM_EVENTS.activate = 1;
240
inst.Node.DOM_EVENTS.focusin = 1;
241
inst.Node.DOM_EVENTS.deactivate = 1;
242
inst.Node.DOM_EVENTS.focusout = 1;
244
//Y.each(inst.Node.DOM_EVENTS, function(v, k) {
245
Y.each(Frame.DOM_EVENTS, function(v, k) {
247
if (k !== 'focus' && k !== 'blur' && k !== 'paste') {
248
//Y.log('Adding DOM event to frame: ' + k, 'info', 'frame');
249
if (k.substring(0, 3) === 'key') {
250
inst.on(k, kfn, inst.config.doc);
252
inst.on(k, fn, inst.config.doc);
258
inst.Node.DOM_EVENTS.paste = 1;
260
inst.on('paste', Y.bind(this._DOMPaste, this), inst.one('body'));
262
//Adding focus/blur to the window object
263
inst.on('focus', fn, inst.config.win);
264
inst.on('blur', fn, inst.config.win);
266
inst._use = inst.use;
267
inst.use = Y.bind(this.use, this);
269
this._iframe.setStyles({
270
visibility: 'inherit'
272
inst.one('body').setStyle('display', 'block');
276
* @method _onContentReady
277
* @description Called once the content is available in the frame/window and calls the final use call
278
* on the internal instance so that the modules are loaded properly.
280
_onContentReady: function(e) {
283
var inst = this.getInstance(),
284
args = Y.clone(this.get('use'));
286
this.fire('contentready');
288
Y.log('On available for body of iframe', 'info', 'frame');
290
inst.config.doc = Y.Node.getDOMNode(e.target);
292
//TODO Circle around and deal with CSS loading...
293
args.push(Y.bind(function() {
294
Y.log('Callback from final internal use call', 'info', 'frame');
297
Y.log('Calling use on internal instance: ' + args, 'info', 'frame');
298
inst.use.apply(inst, args);
300
inst.one('doc').get('documentElement').addClass('yui-js-enabled');
305
* @method _resolveBaseHref
306
* @description Resolves the basehref of the page the frame is created on. Only applies to dynamic content.
307
* @param {String} href The new value to use, if empty it will be resolved from the current url.
310
_resolveBaseHref: function(href) {
311
if (!href || href === '') {
312
href = Y.config.doc.location.href;
313
if (href.indexOf('?') !== -1) { //Remove the query string
314
href = href.substring(0, href.indexOf('?'));
316
href = href.substring(0, href.lastIndexOf('/')) + '/';
323
* @description Get the content from the iframe
324
* @param {String} html The raw HTML from the body of the iframe.
327
_getHTML: function(html) {
329
var inst = this.getInstance();
330
html = inst.one('body').get('innerHTML');
337
* @description Set the content of the iframe
338
* @param {String} html The raw HTML to set the body of the iframe to.
341
_setHTML: function(html) {
343
var inst = this.getInstance();
344
inst.one('body').set('innerHTML', html);
346
//This needs to be wrapped in a contentready callback for the !_ready state
347
this.on('contentready', Y.bind(function(html, e) {
348
var inst = this.getInstance();
349
inst.one('body').set('innerHTML', html);
356
* @method _setExtraCSS
357
* @description Set's the extra CSS on the instance..
359
_setExtraCSS: function(css) {
361
var inst = this.getInstance(),
362
node = inst.get('#extra_css');
365
inst.one('head').append('<style id="extra_css">' + css + '</style>');
371
* @method _instanceLoaded
372
* @description Called from the first YUI instance that sets up the internal instance.
373
* This loads the content into the window/frame and attaches the contentready event.
374
* @param {YUI} inst The internal YUI instance bound to the frame/window
376
_instanceLoaded: function(inst) {
377
this._instance = inst;
379
this._onContentReady();
381
var doc = this._instance.config.doc;
383
if (this.get('designMode')) {
386
//Force other browsers into non CSS styling
387
doc.execCommand('styleWithCSS', false, false);
388
doc.execCommand('insertbronreturn', false, false);
393
//BEGIN PUBLIC METHODS
396
* @description This is a scoped version of the normal YUI.use method & is bound to this frame/window.
397
* At setup, the inst.use method is mapped to this method.
400
Y.log('Calling augmented use after ready', 'info', 'frame');
401
var inst = this.getInstance(),
402
args = Y.Array(arguments),
405
if (Y.Lang.isFunction(args[args.length - 1])) {
409
args.push(function() {
410
Y.log('Internal callback from augmented use', 'info', 'frame');
411
cb.apply(inst, arguments);
415
inst._use.apply(inst, args);
419
* @description A delegate method passed to the instance's delegate method
420
* @param {String} type The type of event to listen for
421
* @param {Function} fn The method to attach
422
* @param {String} cont The container to act as a delegate, if no "sel" passed, the body is assumed as the container.
423
* @param {String} sel The selector to match in the event (optional)
424
* @return {EventHandle} The Event handle returned from Y.delegate
426
delegate: function(type, fn, cont, sel) {
427
var inst = this.getInstance();
429
Y.log('Delegate events can not be attached until after the ready event has fired.', 'error', 'iframe');
436
return inst.delegate(type, fn, cont, sel);
439
* @method getInstance
440
* @description Get a reference to the internal YUI instance.
441
* @return {YUI} The internal YUI instance
443
getInstance: function() {
444
return this._instance;
448
* @description Render the iframe into the container config option or open the window.
449
* @param {String/HTMLElement/Node} node The node to render to
453
render: function(node) {
454
if (this._rendered) {
455
Y.log('Frame already rendered.', 'warn', 'frame');
458
this._rendered = true;
460
this.set('container', node);
463
this._create(Y.bind(function(res) {
465
cb = Y.bind(function(i) {
466
Y.log('Internal instance loaded with node-base', 'info', 'frame');
467
this._instanceLoaded(i);
469
args = Y.clone(this.get('use')),
475
fn = Y.bind(function() {
476
Y.log('New Modules Loaded into main instance', 'info', 'frame');
477
config = this._resolveWinDoc(config);
479
inst.log = Y.log; //Dump the instance logs to the parent instance.
481
Y.log('Creating new internal instance with node-base only', 'info', 'frame');
483
inst.use('node-base', cb);
485
clearInterval(timer);
488
timer = setInterval(function() {
489
Y.log('[TIMER] Internal use call failed, retrying', 'info', 'frame');
492
Y.log('Internal use call failed, retrying', 'info', 'frame');
498
Y.log('Adding new modules to main instance: ' + args, 'info', 'frame');
499
Y.use.apply(Y, args);
507
* @description Set the focus to the iframe
508
* @param {Function} fn Callback function to execute after focus happens
512
focus: function(fn) {
514
Y.one('win').focus();
515
this.getInstance().one('win').focus();
516
if (Y.Lang.isFunction(fn)) {
521
Y.one('win').focus();
522
Y.later(100, this, function() {
523
this.getInstance().one('win').focus();
524
if (Y.Lang.isFunction(fn)) {
529
Y.log('Frame focus failed', 'warn', 'frame');
536
* @description Show the iframe instance
541
this._iframe.setStyles({
547
this._instance.config.doc.designMode = 'on';
555
* @description Hide the iframe instance
560
this._iframe.setStyles({
561
position: 'absolute',
570
* @property DOM_EVENTS
571
* @description The DomEvents that the frame automatically attaches and bubbles
589
* @property DEFAULT_CSS
590
* @description The default css used when creating the document.
593
DEFAULT_CSS: 'html { height: 95%; } body { padding: 7px; background-color: #fff; font: 13px/1.22 arial,helvetica,clean,sans-serif;*font-size:small;*font:x-small; } a, a:visited, a:hover { color: blue !important; text-decoration: underline !important; cursor: text !important; } img { cursor: pointer !important; border: none; }',
595
//DEFAULT_CSS: 'html { } body { margin: -15px 0 0 -15px; padding: 7px 0 0 15px; display: block; background-color: #fff; font: 13px/1.22 arial,helvetica,clean,sans-serif;*font-size:small;*font:x-small; }',
596
//DEFAULT_CSS: 'html { height: 95%; } body { height: 100%; padding: 7px; margin: 0 0 0 -7px; postion: relative; background-color: #fff; font: 13px/1.22 arial,helvetica,clean,sans-serif;*font-size:small;*font:x-small; } a, a:visited, a:hover { color: blue !important; text-decoration: underline !important; cursor: text !important; } img { cursor: pointer !important; border: none; }',
597
//DEFAULT_CSS: 'html { margin: 0; padding: 0; border: none; border-size: 0; } body { height: 97%; margin: 0; padding: 0; display: block; background-color: gray; font: 13px/1.22 arial,helvetica,clean,sans-serif;*font-size:small;*font:x-small; }',
601
* @description The template string used to create the iframe
604
HTML: '<iframe border="0" frameBorder="0" marginWidth="0" marginHeight="0" leftMargin="0" topMargin="0" allowTransparency="true" width="100%" height="99%"></iframe>',
605
//HTML: '<iframe border="0" frameBorder="0" width="100%" height="99%"></iframe>',
608
* @property PAGE_HTML
609
* @description The template used to create the page when created dynamically.
612
PAGE_HTML: '<html dir="{DIR}" lang="{LANG}"><head><title>{TITLE}</title>{META}<base href="{BASE_HREF}"/><style id="editor_css">{DEFAULT_CSS}</style>{EXTRA_CSS}</head><body>{CONTENT}</body></html>',
616
* @description The DOCTYPE to prepend to the new document when created. Should match the one on the page being served.
619
DOC_TYPE: '<!DOCTYPE HTML PUBLIC "-/'+'/W3C/'+'/DTD HTML 4.01/'+'/EN" "http:/'+'/www.w3.org/TR/html4/strict.dtd">',
623
* @description The meta-tag for Content-Type to add to the dynamic document
626
META: '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/><meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7">',
627
//META: '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>',
631
* @description The name of the class (frame)
638
* @description The title to give the blank page.
646
* @description The default text direction for this new frame. Default: ltr
654
* @description The default language. Default: en-US
662
* @description The src of the iframe/window. Defaults to javascript:;
666
//Hackish, IE needs the false in the Javascript URL
667
value: 'javascript' + ((Y.UA.ie) ? ':false' : ':') + ';'
670
* @attribute designMode
671
* @description Should designMode be turned on after creation.
681
* @description The string to inject into the body of the new frame/window.
690
* @attribute basehref
691
* @description The base href to use in the iframe.
696
getter: '_resolveBaseHref'
700
* @description Array of modules to include in the scoped YUI instance at render time. Default: ['none', 'selector-css2']
706
value: ['substitute', 'node', 'node-style', 'selector-css3']
709
* @attribute container
710
* @description The container to append the iFrame to on render.
711
* @type String/HTMLElement/Node
715
setter: function(n) {
721
* @description Set the id of the new Node. (optional)
727
getter: function(id) {
729
id = 'iframe-' + Y.guid();
735
* @attribute extracss
736
* @description A string of CSS to add to the Head of the Editor
741
setter: '_setExtraCSS'
745
* @description A reference to the Editor instance
759
}, '3.2.0' ,{skinnable:false, requires:['base', 'node', 'selector-css3', 'substitute']});
760
YUI.add('selection', function(Y) {
763
* Wraps some common Selection/Range functionality into a simple object
765
* @submodule selection
768
* Wraps some common Selection/Range functionality into a simple object
774
//TODO This shouldn't be there, Y.Node doesn't normalize getting textnode content.
775
var textContent = 'textContent',
776
INNER_HTML = 'innerHTML',
777
FONT_FAMILY = 'fontFamily';
780
textContent = 'nodeValue';
783
Y.Selection = function(domEvent) {
784
var sel, par, ieNode, nodes, rng, i;
786
if (Y.config.win.getSelection) {
787
sel = Y.config.win.getSelection();
788
} else if (Y.config.doc.selection) {
789
sel = Y.config.doc.selection.createRange();
791
this._selection = sel;
794
this.isCollapsed = (sel.compareEndPoints('StartToEnd', sel)) ? false : true;
795
if (this.isCollapsed) {
796
this.anchorNode = this.focusNode = Y.one(sel.parentElement());
799
ieNode = Y.config.doc.elementFromPoint(domEvent.clientX, domEvent.clientY);
803
par = sel.parentElement();
804
nodes = par.childNodes;
805
rng = sel.duplicate();
807
for (i = 0; i < nodes.length; i++) {
808
//This causes IE to not allow a selection on a doubleclick
809
//rng.select(nodes[i]);
810
if (rng.inRange(sel)) {
816
this.ieNode = ieNode;
819
if (ieNode.nodeType !== 3) {
820
if (ieNode.firstChild) {
821
ieNode = ieNode.firstChild;
824
this.anchorNode = this.focusNode = Y.Selection.resolve(ieNode);
826
this.anchorOffset = this.focusOffset = (this.anchorNode.nodeValue) ? this.anchorNode.nodeValue.length : 0 ;
828
this.anchorTextNode = this.focusTextNode = Y.one(ieNode);
837
this.isCollapsed = sel.isCollapsed;
838
this.anchorNode = Y.Selection.resolve(sel.anchorNode);
839
this.focusNode = Y.Selection.resolve(sel.focusNode);
840
this.anchorOffset = sel.anchorOffset;
841
this.focusOffset = sel.focusOffset;
843
this.anchorTextNode = Y.one(sel.anchorNode);
844
this.focusTextNode = Y.one(sel.focusNode);
846
if (Y.Lang.isString(sel.text)) {
847
this.text = sel.text;
850
this.text = sel.toString();
858
* Performs a prefilter on all nodes in the editor. Looks for nodes with a style: fontFamily or font face
859
* It then creates a dynamic class assigns it and removed the property. This is so that we don't lose
860
* the fontFamily when selecting nodes.
864
Y.Selection.filter = function(blocks) {
865
var startTime = (new Date()).getTime();
866
Y.log('Filtering nodes', 'info', 'selection');
868
var nodes = Y.all(Y.Selection.ALL),
869
baseNodes = Y.all('strong,em'),
871
hrs = doc.getElementsByTagName('hr'),
872
classNames = {}, cssString = '',
875
var startTime1 = (new Date()).getTime();
876
nodes.each(function(n) {
877
var raw = Y.Node.getDOMNode(n);
878
if (raw.style[FONT_FAMILY]) {
879
classNames['.' + n._yuid] = raw.style[FONT_FAMILY];
881
raw.style[FONT_FAMILY] = 'inherit';
883
raw.removeAttribute('face');
884
if (raw.getAttribute('style') === '') {
885
raw.removeAttribute('style');
888
if (raw.getAttribute('style')) {
889
if (raw.getAttribute('style').toLowerCase() === 'font-family: ') {
890
raw.removeAttribute('style');
895
if (n.getStyle(FONT_FAMILY)) {
896
classNames['.' + n._yuid] = n.getStyle(FONT_FAMILY);
898
n.removeAttribute('face');
899
n.setStyle(FONT_FAMILY, '');
900
if (n.getAttribute('style') === '') {
901
n.removeAttribute('style');
904
if (n.getAttribute('style').toLowerCase() === 'font-family: ') {
905
n.removeAttribute('style');
910
var endTime1 = (new Date()).getTime();
911
Y.log('Node Filter Timer: ' + (endTime1 - startTime1) + 'ms', 'info', 'selection');
913
Y.each(hrs, function(hr) {
914
var el = doc.createElement('div');
915
el.className = 'hr yui-non';
916
el.setAttribute('style', 'border: 1px solid #ccc; line-height: 0; font-size: 0;margin-top: 5px; margin-bottom: 5px;');
917
el.setAttribute('readonly', true);
918
el.setAttribute('contenteditable', false); //Keep it from being Edited
920
hr.parentNode.replaceChild(el, hr);
925
Y.each(classNames, function(v, k) {
926
cssString += k + ' { font-family: ' + v.replace(/"/gi, '') + '; }';
928
Y.StyleSheet(cssString, 'editor');
931
//Not sure about this one?
932
baseNodes.each(function(n, k) {
933
var t = n.get('tagName').toLowerCase(),
935
if (t === 'strong') {
938
Y.Selection.prototype._swap(baseNodes.item(k), newTag);
941
//Filter out all the empty UL/OL's
943
ls.each(function(v, k) {
944
var lis = v.all('li');
951
Y.Selection.filterBlocks();
953
var endTime = (new Date()).getTime();
954
Y.log('Filter Timer: ' + (endTime - startTime) + 'ms', 'info', 'selection');
958
* Method attempts to replace all "orphined" text nodes in the main body by wrapping them with a <p>. Called from filter.
960
* @method filterBlocks
962
Y.Selection.filterBlocks = function() {
963
var startTime = (new Date()).getTime();
964
Y.log('RAW filter blocks', 'info', 'selection');
965
var childs = Y.config.doc.body.childNodes, i, node, wrapped = false, doit = true,
966
sel, single, br, divs, spans, c, s;
969
for (i = 0; i < childs.length; i++) {
970
node = Y.one(childs[i]);
971
if (!node.test(Y.Selection.BLOCKS)) {
973
if (childs[i].nodeType == 3) {
974
c = childs[i][textContent].match(Y.Selection.REG_CHAR);
975
s = childs[i][textContent].match(Y.Selection.REG_NON);
976
if (c === null && s) {
985
wrapped.push(childs[i]);
988
wrapped = Y.Selection._wrapBlock(wrapped);
991
wrapped = Y.Selection._wrapBlock(wrapped);
995
if (single.size() === 1) {
996
Y.log('Only One Paragragh, focus it..', 'info', 'selection');
997
br = single.item(0).all('br');
998
if (br.size() === 1) {
1000
var html = single.item(0).get('innerHTML');
1001
if (html == '' || html == ' ') {
1002
single.set('innerHTML', Y.Selection.CURSOR);
1003
sel = new Y.Selection();
1004
sel.focusCursor(true, true);
1008
single.each(function(p) {
1009
var html = p.get('innerHTML');
1011
Y.log('Empty Paragraph Tag Found, Removing It', 'info', 'selection');
1018
divs = Y.all('div, p');
1019
divs.each(function(d) {
1020
if (d.hasClass('yui-non')) {
1023
var html = d.get('innerHTML');
1025
//Y.log('Empty DIV/P Tag Found, Removing It', 'info', 'selection');
1028
//Y.log('DIVS/PS Count: ' + d.get('childNodes').size(), 'info', 'selection');
1029
if (d.get('childNodes').size() == 1) {
1030
//Y.log('This Div/P only has one Child Node', 'info', 'selection');
1031
if (d.ancestor('p')) {
1032
//Y.log('This Div/P is a child of a paragraph, remove it..', 'info', 'selection');
1033
d.replace(d.get('firstChild'));
1039
spans = Y.all('.Apple-style-span, .apple-style-span');
1040
Y.log('Apple Spans found: ' + spans.size(), 'info', 'selection');
1041
spans.each(function(s) {
1042
s.setAttribute('style', '');
1047
var endTime = (new Date()).getTime();
1048
Y.log('FilterBlocks Timer: ' + (endTime - startTime) + 'ms', 'info', 'selection');
1052
* Regular Expression to determine if a string has a character in it
1054
* @property REG_CHAR
1056
Y.Selection.REG_CHAR = /[a-zA-Z-0-9_]/gi;
1059
* Regular Expression to determine if a string has a non-character in it
1063
Y.Selection.REG_NON = /[\s\S|\n|\t]/gi;
1067
* Wraps an array of elements in a Block level tag
1070
* @method _wrapBlock
1072
Y.Selection._wrapBlock = function(wrapped) {
1074
var newChild = Y.Node.create('<p></p>'),
1075
firstChild = Y.one(wrapped[0]), i;
1077
for (i = 1; i < wrapped.length; i++) {
1078
newChild.append(wrapped[i]);
1080
firstChild.replace(newChild);
1081
newChild.prepend(firstChild);
1087
* Undoes what filter does enough to return the HTML from the Editor, then re-applies the filter.
1090
* @return {String} The filtered HTML
1092
Y.Selection.unfilter = function() {
1093
var nodes = Y.all('body [class]'),
1094
html = '', nons, ids;
1096
Y.log('UnFiltering nodes', 'info', 'selection');
1098
nodes.each(function(n) {
1099
if (n.hasClass(n._yuid)) {
1101
n.setStyle(FONT_FAMILY, n.getStyle(FONT_FAMILY));
1102
n.removeClass(n._yuid);
1103
if (n.getAttribute('class') === '') {
1104
n.removeAttribute('class');
1109
nons = Y.all('.yui-non');
1110
nons.each(function(n) {
1111
if (n.get('innerHTML') === '') {
1114
n.removeClass('yui-non');
1118
ids = Y.all('body [id]');
1119
ids.each(function(n) {
1120
if (n.get('id').indexOf('yui_3_') === 0) {
1121
n.removeAttribute('id');
1122
n.removeAttribute('_yuid');
1126
html = Y.one('body').get('innerHTML');
1128
nodes.each(function(n) {
1129
n.addClass(n._yuid);
1130
n.setStyle(FONT_FAMILY, '');
1131
if (n.getAttribute('style') === '') {
1132
n.removeAttribute('style');
1139
* Resolve a node from the selection object and return a Node instance
1142
* @param {HTMLElement} n The HTMLElement to resolve. Might be a TextNode, gives parentNode.
1143
* @return {Node} The Resolved node
1145
Y.Selection.resolve = function(n) {
1146
if (n && n.nodeType === 3) {
1153
* Returns the innerHTML of a node with all HTML tags removed.
1156
* @param {Node} node The Node instance to remove the HTML from
1157
* @return {String} The string of text
1159
Y.Selection.getText = function(node) {
1160
var t = node.get('innerHTML').replace(Y.Selection.STRIP_HTML, ''),
1161
c = t.match(Y.Selection.REG_CHAR),
1162
s = t.match(Y.Selection.REG_NON);
1163
if (c === null && s) {
1170
* The selector to use when looking for Nodes to cache the value of: [style],font[face]
1174
Y.Selection.ALL = '[style],font[face]';
1177
* RegExp used to strip HTML tags from a string
1179
* @property STRIP_HTML
1181
Y.Selection.STRIP_HTML = /<\S[^><]*>/g;
1184
* The selector to use when looking for block level items.
1188
Y.Selection.BLOCKS = 'p,div,ul,ol,table,style';
1190
* The temporary fontname applied to a selection to retrieve their values: yui-tmp
1194
Y.Selection.TMP = 'yui-tmp';
1196
* The default tag to use when creating elements: span
1198
* @property DEFAULT_TAG
1200
Y.Selection.DEFAULT_TAG = 'span';
1203
* The id of the outer cursor wrapper
1205
* @property DEFAULT_TAG
1207
Y.Selection.CURID = 'yui-cursor';
1210
* The id used to wrap the inner space of the cursor position
1212
* @property CUR_WRAPID
1214
Y.Selection.CUR_WRAPID = 'yui-cursor-wrapper';
1217
* The default HTML used to focus the cursor..
1221
Y.Selection.CURSOR = '<span id="' + Y.Selection.CURID + '"><span id="' + Y.Selection.CUR_WRAPID + '"> </span></span>';
1224
* Called from Editor keydown to remove the "extra" space before the cursor.
1226
* @method cleanCursor
1228
Y.Selection.cleanCursor = function() {
1230
var cur = Y.config.doc.getElementById(Y.Selection.CUR_WRAPID);
1233
if (cur.innerHTML == ' ' || cur.innerHTML == '<br>') {
1234
if (cur.parentNode) {
1235
cur.parentNode.removeChild(cur);
1241
var cur = Y.all('#' + Y.Selection.CUR_WRAPID);
1243
cur.each(function(c) {
1244
var html = c.get('innerHTML');
1245
if (html == ' ' || html == '<br>') {
1253
Y.Selection.prototype = {
1261
* Flag to show if the range is collapsed or not
1262
* @property isCollapsed
1267
* A Node instance of the parentNode of the anchorNode of the range
1268
* @property anchorNode
1273
* The offset from the range object
1274
* @property anchorOffset
1279
* A Node instance of the actual textNode of the range.
1280
* @property anchorTextNode
1283
anchorTextNode: null,
1285
* A Node instance of the parentNode of the focusNode of the range
1286
* @property focusNode
1291
* The offset from the range object
1292
* @property focusOffset
1297
* A Node instance of the actual textNode of the range.
1298
* @property focusTextNode
1301
focusTextNode: null,
1303
* The actual Selection/Range object
1304
* @property _selection
1309
* Wrap an element, with another element
1312
* @param {HTMLElement} n The node to wrap
1313
* @param {String} tag The tag to use when creating the new element.
1314
* @return {HTMLElement} The wrapped node
1316
_wrap: function(n, tag) {
1317
var tmp = Y.Node.create('<' + tag + '></' + tag + '>');
1318
tmp.set(INNER_HTML, n.get(INNER_HTML));
1319
n.set(INNER_HTML, '');
1321
return Y.Node.getDOMNode(tmp);
1324
* Swap an element, with another element
1327
* @param {HTMLElement} n The node to swap
1328
* @param {String} tag The tag to use when creating the new element.
1329
* @return {HTMLElement} The new node
1331
_swap: function(n, tag) {
1332
var tmp = Y.Node.create('<' + tag + '></' + tag + '>');
1333
tmp.set(INNER_HTML, n.get(INNER_HTML));
1335
return Y.Node.getDOMNode(tmp);
1338
* Get all the nodes in the current selection. This method will actually perform a filter first.
1339
* Then it calls doc.execCommand('fontname', null, 'yui-tmp') to touch all nodes in the selection.
1340
* The it compiles a list of all nodes affected by the execCommand and builds a NodeList to return.
1341
* @method getSelected
1342
* @return {NodeList} A NodeList of all items in the selection.
1344
getSelected: function() {
1345
Y.Selection.filter();
1346
Y.config.doc.execCommand('fontname', null, Y.Selection.TMP);
1347
var nodes = Y.all(Y.Selection.ALL),
1350
nodes.each(function(n, k) {
1351
if (n.getStyle(FONT_FAMILY, Y.Selection.TMP)) {
1352
n.setStyle(FONT_FAMILY, '');
1353
n.removeAttribute('face');
1354
if (n.getAttribute('style') === '') {
1355
n.removeAttribute('style');
1357
if (!n.test('body')) {
1358
items.push(Y.Node.getDOMNode(nodes.item(k)));
1362
return Y.all(items);
1365
* Insert HTML at the current cursor position and return a Node instance of the newly inserted element.
1366
* @method insertContent
1367
* @param {String} html The HTML to insert.
1368
* @return {Node} The inserted Node.
1370
insertContent: function(html) {
1371
return this.insertAtCursor(html, this.anchorTextNode, this.anchorOffset, true);
1374
* 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.
1375
* @method insertAtCursor
1376
* @param {String} html The HTML to insert.
1377
* @param {Node} node The text node to break when inserting.
1378
* @param {Number} offset The left offset of the text node to break and insert the new content.
1379
* @param {Boolean} collapse Should the range be collapsed after insertion. default: false
1380
* @return {Node} The inserted Node.
1382
insertAtCursor: function(html, node, offset, collapse) {
1383
var cur = Y.Node.create('<' + Y.Selection.DEFAULT_TAG + ' class="yui-non"></' + Y.Selection.DEFAULT_TAG + '>'),
1384
inHTML, txt, txt2, newNode, range = this.createRange(), b;
1386
if (node && node.test('body')) {
1387
b = Y.Node.create('<span></span>');
1393
if (range.pasteHTML) {
1394
newNode = Y.Node.create(html);
1396
range.pasteHTML('<span id="rte-insert"></span>');
1398
inHTML = Y.one('#rte-insert');
1400
inHTML.set('id', '');
1401
inHTML.replace(newNode);
1404
Y.on('available', function() {
1405
inHTML.set('id', '');
1406
inHTML.replace(newNode);
1410
//TODO using Y.Node.create here throws warnings & strips first white space character
1411
//txt = Y.one(Y.Node.create(inHTML.substr(0, offset)));
1412
//txt2 = Y.one(Y.Node.create(inHTML.substr(offset)));
1414
inHTML = node.get(textContent);
1416
txt = Y.one(Y.config.doc.createTextNode(inHTML.substr(0, offset)));
1417
txt2 = Y.one(Y.config.doc.createTextNode(inHTML.substr(offset)));
1419
node.replace(txt, node);
1420
newNode = Y.Node.create(html);
1421
if (newNode.get('nodeType') === 11) {
1422
b = Y.Node.create('<span></span>');
1426
txt.insert(newNode, 'after');
1427
//if (txt2 && txt2.get('length')) {
1429
newNode.insert(cur, 'after');
1430
cur.insert(txt2, 'after');
1431
this.selectNode(cur, collapse);
1434
if (node.get('nodeType') === 3) {
1435
node = node.get('parentNode');
1437
newNode = Y.Node.create(html);
1438
html = node.get('innerHTML').replace(/\n/gi, '');
1439
if (html == '' || html == '<br>') {
1440
node.append(newNode);
1442
node.insert(newNode, 'before');
1444
if (node.get('firstChild').test('br')) {
1445
node.get('firstChild').remove();
1452
* Get all elements inside a selection and wrap them with a new element and return a NodeList of all elements touched.
1453
* @method wrapContent
1454
* @param {String} tag The tag to wrap all selected items with.
1455
* @return {NodeList} A NodeList of all items in the selection.
1457
wrapContent: function(tag) {
1458
tag = (tag) ? tag : Y.Selection.DEFAULT_TAG;
1460
if (!this.isCollapsed) {
1461
Y.log('Wrapping selection with: ' + tag, 'info', 'selection');
1462
var items = this.getSelected(),
1463
changed = [], range, last, first, range2;
1465
items.each(function(n, k) {
1466
var t = n.get('tagName').toLowerCase();
1468
changed.push(this._swap(items.item(k), tag));
1470
changed.push(this._wrap(items.item(k), tag));
1474
range = this.createRange();
1476
last = changed[changed.length - 1];
1477
if (this._selection.removeAllRanges) {
1478
range.setStart(changed[0], 0);
1479
range.setEnd(last, last.childNodes.length);
1480
this._selection.removeAllRanges();
1481
this._selection.addRange(range);
1483
range.moveToElementText(Y.Node.getDOMNode(first));
1484
range2 = this.createRange();
1485
range2.moveToElementText(Y.Node.getDOMNode(last));
1486
range.setEndPoint('EndToEnd', range2);
1490
changed = Y.all(changed);
1491
Y.log('Returning NodeList with (' + changed.size() + ') item(s)' , 'info', 'selection');
1496
Y.log('Can not wrap a collapsed selection, use insertContent', 'error', 'selection');
1501
* Find and replace a string inside a text node and replace it with HTML focusing the node after
1502
* to allow you to continue to type.
1504
* @param {String} se The string to search for.
1505
* @param {String} re The string of HTML to replace it with.
1506
* @return {Node} The node inserted.
1508
replace: function(se,re) {
1509
Y.log('replacing (' + se + ') with (' + re + ')');
1510
var range = this.createRange(), node, txt, index, newNode;
1512
if (range.getBookmark) {
1513
index = range.getBookmark();
1514
txt = this.anchorNode.get('innerHTML').replace(se, re);
1515
this.anchorNode.set('innerHTML', txt);
1516
range.moveToBookmark(index);
1517
newNode = Y.one(range.parentElement());
1519
node = this.anchorTextNode;
1520
txt = node.get(textContent);
1521
index = txt.indexOf(se);
1523
txt = txt.replace(se, '');
1524
node.set(textContent, txt);
1525
newNode = this.insertAtCursor(re, node, index, true);
1530
* Destroy the range.
1533
* @return {Y.Selection}
1535
remove: function() {
1536
this._selection.removeAllRanges();
1540
* Wrapper for the different range creation methods.
1541
* @method createRange
1542
* @return {RangeObject}
1544
createRange: function() {
1545
if (Y.config.doc.selection) {
1546
return Y.config.doc.selection.createRange();
1548
return Y.config.doc.createRange();
1552
* Select a Node (hilighting it).
1553
* @method selectNode
1554
* @param {Node} node The node to select
1555
* @param {Boolean} collapse Should the range be collapsed after insertion. default: false
1557
* @return {Y.Selection}
1559
selectNode: function(node, collapse, end) {
1561
node = Y.Node.getDOMNode(node);
1562
var range = this.createRange();
1563
if (range.selectNode) {
1564
range.selectNode(node);
1565
this._selection.removeAllRanges();
1566
this._selection.addRange(range);
1569
this._selection.collapse(node, end);
1571
this._selection.collapse(node, 0);
1575
if (node.nodeType === 3) {
1576
node = node.parentNode;
1579
range.moveToElementText(node);
1582
range.collapse(((end) ? false : true));
1589
* Put a placeholder in the DOM at the current cursor position.
1593
setCursor: function() {
1594
this.removeCursor(false);
1595
return this.insertContent(Y.Selection.CURSOR);
1598
* Get the placeholder in the DOM at the current cursor position.
1602
getCursor: function() {
1603
return Y.all('#' + Y.Selection.CURID);
1606
* Remove the cursor placeholder from the DOM.
1607
* @method removeCursor
1608
* @param {Boolean} keep Setting this to true will keep the node, but remove the unique parts that make it the cursor.
1611
removeCursor: function(keep) {
1612
var cur = this.getCursor();
1615
cur.removeAttribute('id');
1616
cur.set('innerHTML', '<span id="' + Y.Selection.CUR_WRAPID + '"> </span>');
1624
* Gets a stored cursor and focuses it for editing, must be called sometime after setCursor
1625
* @method focusCursor
1628
focusCursor: function(collapse, end) {
1629
if (collapse !== false) {
1632
if (end !== false) {
1635
var cur = this.removeCursor(true);
1637
cur.each(function(c) {
1638
this.selectNode(c, collapse, end);
1643
* Generic toString for logging.
1647
toString: function() {
1648
return 'Selection Object';
1653
}, '3.2.0' ,{skinnable:false, requires:['node']});
1654
YUI.add('exec-command', function(Y) {
1658
* Plugin for the frame module to handle execCommands for Editor
1660
* @submodule exec-command
1663
* Plugin for the frame module to handle execCommands for Editor
1664
* @class Plugin.ExecCommand
1668
var ExecCommand = function() {
1669
ExecCommand.superclass.constructor.apply(this, arguments);
1672
Y.extend(ExecCommand, Y.Base, {
1674
* An internal reference to the instance of the frame plugged into.
1680
* Execute a command on the frame's document.
1682
* @param {String} action The action to perform (bold, italic, fontname)
1683
* @param {String} value The optional value (helvetica)
1684
* @return {Node/NodeList} Should return the Node/Nodelist affected
1686
command: function(action, value) {
1687
var fn = ExecCommand.COMMANDS[action];
1689
Y.log('execCommand(' + action + '): "' + value + '"', 'info', 'exec-command');
1691
return fn.call(this, action, value);
1693
return this._command(action, value);
1697
* The private version of execCommand that doesn't filter for overrides.
1700
* @param {String} action The action to perform (bold, italic, fontname)
1701
* @param {String} value The optional value (helvetica)
1703
_command: function(action, value) {
1704
var inst = this.getInstance();
1706
Y.log('Internal execCommand(' + action + '): "' + value + '"', 'info', 'exec-command');
1707
inst.config.doc.execCommand(action, false, value);
1709
Y.log(e.message, 'error', 'exec-command');
1713
* Get's the instance of YUI bound to the parent frame
1714
* @method getInstance
1715
* @return {YUI} The YUI instance bound to the parent frame
1717
getInstance: function() {
1719
this._inst = this.get('host').getInstance();
1723
initializer: function() {
1724
Y.mix(this.get('host'), {
1725
execCommand: function(action, value) {
1726
return this.exec.command(action, value);
1728
_execCommand: function(action, value) {
1729
return this.exec._command(action, value);
1739
NAME: 'execCommand',
1752
* Static object literal of execCommand overrides
1753
* @property COMMANDS
1758
* Wraps the content with a new element of type (tag)
1759
* @method COMMANDS.wrap
1761
* @param {String} cmd The command executed: wrap
1762
* @param {String} tag The tag to wrap the selection with
1763
* @return {NodeList} NodeList of the items touched by this command.
1765
wrap: function(cmd, tag) {
1766
var inst = this.getInstance();
1767
return (new inst.Selection()).wrapContent(tag);
1770
* Inserts the provided HTML at the cursor, should be a single element.
1771
* @method COMMANDS.inserthtml
1773
* @param {String} cmd The command executed: inserthtml
1774
* @param {String} html The html to insert
1775
* @return {Node} Node instance of the item touched by this command.
1777
inserthtml: function(cmd, html) {
1778
var inst = this.getInstance();
1779
return (new inst.Selection()).insertContent(html);
1782
* Inserts the provided HTML at the cursor, and focuses the cursor afterwards.
1783
* @method COMMANDS.insertandfocus
1785
* @param {String} cmd The command executed: insertandfocus
1786
* @param {String} html The html to insert
1787
* @return {Node} Node instance of the item touched by this command.
1789
insertandfocus: function(cmd, html) {
1790
var inst = this.getInstance(), out, sel;
1791
html += inst.Selection.CURSOR;
1792
out = this.command('inserthtml', html);
1793
sel = new inst.Selection();
1794
sel.focusCursor(true, true);
1798
* Inserts a BR at the current cursor position
1799
* @method COMMANDS.insertbr
1801
* @param {String} cmd The command executed: insertbr
1803
insertbr: function(cmd) {
1804
var inst = this.getInstance(), cur,
1805
sel = new inst.Selection();
1808
cur = sel.getCursor();
1809
cur.insert('<br>', 'before');
1810
sel.focusCursor(true, false);
1811
return cur.previous();
1814
* Inserts an image at the cursor position
1815
* @method COMMANDS.insertimage
1817
* @param {String} cmd The command executed: insertimage
1818
* @param {String} img The url of the image to be inserted
1819
* @return {Node} Node instance of the item touched by this command.
1821
insertimage: function(cmd, img) {
1822
return this.command('inserthtml', '<img src="' + img + '">');
1825
* Add a class to all of the elements in the selection
1826
* @method COMMANDS.addclass
1828
* @param {String} cmd The command executed: addclass
1829
* @param {String} cls The className to add
1830
* @return {NodeList} NodeList of the items touched by this command.
1832
addclass: function(cmd, cls) {
1833
var inst = this.getInstance();
1834
return (new inst.Selection()).getSelected().addClass(cls);
1837
* Remove a class from all of the elements in the selection
1838
* @method COMMANDS.removeclass
1840
* @param {String} cmd The command executed: removeclass
1841
* @param {String} cls The className to remove
1842
* @return {NodeList} NodeList of the items touched by this command.
1844
removeclass: function(cmd, cls) {
1845
var inst = this.getInstance();
1846
return (new inst.Selection()).getSelected().removeClass(cls);
1849
* Adds a background color to the current selection, or creates a new element and applies it
1850
* @method COMMANDS.backcolor
1852
* @param {String} cmd The command executed: backcolor
1853
* @param {String} val The color value to apply
1854
* @return {NodeList} NodeList of the items touched by this command.
1856
forecolor: function(cmd, val) {
1857
var inst = this.getInstance(),
1858
sel = new inst.Selection(), n;
1861
this._command('styleWithCSS', 'true');
1863
if (sel.isCollapsed) {
1864
if (sel.anchorNode && (sel.anchorNode.get('innerHTML') === ' ')) {
1865
sel.anchorNode.setStyle('color', val);
1868
n = this.command('inserthtml', '<span style="color: ' + val + '">' + inst.Selection.CURSOR + '</span>');
1869
sel.focusCursor(true, true);
1873
return this._command(cmd, val);
1876
this._command('styleWithCSS', false);
1879
backcolor: function(cmd, val) {
1880
var inst = this.getInstance(),
1881
sel = new inst.Selection(), n;
1883
if (Y.UA.gecko || Y.UA.opera) {
1884
cmd = 'hilitecolor';
1887
this._command('styleWithCSS', 'true');
1889
if (sel.isCollapsed) {
1890
if (sel.anchorNode && (sel.anchorNode.get('innerHTML') === ' ')) {
1891
sel.anchorNode.setStyle('backgroundColor', val);
1894
n = this.command('inserthtml', '<span style="background-color: ' + val + '">' + inst.Selection.CURSOR + '</span>');
1895
sel.focusCursor(true, true);
1899
return this._command(cmd, val);
1902
this._command('styleWithCSS', false);
1906
* Sugar method, calles backcolor
1907
* @method COMMANDS.hilitecolor
1909
* @param {String} cmd The command executed: backcolor
1910
* @param {String} val The color value to apply
1911
* @return {NodeList} NodeList of the items touched by this command.
1913
hilitecolor: function() {
1914
return ExecCommand.COMMANDS.backcolor.apply(this, arguments);
1917
* Adds a font name to the current selection, or creates a new element and applies it
1918
* @method COMMANDS.fontname
1920
* @param {String} cmd The command executed: fontname
1921
* @param {String} val The font name to apply
1922
* @return {NodeList} NodeList of the items touched by this command.
1924
fontname: function(cmd, val) {
1925
var inst = this.getInstance(),
1926
sel = new inst.Selection(), n;
1928
if (sel.isCollapsed) {
1929
if (sel.anchorNode && (sel.anchorNode.get('innerHTML') === ' ')) {
1930
sel.anchorNode.setStyle('fontFamily', val);
1933
n = this.command('inserthtml', '<span style="font-family: ' + val + '">' + inst.Selection.CURSOR + '</span>');
1934
sel.focusCursor(true, true);
1938
return this._command('fontname', val);
1942
* Adds a fontsize to the current selection, or creates a new element and applies it
1943
* @method COMMANDS.fontsize
1945
* @param {String} cmd The command executed: fontsize
1946
* @param {String} val The font size to apply
1947
* @return {NodeList} NodeList of the items touched by this command.
1949
fontsize: function(cmd, val) {
1950
var inst = this.getInstance(),
1951
sel = new inst.Selection(), n, prev;
1953
if (sel.isCollapsed) {
1954
n = this.command('inserthtml', '<font size="' + val + '"> </font>');
1955
prev = n.get('previousSibling');
1956
if (prev && prev.get('nodeType') === 3) {
1957
if (prev.get('length') < 2) {
1961
sel.selectNode(n.get('firstChild'), true, false);
1964
return this._command('fontsize', val);
1970
Y.namespace('Plugin');
1971
Y.Plugin.ExecCommand = ExecCommand;
1975
}, '3.2.0' ,{skinnable:false, requires:['frame']});
1976
YUI.add('editor-tab', function(Y) {
1979
* Handles tab and shift-tab indent/outdent support.
1981
* @submodule editor-tab
1984
* Handles tab and shift-tab indent/outdent support.
1985
* @class Plugin.EditorTab
1990
var EditorTab = function() {
1991
EditorTab.superclass.constructor.apply(this, arguments);
1994
Y.extend(EditorTab, Y.Base, {
1996
* Listener for host's nodeChange event and captures the tabkey interaction.
1998
* @method _onNodeChange
1999
* @param {Event} e The Event facade passed from the host.
2001
_onNodeChange: function(e) {
2002
var action = 'indent';
2004
if (e.changedType === 'tab') {
2005
if (!e.changedNode.test('li, li *')) {
2006
e.changedEvent.halt();
2008
if (e.changedEvent.shiftKey) {
2012
Y.log('Overriding TAB to ' + action, 'info', 'editorTab');
2013
this.get(HOST).execCommand(action, '');
2017
initializer: function() {
2018
this.get(HOST).on('nodeChange', Y.bind(this._onNodeChange, this));
2041
Y.namespace('Plugin');
2043
Y.Plugin.EditorTab = EditorTab;
2046
}, '3.2.0' ,{skinnable:false, requires:['editor-base']});
2047
YUI.add('createlink-base', function(Y) {
2050
* Base class for Editor. Handles the business logic of Editor, no GUI involved only utility methods and events.
2052
* @submodule createlink-base
2055
* Adds prompt style link creation. Adds an override for the <a href="Plugin.ExecCommand.html#method_COMMANDS.createlink">createlink execCommand</a>.
2056
* @class Plugin.CreateLinkBase
2060
var CreateLinkBase = {};
2062
* Strings used by the plugin
2066
CreateLinkBase.STRINGS = {
2068
* String used for the Prompt
2072
PROMPT: 'Please enter the URL for the link to point to:',
2074
* String used as the default value of the Prompt
2081
Y.namespace('Plugin');
2082
Y.Plugin.CreateLinkBase = CreateLinkBase;
2084
Y.mix(Y.Plugin.ExecCommand.COMMANDS, {
2086
* Override for the createlink method from the <a href="Plugin.CreateLinkBase.html">CreateLinkBase</a> plugin.
2088
* @method COMMANDS.createlink
2090
* @param {String} cmd The command executed: createlink
2091
* @return {Node} Node instance of the item touched by this command.
2093
createlink: function(cmd) {
2094
var inst = this.get('host').getInstance(), out, a, sel,
2095
url = prompt(CreateLinkBase.STRINGS.PROMPT, CreateLinkBase.STRINGS.DEFAULT);
2098
Y.log('Adding link: ' + url, 'info', 'createLinkBase');
2100
this.get('host')._execCommand(cmd, url);
2101
sel = new inst.Selection();
2102
out = sel.getSelected();
2103
if (!sel.isCollapsed && out.size()) {
2104
//We have a selection
2105
a = out.item(0).one('a');
2107
out.item(0).replace(a);
2110
//No selection, insert a new node..
2111
this.get('host').execCommand('inserthtml', '<a href="' + url + '">' + url + '</a>');
2120
}, '3.2.0' ,{skinnable:false, requires:['editor-base']});
2121
YUI.add('editor-base', function(Y) {
2125
* Base class for Editor. Handles the business logic of Editor, no GUI involved only utility methods and events.
2127
* @submodule editor-base
2130
* Base class for Editor. Handles the business logic of Editor, no GUI involved only utility methods and events.
2137
var EditorBase = function() {
2138
EditorBase.superclass.constructor.apply(this, arguments);
2141
Y.extend(EditorBase, Y.Base, {
2143
* Internal reference to the Y.Frame instance
2147
initializer: function() {
2148
var frame = new Y.Frame({
2150
title: EditorBase.STRINGS.title,
2151
use: EditorBase.USE,
2152
dir: this.get('dir'),
2153
extracss: this.get('extracss'),
2155
}).plug(Y.Plugin.ExecCommand);
2157
frame.after('ready', Y.bind(this._afterFrameReady, this));
2158
frame.addTarget(this);
2162
this.publish('nodeChange', {
2165
defaultFn: this._defNodeChangeFn
2168
this.plug(Y.Plugin.EditorPara);
2170
destructor: function() {
2171
this.frame.destroy();
2176
* Copy certain styles from one node instance to another (used for new paragraph creation mainly)
2177
* @method copyStyles
2178
* @param {Node} from The Node instance to copy the styles from
2179
* @param {Node} to The Node instance to copy the styles to
2181
copyStyles: function(from, to) {
2182
var styles = ['color', 'fontSize', 'fontFamily', 'backgroundColor', 'fontStyle' ],
2185
Y.each(styles, function(v) {
2186
newStyles[v] = from.getStyle(v);
2188
if (from.ancestor('b,strong')) {
2189
newStyles.fontWeight = 'bold';
2191
to.setStyles(newStyles);
2194
* Holder for the selection bookmark in IE.
2195
* @property _lastBookmark
2198
_lastBookmark: null,
2200
* The default handler for the nodeChange event.
2201
* @method _defNodeChangeFn
2202
* @param {Event} e The event
2205
_defNodeChangeFn: function(e) {
2206
var startTime = (new Date()).getTime();
2207
//Y.log('Default nodeChange function: ' + e.changedType, 'info', 'editor');
2208
var inst = this.getInstance(), sel;
2211
sel = inst.config.doc.selection.createRange();
2212
this._lastBookmark = sel.getBookmark();
2217
* This whole method needs to be fixed and made more dynamic.
2218
* Maybe static functions for the e.changeType and an object bag
2219
* to walk through and filter to pass off the event to before firing..
2222
switch (e.changedType) {
2224
inst.Selection.cleanCursor();
2228
//Webkit doesn't support shift+enter as a BR, this fixes that.
2229
if (e.changedEvent.shiftKey) {
2230
this.execCommand('insertbr');
2231
e.changedEvent.preventDefault();
2236
if (!e.changedNode.test('li, li *') && !e.changedEvent.shiftKey) {
2237
e.changedEvent.preventDefault();
2239
Y.log('Overriding TAB key to insert HTML: HALTING', 'info', 'editor');
2240
var sel = new inst.Selection();
2242
var cur = sel.getCursor();
2243
cur.insert(EditorBase.TABKEY, 'before');
2248
if (e.changedNode.test('p')) {
2249
var prev = e.changedNode.previous(), lc, lc2, found = false;
2251
lc = prev.one(':last-child');
2254
lc2 = lc.one(':last-child');
2265
this.copyStyles(lc, e.changedNode);
2272
var changed = this.getDomPath(e.changedNode, false),
2273
cmds = {}, family, fsize, classes = [],
2274
fColor = '', bColor = '';
2280
Y.each(changed, function(el) {
2281
var tag = el.tagName.toLowerCase(),
2282
cmd = EditorBase.TAG2CMD[tag];
2288
//Bold and Italic styles
2289
var s = el.currentStyle || el.style;
2291
if ((''+s.fontWeight) == 'bold') { //Cast this to a string
2294
if (s.fontStyle == 'italic') {
2297
if (s.textDecoration == 'underline') {
2300
if (s.textDecoration == 'line-through') {
2301
cmds.strikethrough = 1;
2304
var n = inst.one(el);
2305
if (n.getStyle('fontFamily')) {
2306
var family2 = n.getStyle('fontFamily').split(',')[0].toLowerCase();
2311
family = family.replace(/'/g, '').replace(/"/g, '');
2315
fsize = n.getStyle('fontSize');
2317
var cls = el.className.split(' ');
2319
Y.each(cls, function(v) {
2320
if (v !== '' && (v.substr(0, 4) !== 'yui_')) {
2325
fColor = EditorBase.FILTER_RGB(s.color);
2326
var bColor2 = EditorBase.FILTER_RGB(s.backgroundColor);
2327
if (bColor2 !== 'transparent') {
2333
e.dompath = inst.all(changed);
2334
e.classNames = classes;
2337
//TODO Dont' like this, not dynamic enough..
2338
if (!e.fontFamily) {
2339
e.fontFamily = family;
2345
e.fontColor = fColor;
2347
if (!e.backgroundColor) {
2348
e.backgroundColor = bColor;
2351
var endTime = (new Date()).getTime();
2352
Y.log('_defNodeChangeTimer 2: ' + (endTime - startTime) + 'ms', 'info', 'selection');
2355
* Walk the dom tree from this node up to body, returning a reversed array of parents.
2356
* @method getDomPath
2357
* @param {Node} node The Node to start from
2359
getDomPath: function(node, nodeList) {
2360
var domPath = [], domNode,
2361
inst = this.frame.getInstance();
2363
domNode = inst.Node.getDOMNode(node);
2364
//return inst.all(domNode);
2366
while (domNode !== null) {
2368
if ((domNode === inst.config.doc.documentElement) || (domNode === inst.config.doc) || !domNode.tagName) {
2373
if (!inst.DOM.inDoc(domNode)) {
2378
//Check to see if we get el.nodeName and nodeType
2379
if (domNode.nodeName && domNode.nodeType && (domNode.nodeType == 1)) {
2380
domPath.push(domNode);
2383
if (domNode == inst.config.doc.body) {
2388
domNode = domNode.parentNode;
2392
while (node !== null) {
2393
if (node.test('html') || node.test('doc') || !node.get('tagName')) {
2397
if (!node.inDoc()) {
2401
//Check to see if we get el.nodeName and nodeType
2402
if (node.get('nodeName') && node.get('nodeType') && (node.get('nodeType') == 1)) {
2403
domPath.push(inst.Node.getDOMNode(node));
2406
if (node.test('body')) {
2411
node = node.get('parentNode');
2415
if (domPath.length === 0) {
2416
domPath[0] = inst.config.doc.body;
2420
return inst.all(domPath.reverse());
2422
return domPath.reverse();
2427
* After frame ready, bind mousedown & keyup listeners
2428
* @method _afterFrameReady
2431
_afterFrameReady: function() {
2432
var inst = this.frame.getInstance();
2434
this.frame.on('dom:mouseup', Y.bind(this._onFrameMouseUp, this));
2435
this.frame.on('dom:mousedown', Y.bind(this._onFrameMouseDown, this));
2437
this.frame.on('dom:keyup', Y.bind(this._onFrameKeyUp, this));
2438
this.frame.on('dom:keydown', Y.bind(this._onFrameKeyDown, this));
2439
this.frame.on('dom:keypress', Y.bind(this._onFrameKeyPress, this));
2441
//this.frame.on('dom:keydown', Y.throttle(Y.bind(this._onFrameKeyDown, this), 500));
2444
this.frame.on('dom:keydown', Y.bind(this._onFrameKeyDown, this));
2447
this.frame.on('dom:activate', Y.bind(this._onFrameActivate, this));
2448
this.frame.on('dom:keyup', Y.throttle(Y.bind(this._onFrameKeyUp, this), 800));
2449
this.frame.on('dom:keypress', Y.throttle(Y.bind(this._onFrameKeyPress, this), 800));
2451
this.frame.on('dom:keyup', Y.bind(this._onFrameKeyUp, this));
2452
this.frame.on('dom:keypress', Y.bind(this._onFrameKeyPress, this));
2455
inst.Selection.filter();
2459
* Moves the cached selection bookmark back so IE can place the cursor in the right place.
2460
* @method _onFrameActivate
2463
_onFrameActivate: function() {
2464
if (this._lastBookmark) {
2465
Y.log('IE Activate handler, resetting cursor position', 'info', 'editor');
2466
var inst = this.getInstance(),
2467
sel = inst.config.doc.selection.createRange(),
2468
bk = sel.moveToBookmark(this._lastBookmark);
2472
this._lastBookmark = null;
2476
* Fires nodeChange event
2477
* @method _onFrameMouseUp
2480
_onFrameMouseUp: function(e) {
2481
this.fire('nodeChange', { changedNode: e.frameTarget, changedType: 'mouseup', changedEvent: e.frameEvent });
2484
* Fires nodeChange event
2485
* @method _onFrameMouseDown
2488
_onFrameMouseDown: function(e) {
2489
this.fire('nodeChange', { changedNode: e.frameTarget, changedType: 'mousedown', changedEvent: e.frameEvent });
2492
* Caches a copy of the selection for key events. Only creating the selection on keydown
2493
* @property _currentSelection
2496
_currentSelection: null,
2497
_currentSelectionTimer: null,
2498
_currentSelectionClear: null,
2500
* Fires nodeChange event
2501
* @method _onFrameKeyDown
2504
_onFrameKeyDown: function(e) {
2505
if (!this._currentSelection) {
2506
if (this._currentSelectionTimer) {
2507
this._currentSelectionTimer.cancel();
2509
this._currentSelectionTimer = Y.later(850, this, function() {
2510
this._currentSelectionClear = true;
2512
var inst = this.frame.getInstance(),
2513
sel = new inst.Selection(e);
2515
this._currentSelection = sel;
2517
var sel = this._currentSelection;
2519
var inst = this.frame.getInstance(),
2520
sel = new inst.Selection();
2522
this._currentSelection = sel;
2524
if (sel && sel.anchorNode) {
2525
this.fire('nodeChange', { changedNode: sel.anchorNode, changedType: 'keydown', changedEvent: e.frameEvent });
2526
if (EditorBase.NC_KEYS[e.keyCode]) {
2527
this.fire('nodeChange', { changedNode: sel.anchorNode, changedType: EditorBase.NC_KEYS[e.keyCode], changedEvent: e.frameEvent });
2528
this.fire('nodeChange', { changedNode: sel.anchorNode, changedType: EditorBase.NC_KEYS[e.keyCode] + '-down', changedEvent: e.frameEvent });
2533
* Fires nodeChange event
2534
* @method _onFrameKeyPress
2537
_onFrameKeyPress: function(e) {
2538
var sel = this._currentSelection;
2540
if (sel && sel.anchorNode) {
2541
this.fire('nodeChange', { changedNode: sel.anchorNode, changedType: 'keypress', changedEvent: e.frameEvent });
2542
if (EditorBase.NC_KEYS[e.keyCode]) {
2543
this.fire('nodeChange', { changedNode: sel.anchorNode, changedType: EditorBase.NC_KEYS[e.keyCode] + '-press', changedEvent: e.frameEvent });
2548
* Fires nodeChange event for keyup on specific keys
2549
* @method _onFrameKeyUp
2552
_onFrameKeyUp: function(e) {
2553
var sel = this._currentSelection;
2555
if (sel && sel.anchorNode) {
2556
this.fire('nodeChange', { changedNode: sel.anchorNode, changedType: 'keyup', selection: sel, changedEvent: e.frameEvent });
2557
if (EditorBase.NC_KEYS[e.keyCode]) {
2558
this.fire('nodeChange', { changedNode: sel.anchorNode, changedType: EditorBase.NC_KEYS[e.keyCode] + '-up', selection: sel, changedEvent: e.frameEvent });
2561
if (this._currentSelectionClear) {
2562
this._currentSelectionClear = this._currentSelection = null;
2566
* Pass through to the frame.execCommand method
2567
* @method execCommand
2568
* @param {String} cmd The command to pass: inserthtml, insertimage, bold
2569
* @param {String} val The optional value of the command: Helvetica
2570
* @return {Node/NodeList} The Node or Nodelist affected by the command. Only returns on override commands, not browser defined commands.
2572
execCommand: function(cmd, val) {
2573
var ret = this.frame.execCommand(cmd, val),
2574
inst = this.frame.getInstance(),
2575
sel = new inst.Selection(), cmds = {},
2576
e = { changedNode: sel.anchorNode, changedType: 'execcommand', nodes: ret };
2583
e.backgroundColor = val;
2596
this.fire('nodeChange', e);
2601
* Get the YUI instance of the frame
2602
* @method getInstance
2603
* @return {YUI} The YUI instance bound to the frame.
2605
getInstance: function() {
2606
return this.frame.getInstance();
2609
* Renders the Y.Frame to the passed node.
2611
* @param {Selector/HTMLElement/Node} node The node to append the Editor to
2612
* @return {EditorBase}
2615
render: function(node) {
2616
this.frame.set('content', this.get('content'));
2617
this.frame.render(node);
2621
* Focus the contentWindow of the iframe
2623
* @param {Function} fn Callback function to execute after focus happens
2624
* @return {EditorBase}
2627
focus: function(fn) {
2628
this.frame.focus(fn);
2632
* Handles the showing of the Editor instance. Currently only handles the iframe
2634
* @return {EditorBase}
2642
* Handles the hiding of the Editor instance. Currently only handles the iframe
2644
* @return {EditorBase}
2652
* (Un)Filters the content of the Editor, cleaning YUI related code. //TODO better filtering
2653
* @method getContent
2654
* @return {String} The filtered content of the Editor
2656
getContent: function() {
2657
var html = '', inst = this.getInstance();
2658
if (inst && inst.Selection) {
2659
html = inst.Selection.unfilter();
2661
//Removing the _yuid from the objects in IE
2662
html = html.replace(/ _yuid="([^>]*)"/g, '');
2669
* @description The HTML markup to use for the tabkey
2671
TABKEY: '<span class="tab"> </span>',
2674
* @method FILTER_RGB
2675
* @param String css The CSS string containing rgb(#,#,#);
2676
* @description Converts an RGB color string to a hex color, example: rgb(0, 255, 0) converts to #00ff00
2679
FILTER_RGB: function(css) {
2680
if (css.toLowerCase().indexOf('rgb') != -1) {
2681
var exp = new RegExp("(.*?)rgb\\s*?\\(\\s*?([0-9]+).*?,\\s*?([0-9]+).*?,\\s*?([0-9]+).*?\\)(.*?)", "gi");
2682
var rgb = css.replace(exp, "$1,$2,$3,$4,$5").split(',');
2684
if (rgb.length == 5) {
2685
var r = parseInt(rgb[1], 10).toString(16);
2686
var g = parseInt(rgb[2], 10).toString(16);
2687
var b = parseInt(rgb[3], 10).toString(16);
2689
r = r.length == 1 ? '0' + r : r;
2690
g = g.length == 1 ? '0' + g : g;
2691
b = b.length == 1 ? '0' + b : b;
2693
css = "#" + r + g + b;
2701
* @description A hash table of tags to their execcomand's
2709
'sup': 'superscript',
2711
'img': 'insertimage',
2713
'ul' : 'insertunorderedlist',
2714
'ol' : 'insertorderedlist'
2717
* Hash table of keys to fire a nodeChange event for.
2738
* The default modules to use inside the Frame
2743
USE: ['substitute', 'node', 'selector-css3', 'selection', 'stylesheet'],
2745
* The Class Name: editorBase
2757
* Title of frame document: Rich Text Editor
2759
* @property STRINGS.title
2761
title: 'Rich Text Editor'
2765
* The content to load into the Editor Frame
2766
* @attribute content
2770
setter: function(str) {
2771
if (str.substr(0, 1) === "\n") {
2772
Y.log('Stripping first carriage return from content before injecting', 'warn', 'editor');
2773
str = str.substr(1);
2778
return this.frame.set('content', str);
2780
getter: function() {
2781
return this.frame.get('content');
2785
* The value of the dir attribute on the HTML element of the frame. Default: ltr
2793
* @attribute extracss
2794
* @description A string of CSS to add to the Head of the Editor
2799
setter: function(css) {
2801
this.frame.set('extracss', css);
2809
Y.EditorBase = EditorBase;
2813
* @description Fired from mouseup & keyup.
2814
* @param {Event.Facade} event An Event Facade object with the following specific properties added:
2816
* <dt>changedEvent</dt><dd>The event that caused the nodeChange</dd>
2817
* <dt>changedNode</dt><dd>The node that was interacted with</dd>
2818
* <dt>changedType</dt><dd>The type of change: mousedown, mouseup, right, left, backspace, tab, enter, etc..</dd>
2819
* <dt>commands</dt><dd>The list of execCommands that belong to this change and the dompath that's associated with the changedNode</dd>
2820
* <dt>classNames</dt><dd>An array of classNames that are applied to the changedNode and all of it's parents</dd>
2821
* <dt>dompath</dt><dd>A sorted array of node instances that make up the DOM path from the changedNode to body.</dd>
2822
* <dt>backgroundColor</dt><dd>The cascaded backgroundColor of the changedNode</dd>
2823
* <dt>fontColor</dt><dd>The cascaded fontColor of the changedNode</dd>
2824
* <dt>fontFamily</dt><dd>The cascaded fontFamily of the changedNode</dd>
2825
* <dt>fontSize</dt><dd>The cascaded fontSize of the changedNode</dd>
2827
* @type {Event.Custom}
2832
* @description Fired after the frame is ready.
2833
* @param {Event.Facade} event An Event Facade object.
2834
* @type {Event.Custom}
2841
}, '3.2.0' ,{skinnable:false, requires:['base', 'frame', 'node', 'exec-command']});
2842
YUI.add('editor-lists', function(Y) {
2845
* Handles list manipulation inside the Editor. Adds keyboard manipulation and execCommand support. Adds overrides for the <a href="Plugin.ExecCommand.html#method_COMMANDS.insertorderedlist">insertorderedlist</a> and <a href="Plugin.ExecCommand.html#method_COMMANDS.insertunorderedlist">insertunorderedlist</a> execCommands.
2847
* @submodule editor-lists
2850
* Handles list manipulation inside the Editor. Adds keyboard manipulation and execCommand support. Adds overrides for the <a href="Plugin.ExecCommand.html#method_COMMANDS.insertorderedlist">insertorderedlist</a> and <a href="Plugin.ExecCommand.html#method_COMMANDS.insertunorderedlist">insertunorderedlist</a> execCommands.
2851
* @class Plugin.EditorLists
2856
var EditorLists = function() {
2857
EditorLists.superclass.constructor.apply(this, arguments);
2858
}, LI = 'li', OL = 'ol', UL = 'ul', HOST = 'host';
2860
Y.extend(EditorLists, Y.Base, {
2862
* Listener for host's nodeChange event and captures the tabkey interaction only when inside a list node.
2864
* @method _onNodeChange
2865
* @param {Event} e The Event facade passed from the host.
2867
_onNodeChange: function(e) {
2868
var inst = this.get(HOST).getInstance(), sel, li,
2869
newLi, newList, sTab, par, moved = false, tag, focusEnd = false;
2871
if (Y.UA.ie && e.changedType === 'enter') {
2872
if (e.changedNode.test(LI + ', ' + LI + ' *')) {
2873
Y.log('Overriding the Enter Key', 'info', 'editorLists');
2874
e.changedEvent.halt();
2877
newLi = inst.Node.create('<' + LI + '>' + EditorLists.NON + '</' + LI + '>');
2880
li = li.ancestor(LI);
2882
li.insert(newLi, 'after');
2884
sel = new inst.Selection();
2885
sel.selectNode(newLi.get('firstChild'), true, false);
2888
if (e.changedType === 'tab') {
2889
if (e.changedNode.test(LI + ', ' + LI + ' *')) {
2890
Y.log('Overriding TAB to move lists around', 'info', 'editorLists');
2891
e.changedEvent.halt();
2894
sTab = e.changedEvent.shiftKey;
2895
par = li.ancestor(OL + ',' + UL);
2899
if (par.get('tagName').toLowerCase() === OL) {
2902
Y.log('ShiftKey: ' + sTab, 'info', 'editorLists');
2905
li = li.ancestor(LI);
2908
if (li.ancestor(LI)) {
2909
Y.log('Shifting list up one level', 'info', 'editorLists');
2910
li.ancestor(LI).insert(li, 'after');
2915
//li.setStyle('border', '1px solid red');
2916
if (li.previous(LI)) {
2917
Y.log('Shifting list down one level', 'info', 'editorLists');
2918
newList = inst.Node.create('<' + tag + '></' + tag + '>');
2919
li.previous(LI).append(newList);
2927
li = li.ancestor(LI);
2929
li.all(EditorLists.REMOVE).remove();
2931
li = li.append(EditorLists.NON).one(EditorLists.NON_SEL);
2934
Y.log('Selecting the new node', 'info', 'editorLists');
2935
(new inst.Selection()).selectNode(li, true, focusEnd);
2939
initializer: function() {
2940
this.get(HOST).on('nodeChange', Y.bind(this._onNodeChange, this));
2944
* The non element placeholder, used for positioning the cursor and filling empty items
2948
NON: '<span class="yui-non"> </span>',
2950
* The selector query to get all non elements
2954
NON_SEL: 'span.yui-non',
2956
* The items to removed from a list when a list item is moved, currently removes BR nodes
2966
NAME: 'editorLists',
2981
Y.namespace('Plugin');
2983
Y.Plugin.EditorLists = EditorLists;
2985
Y.mix(Y.Plugin.ExecCommand.COMMANDS, {
2987
* Override for the insertunorderedlist method from the <a href="Plugin.EditorLists.html">EditorLists</a> plugin.
2989
* @method COMMANDS.insertunorderedlist
2991
* @param {String} cmd The command executed: insertunorderedlist
2992
* @return {Node} Node instance of the item touched by this command.
2994
insertunorderedlist: function(cmd) {
2995
var inst = this.get('host').getInstance(), out;
2996
this.get('host')._execCommand(cmd, '');
2999
* Override for the insertorderedlist method from the <a href="Plugin.EditorLists.html">EditorLists</a> plugin.
3001
* @method COMMANDS.insertorderedlist
3003
* @param {String} cmd The command executed: insertorderedlist
3004
* @return {Node} Node instance of the item touched by this command.
3006
insertorderedlist: function(cmd) {
3007
var inst = this.get('host').getInstance(), out;
3008
this.get('host')._execCommand(cmd, '');
3015
}, '3.2.0' ,{skinnable:false, requires:['editor-base']});
3016
YUI.add('editor-bidi', function(Y) {
3021
* Plugin for Editor to support BiDirectional (bidi) text operations.
3023
* @submodule editor-bidi
3026
* Plugin for Editor to support BiDirectional (bidi) text operations.
3027
* @class Plugin.EditorBidi
3033
var EditorBidi = function() {
3034
EditorBidi.superclass.constructor.apply(this, arguments);
3035
}, HOST = 'host', DIR = 'dir', BODY = 'BODY', NODE_CHANGE = 'nodeChange',
3036
B_C_CHANGE = 'bidiContextChange', FIRST_P = BODY + ' > p';
3038
Y.extend(EditorBidi, Y.Base, {
3040
* Place holder for the last direction when checking for a switch
3042
* @property lastDirection
3044
lastDirection: null,
3046
* Tells us that an initial bidi check has already been performed
3048
* @property firstEvent
3053
* Method checks to see if the direction of the text has changed based on a nodeChange event.
3055
* @method _checkForChange
3057
_checkForChange: function() {
3058
var host = this.get(HOST),
3059
inst = host.getInstance(),
3060
sel = new inst.Selection(),
3063
if (sel.isCollapsed) {
3064
node = EditorBidi.blockParent(sel.focusNode);
3065
direction = node.getStyle('direction');
3066
if (direction !== this.lastDirection) {
3067
host.fire(B_C_CHANGE, { changedTo: direction });
3068
this.lastDirection = direction;
3071
host.fire(B_C_CHANGE, { changedTo: 'select' });
3072
this.lastDirection = null;
3077
* Checked for a change after a specific nodeChange event has been fired.
3079
* @method _afterNodeChange
3081
_afterNodeChange: function(e) {
3082
// If this is the first event ever, or an event that can result in a context change
3083
if (this.firstEvent || EditorBidi.EVENTS[e.changedType]) {
3084
this._checkForChange();
3085
this.firstEvent = false;
3090
* Checks for a direction change after a mouseup occurs.
3092
* @method _afterMouseUp
3094
_afterMouseUp: function(e) {
3095
this._checkForChange();
3096
this.firstEvent = false;
3098
initializer: function() {
3099
var host = this.get(HOST);
3101
this.firstEvent = true;
3103
host.after(NODE_CHANGE, Y.bind(this._afterNodeChange, this));
3104
host.after('dom:mouseup', Y.bind(this._afterMouseUp, this));
3108
* The events to check for a direction change on
3113
'backspace-up': true,
3115
'pagedown-down': true,
3126
* More elements may be needed. BODY *must* be in the list to take care of the special case.
3128
* blockParent could be changed to use inst.Selection.BLOCKS
3129
* instead, but that would make Y.Plugin.EditorBidi.blockParent
3130
* unusable in non-RTE contexts (it being usable is a nice
3135
BLOCKS: Y.Selection.BLOCKS+',LI,HR,' + BODY,
3137
* Template for creating a block element
3139
* @property DIV_WRAPPER
3141
DIV_WRAPPER: '<DIV></DIV>',
3143
* Returns a block parent for a given element
3145
* @method blockParent
3147
blockParent: function(node, wrap) {
3148
var parent = node, divNode, firstChild;
3151
parent = Y.one(BODY);
3154
if (!parent.test(EditorBidi.BLOCKS)) {
3155
parent = parent.ancestor(EditorBidi.BLOCKS);
3157
if (wrap && parent.test(BODY)) {
3158
// This shouldn't happen if the RTE handles everything
3159
// according to spec: we should get to a P before BODY. But
3160
// we don't want to set the direction of BODY even if that
3161
// happens, so we wrap everything in a DIV.
3163
// The code is based on YUI3's Y.Selection._wrapBlock function.
3164
divNode = Y.Node.create(EditorBidi.DIV_WRAPPER);
3165
parent.get('children').each(function(node, index) {
3169
divNode.append(node);
3172
firstChild.replace(divNode);
3173
divNode.prepend(firstChild);
3179
* The data key to store on the node.
3181
* @property _NODE_SELECTED
3183
_NODE_SELECTED: 'bidiSelected',
3185
* Generates a list of all the block parents of the current NodeList
3187
* @method addParents
3189
addParents: function(nodeArray) {
3190
var i, parent, addParent;
3192
for (i = 0; i < nodeArray.length; i += 1) {
3193
nodeArray[i].setData(EditorBidi._NODE_SELECTED, true);
3196
// This works automagically, since new parents added get processed
3197
// later themselves. So if there's a node early in the process that
3198
// we haven't discovered some of its siblings yet, thus resulting in
3199
// its parent not added, the parent will be added later, since those
3200
// siblings will be added to the array and then get processed.
3201
for (i = 0; i < nodeArray.length; i += 1) {
3202
parent = nodeArray[i].get('parentNode');
3204
// Don't add the parent if the parent is the BODY element.
3205
// We don't want to change the direction of BODY. Also don't
3206
// do it if the parent is already in the list.
3207
if (!parent.test(BODY) && !parent.getData(EditorBidi._NODE_SELECTED)) {
3209
parent.get('children').some(function(sibling) {
3210
if (!sibling.getData(EditorBidi._NODE_SELECTED)) {
3212
return true; // stop more processing
3216
nodeArray.push(parent);
3217
parent.setData(EditorBidi._NODE_SELECTED, true);
3222
for (i = 0; i < nodeArray.length; i += 1) {
3223
nodeArray[i].clearData(EditorBidi._NODE_SELECTED);
3249
Y.namespace('Plugin');
3251
Y.Plugin.EditorBidi = EditorBidi;
3254
* bidi execCommand override for setting the text direction of a node.
3255
* @for Plugin.ExecCommand
3256
* @property COMMANDS.bidi
3259
Y.Plugin.ExecCommand.COMMANDS.bidi = function(cmd, direction) {
3260
var inst = this.getInstance(),
3261
sel = new inst.Selection(),
3263
selected, selectedBlocks;
3265
inst.Selection.filterBlocks();
3266
if (sel.isCollapsed) { // No selection
3267
block = EditorBidi.blockParent(sel.anchorNode);
3268
block.setAttribute(DIR, direction);
3269
returnValue = block;
3270
} else { // some text is selected
3271
selected = sel.getSelected();
3272
selectedBlocks = [];
3273
selected.each(function(node) {
3274
if (!node.test(BODY)) { // workaround for a YUI bug
3275
selectedBlocks.push(EditorBidi.blockParent(node));
3278
selectedBlocks = inst.all(EditorBidi.addParents(selectedBlocks));
3279
selectedBlocks.setAttribute(DIR, direction);
3280
returnValue = selectedBlocks;
3283
this.get(HOST).get(HOST).editorBidi.checkForChange();
3290
}, '3.2.0' ,{skinnable:false, requires:['editor-base', 'selection']});
3291
YUI.add('editor-para', function(Y) {
3296
* Plugin for Editor to paragraph auto wrapping and correction.
3298
* @submodule editor-para
3301
* Plugin for Editor to paragraph auto wrapping and correction.
3302
* @class Plugin.EditorPara
3308
var EditorPara = function() {
3309
EditorPara.superclass.constructor.apply(this, arguments);
3310
}, HOST = 'host', BODY = 'body', NODE_CHANGE = 'nodeChange',
3311
FIRST_P = BODY + ' > p';
3313
Y.extend(EditorPara, Y.Base, {
3315
* Utility method to create an empty paragraph when the document is empty.
3317
* @method _fixFirstPara
3319
_fixFirstPara: function() {
3320
var host = this.get(HOST), inst = host.getInstance(), sel;
3321
inst.one('body').setContent('<p>' + inst.Selection.CURSOR + '</p>');
3322
sel = new inst.Selection();
3323
sel.focusCursor(true, false);
3326
* nodeChange handler to handle fixing an empty document.
3328
* @method _onNodeChange
3330
_onNodeChange: function(e) {
3331
var host = this.get(HOST), inst = host.getInstance();
3333
switch (e.changedType) {
3335
if (inst.config.doc.childNodes.length < 2) {
3336
var cont = inst.config.doc.body.innerHTML;
3337
if (cont && cont.length < 5 && cont.toLowerCase() == '<br>') {
3338
this._fixFirstPara();
3342
case 'backspace-up':
3344
var ps = inst.all(FIRST_P), br, item;
3345
if (ps.size() < 2) {
3346
item = inst.one(BODY);
3350
if (inst.Selection.getText(item) === '' && !item.test('p')) {
3351
this._fixFirstPara();
3352
} else if (item.test('p') && item.get('innerHTML').length === 0) {
3353
e.changedEvent.halt();
3361
* Performs a block element filter when the Editor is first ready
3363
* @method _afterEditorReady
3365
_afterEditorReady: function() {
3366
var host = this.get(HOST), inst = host.getInstance();
3368
inst.Selection.filterBlocks();
3372
* Performs a block element filter when the Editor after an content change
3374
* @method _afterContentChange
3376
_afterContentChange: function() {
3377
var host = this.get(HOST), inst = host.getInstance();
3378
if (inst && inst.Selection) {
3379
inst.Selection.filterBlocks();
3383
* Performs block/paste filtering after paste.
3385
* @method _afterPaste
3387
_afterPaste: function() {
3388
var host = this.get(HOST), inst = host.getInstance(),
3389
sel = new inst.Selection();
3393
Y.later(50, host, function() {
3394
inst.Selection.filterBlocks();
3395
sel.focusCursor(true, true);
3399
initializer: function() {
3400
var host = this.get(HOST);
3402
host.on(NODE_CHANGE, Y.bind(this._onNodeChange, this));
3403
host.after('ready', Y.bind(this._afterEditorReady, this));
3404
host.after('contentChange', Y.bind(this._afterContentChange, this));
3405
host.after('dom:paste', Y.bind(this._afterPaste, this));
3427
Y.namespace('Plugin');
3429
Y.Plugin.EditorPara = EditorPara;
3433
}, '3.2.0' ,{skinnable:false, requires:['editor-base', 'selection']});
3436
YUI.add('editor', function(Y){}, '3.2.0' ,{use:['frame', 'selection', 'exec-command', 'editor-base'], skinnable:false});