diff --git a/build/gallery-accordion/gallery-accordion-debug.js b/build/gallery-accordion/gallery-accordion-debug.js index 330f1c21bb..07542fc17c 100644 --- a/build/gallery-accordion/gallery-accordion-debug.js +++ b/build/gallery-accordion/gallery-accordion-debug.js @@ -1,7 +1,7 @@ YUI.add('gallery-accordion', function(Y) { /** - * Provides the Accordion class + * Provides Accordion widget * * @module gallery-accordion */ @@ -1712,7 +1712,7 @@ Y.Accordion = Accordion; }()); /** - * Provides the Accordion class + * Provides AccordionItem class * * @module gallery-accordion */ @@ -1785,7 +1785,12 @@ var Lang = Y.Lang, HREF = "href", HREF_VALUE = "#", YUICONFIG = "yuiConfig", - HEADER_CONTENT = "headerContent"; + HEADER_CONTENT = "headerContent", + + REGEX_TRUE = /^(?:true|yes|1)$/, + REGEX_AUTO = /^auto\s*/, + REGEX_STRETCH = /^stretch\s*/, + REGEX_FIXED = /^fixed-\d+/; /** * Static property provides a string to identify the class. @@ -1906,7 +1911,7 @@ AccordionItem.ATTRS = { }, /** - * @description Get/Set the expanded status of the item + * @description Get/Set expanded status of the item * * @attribute expanded * @default false @@ -1952,7 +1957,7 @@ AccordionItem.ATTRS = { }, /** - * @description Get/Set the expanded status of the item + * @description Get/Set always visible status of the item * * @attribute alwaysVisible * @default false @@ -2032,7 +2037,7 @@ AccordionItem.HTML_PARSER = { }, label: function( contentBox ){ - var node, labelSelector, yuiConfig; + var node, labelSelector, yuiConfig, label; yuiConfig = this._getConfigDOMAttribute( contentBox ); @@ -2040,6 +2045,12 @@ AccordionItem.HTML_PARSER = { return yuiConfig.label; } + label = contentBox.getAttribute( "data-label" ); + + if( label ){ + return label; + } + labelSelector = HEADER_SELECTOR_SUB + C_LABEL; node = contentBox.query( labelSelector ); @@ -2092,47 +2103,70 @@ AccordionItem.HTML_PARSER = { }, expanded: function( contentBox ){ - var yuiConfig = this._getConfigDOMAttribute( contentBox ); + var yuiConfig, expanded; + + yuiConfig = this._getConfigDOMAttribute( contentBox ); - if( yuiConfig && Lang.isValue( yuiConfig.expanded ) ){ + if( yuiConfig && Lang.isBoolean( yuiConfig.expanded ) ){ return yuiConfig.expanded; } + expanded = contentBox.getAttribute( "data-expanded" ); + + if( expanded ) { + return REGEX_TRUE.test( expanded ); + } + return contentBox.hasClass( C_EXPANDED ); }, alwaysVisible: function( contentBox ){ - var value, yuiConfig; + var yuiConfig, alwaysVisible; yuiConfig = this._getConfigDOMAttribute( contentBox ); - if( yuiConfig && Lang.isValue( yuiConfig.alwaysVisible ) ){ - value = yuiConfig.alwaysVisible; + if( yuiConfig && Lang.isBoolean( yuiConfig.alwaysVisible ) ){ + alwaysVisible = yuiConfig.alwaysVisible; } else { - value = contentBox.hasClass( C_ALWAYSVISIBLE ); + alwaysVisible = contentBox.getAttribute( "data-alwaysvisible" ); + + if( alwaysVisible ) { + alwaysVisible = REGEX_TRUE.test( alwaysVisible ); + } else { + alwaysVisible = contentBox.hasClass( C_ALWAYSVISIBLE ); + } } - if( Lang.isBoolean(value) && value ){ + if( alwaysVisible ){ this.set( "expanded", true, { internalCall: true } ); } - return value; + return alwaysVisible; }, closable: function( contentBox ){ - var yuiConfig = this._getConfigDOMAttribute( contentBox ); + var yuiConfig, closable; + + yuiConfig = this._getConfigDOMAttribute( contentBox ); - if( yuiConfig && Lang.isValue( yuiConfig.closable ) ){ + if( yuiConfig && Lang.isBoolean( yuiConfig.closable ) ){ return yuiConfig.closable; } + closable = contentBox.getAttribute( "data-closable" ); + + if( closable ) { + return REGEX_TRUE.test( closable ); + } + return contentBox.hasClass( C_CLOSABLE ); }, contentHeight: function( contentBox ){ - var contentHeightClass, classValue, height = 0, i, length, index, chr, yuiConfig; + var contentHeightClass, classValue, height = 0, index, yuiConfig, + contentHeight; yuiConfig = this._getConfigDOMAttribute( contentBox ); @@ -2140,6 +2174,26 @@ AccordionItem.HTML_PARSER = { return yuiConfig.contentHeight; } + contentHeight = contentBox.getAttribute( "data-contentheight" ); + + if( REGEX_AUTO.test( contentHeight ) ){ + return { + method: AUTO + }; + } else if( REGEX_STRETCH.test( contentHeight ) ){ + return { + method: STRETCH + }; + } else if( REGEX_FIXED.test( contentHeight ) ){ + height = this._extractFixedMethodValue( contentHeight ); + + return { + method: FIXED, + height: height + }; + } + + classValue = contentBox.get( CLASS_NAME ); contentHeightClass = C_CONTENTHEIGHT + '-'; @@ -2147,31 +2201,21 @@ AccordionItem.HTML_PARSER = { index = classValue.indexOf( contentHeightClass, 0); if( index >= 0 ){ - length = classValue.length; index += contentHeightClass.length; classValue = classValue.substring( index ); - if( classValue.match( /^auto\s*/g ) ){ + if( REGEX_AUTO.test( classValue ) ){ return { method: AUTO }; - } else if( classValue.match( /^stretch\s*/g ) ){ + } else if( REGEX_STRETCH.test( classValue ) ){ return { method: STRETCH }; - } else if( classValue.match( /^fixed-\d+/g ) ){ - for( i = 6, length = classValue.length; i < length; i++ ){ // 6 = "fixed-".length - chr = classValue.charAt(i); - chr = parseInt( chr, 10 ); - - if( Lang.isNumber( chr ) ){ - height = (height * 10) + chr; - } else { - break; - } - } - + } else if( REGEX_FIXED.test( classValue ) ){ + height = this._extractFixedMethodValue( classValue ); + return { method: FIXED, height: height @@ -2363,7 +2407,7 @@ Y.extend( AccordionItem, Y.Widget, { if( this.get( RENDERED ) ){ label = this.get( NODE_LABEL ); - label.set( INNER_HTML, ["", params.newVal, "" ].join('') ); + label.set( INNER_HTML, params.newVal ); } }, @@ -2578,9 +2622,9 @@ Y.extend( AccordionItem, Y.Widget, { * This function will be replaced with more clever solution when YUI 3.1 becomes available * * @method _getConfigDOMAttribute - * @private * @param {Node} contentBox Widget's contentBox * @return {Object} The parsed yuiConfig value + * @private */ _getConfigDOMAttribute: function( contentBox ) { if( !this._parsedCfg ){ @@ -2592,6 +2636,33 @@ Y.extend( AccordionItem, Y.Widget, { } return this._parsedCfg; + }, + + + /** + * Parses and returns the value of contentHeight property, if set method "fixed". + * The value must be in this format: fixed-X, where X is integer + * + * @method _extractFixedMethodValue + * @param {String} value The value to be parsed + * @return {Number} The parsed value or null + * @protected + */ + _extractFixedMethodValue: function( value ){ + var i, length, chr, height = null; + + for( i = 6, length = value.length; i < length; i++ ){ // 6 = "fixed-".length + chr = value.charAt(i); + chr = parseInt( chr, 10 ); + + if( Lang.isNumber( chr ) ){ + height = (height * 10) + chr; + } else { + break; + } + } + + return height; } }); @@ -2607,4 +2678,4 @@ Y.AccordionItem = AccordionItem; -}, 'gallery-2009.10.27' ,{requires:['event', 'anim-easing', 'dd-constrain', 'dd-proxy', 'dd-drop', 'widget', 'widget-stdmod', 'json-parse']}); +}, 'gallery-2009.11.09-19' ,{requires:['event', 'anim-easing', 'dd-constrain', 'dd-proxy', 'dd-drop', 'widget', 'widget-stdmod', 'json-parse']}); diff --git a/build/gallery-accordion/gallery-accordion-min.js b/build/gallery-accordion/gallery-accordion-min.js index 11c7799103..fc6b052faa 100644 --- a/build/gallery-accordion/gallery-accordion-min.js +++ b/build/gallery-accordion/gallery-accordion-min.js @@ -1,4 +1,4 @@ YUI.add("gallery-accordion",function(A){(function(){function M(v){M.superclass.constructor.apply(this,arguments);}var u=A.Lang,V=A.Node,Z=A.Anim,H=A.Easing,N="accordion",X=A.WidgetStdMod,s=document.compatMode=="BackCompat",T=s&&A.UA.ie>0,U=T?1:0,I=A.ClassNameManager.getClassName,W="yui-accordion-item",f=I(N,"proxyel","visible"),h=I(N,"graggroup"),b="beforeItemAdd",e="itemAdded",D="beforeItemRemove",i="itemRemoved",C="beforeItemResized",p="itemResized",d="beforeItemExpand",g="beforeItemCollapse",J="itemExpanded",m="itemCollapsed",j="beforeItemReorder",R="beforeEndItemReorder",S="itemReordered",k="default",O="animation",P="alwaysVisible",E="expanded",c="collapseOthersOnExpand",Y="items",t="contentHeight",B="iconClose",F="iconAlwaysVisible",G="stretch",r="px",a="contentBox",n="boundingBox",l="rendered",o="bodyContent",Q="children",K="parentNode",q="node",L="data";M.NAME=N;M.ATTRS={itemChosen:{value:"click",validator:u.isString},items:{value:[],readOnly:true,validator:u.isArray},resizeEvent:{value:k,validator:function(v){return(u.isString(v)||u.isObject(v));}},useAnimation:{value:true,validator:u.isBoolean},animation:{value:{duration:1,easing:H.easeOutStrong},validator:function(v){return u.isObject(v)&&u.isNumber(v.duration)&&u.isFunction(v.easing);}},reorderItems:{value:true,validator:u.isBoolean},collapseOthersOnExpand:{value:true,validator:u.isBoolean}};A.extend(M,A.Widget,{initializer:function(v){this._initEvents();this.after("render",A.bind(this._afterRender,this));this._forCollapsing={};this._forExpanding={};this._animations={};},destructor:function(){var v,y,w,x;v=this.get(Y);x=v.length;for(w=x-1;w>=0;w--){y=v[w];v.splice(w,1);this._removeItemHandles(y);y.destroy();}},_initEvents:function(){this.publish(b);this.publish(e);this.publish(D);this.publish(i);this.publish(C);this.publish(p);this.publish(d);this.publish(g);this.publish(J);this.publish(m);this.publish(j);this.publish(R);this.publish(S);},_itemsHandles:{},_removeItemHandles:function(x){var w,v;w=this._itemsHandles[x];for(v in w){if(w.hasOwnProperty(v)){v=w[v];v.detach();}}delete this._itemsHandles[x];},_getNodeOffsetHeight:function(x){var v,w;if(x instanceof V){if(x.hasMethod("getBoundingClientRect")){w=x.invoke("getBoundingClientRect");if(w){v=w.bottom-w.top;return v;}}else{v=x.get("offsetHeight");return A.Lang.isValue(v)?v:0;}}else{if(x){v=x.offsetHeight;return A.Lang.isValue(v)?v:0;}}return 0;},_setItemProperties:function(x,z,w){var v,y;v=x.get(P);y=x.get(E);if(z!=y){x.set(E,z,{internalCall:true});}if(w!==v){x.set(P,w,{internalCall:true});}},_setItemUI:function(w,x,v){w.markAsExpanded(x);w.markAsAlwaysVisible(v);},_afterRender:function(w){var v;v=this.get("resizeEvent");this._setUpResizing(v);this.after("resizeEventChange",A.bind(this._afterResizeEventChange,this));},_afterResizeEventChange:function(v){this._setUpResizing(v.newValue);},_onItemChosen:function(AA,AB,v){var z,x,y,w;z={};w=this.get(c);x=AA.get(P);y=AA.get(E);if(v){this.removeItem(AA);return;}else{if(AB){if(y){x=!x;y=x?true:y;this._setItemProperties(AA,y,x);this._setItemUI(AA,y,x);return;}else{this._forExpanding[AA]={"item":AA,alwaysVisible:true};if(w){z[AA]={"item":AA};this._storeItemsForCollapsing(z);}}}else{if(y){this._forCollapsing[AA]={"item":AA};}else{this._forExpanding[AA]={"item":AA,"alwaysVisible":x};if(w){z[AA]={"item":AA};this._storeItemsForCollapsing(z);}}}}this._processItems();},_adjustStretchItems:function(){var v=this.get(Y),w;w=this._getHeightPerStretchItem();A.Array.each(v,function(AB,AA,z){var x,AD,AC,AE,y;AE=AB.get(t);y=AB.get(E);if(AE.method===G&&y){AC=this._animations[AB];if(AC){AC.stop();}x=AB.getStdModNode(X.BODY);AD=this._getNodeOffsetHeight(x);if(wAD){this._processExpanding(AB,w);}}}},this);return w;},_getHeightPerStretchItem:function(){var v,x,w=0;x=this.get(Y);v=this.get(n).get("clientHeight");A.Array.each(x,function(AC,AB,AA){var AD,z,AF,AE,y;AF=AC.getStdModNode(X.HEADER);AE=AC.get(t);y=this._getNodeOffsetHeight(AF);v-=y;AD=!AC.get(E);if(AD){v-=U;return;}if(AE.method===G){w++;}else{z=this._getItemContentHeight(AC);v-=z;}},this);if(w>0){v/=w;}if(v<0){v=0;}return v;},_getItemContentHeight:function(x){var z,w=0,v,y;z=x.get(t);if(z.method==="auto"){v=x.getStdModNode(X.BODY);y=v.get(Q).item(0);w=y?this._getNodeOffsetHeight(y):0;}else{if(z.method==="fixed"){w=z.height;}else{w=this._getHeightPerStretchItem();}}return w;},_storeItemsForCollapsing:function(w){var v;w=w||{};v=this.get(Y);A.Array.each(v,function(AB,AA,z){var y,x;y=AB.get(E);x=AB.get(P);if(y&&!x&&!w[AB]){this._forCollapsing[AB]={"item":AB};}},this);},_expandItem:function(x,v){var w=x.get(P);this._processExpanding(x,v);this._setItemUI(x,true,w);},_processExpanding:function(AC,AB,v){var w,x,z,AD=false,AA,y;y=AC.getStdModNode(X.BODY);this.fire(C,{"item":AC});if(y.get("clientHeight")<=0){AD=true;this.fire(d,{"item":AC});}if(!v&&this.get("useAnimation")){z=AC.get(O)||{};w=new Z({node:y,to:{"height":AB}});w.on("end",A.bind(this._onExpandComplete,this,AC,AD));AA=this.get(O);w.set("duration",z.duration||AA.duration);w.set("easing",z.easing||AA.easing);x=this._animations[AC];if(x){x.stop();}AC.markAsExpanding(true);this._animations[AC]=w;w.run();}else{y.setStyle("height",AB+r);this.fire(p,{"item":AC});if(AD){this.fire(J,{"item":AC});}}},_onExpandComplete:function(v,w){delete this._animations[v];v.markAsExpanding(false);this.fire(p,{"item":v});if(w){this.fire(J,{"item":v});}},_collapseItem:function(v){this._processCollapsing(v,U);this._setItemUI(v,false,false);},_processCollapsing:function(AC,AB,v){var w,x,z,AA,y,AD=(AB===U);y=AC.getStdModNode(X.BODY);this.fire(C,{"item":AC});if(AD){this.fire(g,{"item":AC});}if(!v&&this.get("useAnimation")){z=AC.get(O)||{};w=new Z({node:y,to:{"height":AB}});w.on("end",A.bind(this._onCollapseComplete,this,AC,AD));AA=this.get(O);w.set("duration",z.duration||AA.duration);w.set("easing",z.easing||AA.easing);x=this._animations[AC];if(x){x.stop();}AC.markAsCollapsing(true);this._animations[AC]=w;w.run();}else{y.setStyle("height",AB+r); this.fire(p,{"item":AC});if(AD){this.fire(m,{"item":AC});}}},_onCollapseComplete:function(v,w){delete this._animations[v];v.markAsCollapsing(false);this.fire(p,{item:v});if(w){this.fire(m,{"item":v});}},_initItemDragDrop:function(w){var AA,v,z,x,y;AA=w.getStdModNode(X.HEADER);if(AA.dd){return;}z=this.get(n);x=w.get(n);v=new A.DD.Drag({node:AA,groups:[h]}).plug(A.Plugin.DDProxy,{moveOnEnd:false}).plug(A.Plugin.DDConstrained,{constrain2node:z});y=new A.DD.Drop({node:x,groups:[h]});v.on("drag:start",A.bind(this._onDragStart,this,v));v.on("drag:end",A.bind(this._onDragEnd,this,v));v.after("drag:end",A.bind(this._afterDragEnd,this,v));v.on("drag:drophit",A.bind(this._onDropHit,this,v));},_onDragStart:function(v,y){var x,w;w=this.getItem(v.get(q).get(K));x=v.get("dragNode");x.addClass(f);x.set("innerHTML",w.get("label"));return this.fire(j,{"item":w});},_onDragEnd:function(v,y){var x,w;x=v.get("dragNode");x.removeClass(f);x.set("innerHTML","");w=this.getItem(v.get(q).get(K));return this.fire(R,{"item":w});},_afterDragEnd:function(v,y){var w,x;x=v.get(L);if(x.drophit){w=this.getItem(v.get(q).get(K));v.set(L,{drophit:false});return this.fire(S,{"item":w});}return true;},_onDropHit:function(AE,AA){var z,AD,w,AC,y,v,AB,x,AF;AF=this.getItem(AE.get(q).get(K));x=this.getItem(AA.drop.get(q));if(x===AF){return false;}z=this.getItemIndex(AF);AD=this.getItemIndex(x);w=x.get(n);AC=AF.get(n);y=this.get(a);v=false;AB=this.get(Y);if(ADz){this._processExpanding(y,w,!x);}}}},_setUpResizing:function(v){if(this._resizeEventHandle){this._resizeEventHandle.detach();}if(v===k){this._resizeEventHandle=A.on("windowresize",A.bind(this._adjustStretchItems,this));}else{this._resizeEventHandle=v.sourceObject.on(v.resizeEvent,A.bind(this._adjustStretchItems,this));}},renderUI:function(){var v,w;v=this.get(a);w=v.queryAll("> div."+W);w.each(function(AA,x,z){var y;if(!this.getItem(AA)){y=new A.AccordionItem({contentBox:AA});this.addItem(y);}},this);},bindUI:function(){var v,w;v=this.get(a);w=this.get("itemChosen");v.delegate(w,A.bind(this._onItemChosenEvent,this),"div.yui-widget-hd");},_onItemChosenEvent:function(AA){var AC,AB,x,y,w,z,v;AC=AA.currentTarget;AB=AC.get(K);x=this.getItem(AB);y=x.get(F);w=x.get(B);z=(y===AA.target);v=(w===AA.target);this._onItemChosen(x,z,v);},addItem:function(AI,y){var AB,AG,AF,v,AD,AE,AH,AA,AC,z,x,w;AC=this.fire(b,{"item":AI});if(!AC){return false;}AD=this.get(Y);AE=this.get(a);AA=AI.get(a);w=AI.get(n);if(!AA.inDoc()){if(y){v=this.getItemIndex(y);if(v<0){return false;}AD.splice(v,0,AI);if(AI.get(l)){AE.insertBefore(w,y.get(n));}else{AE.insertBefore(AA,y.get(n));}}else{AD.push(AI);if(AI.get(l)){AE.insertBefore(w,null);}else{AE.insertBefore(AA,null);}}}else{z=this.get(a);x=z.get(Q);AC=x.some(function(AL,AK,AJ){if(AL===AA){AD.splice(AK,0,AI);return true;}else{return false;}},this);if(!AC){return false;}}AF=AI.get(o);if(!AF){AI.set(o," ");}if(!AI.get(l)){AI.render();}AB=AI.get(E);AG=AI.get(P);AB=AB||AG;if(AB){this._forExpanding[AI]={"item":AI,"alwaysVisible":AG};}else{this._forCollapsing[AI]={"item":AI};}this._processItems();if(this.get("reorderItems")){this._initItemDragDrop(AI);}AH=this._itemsHandles[AI];if(!AH){AH={};}AH={"expandedChange":AI.after("expandedChange",A.bind(this._afterItemExpand,this)),"alwaysVisibleChange":AI.after("alwaysVisibleChange",A.bind(this._afterItemAlwaysVisible,this)),"contentHeightChange":AI.after("contentHeightChange",A.bind(this._afterContentHeight,this))};this._itemsHandles[AI]=AH;this.fire(e,{"item":AI});return true;},removeItem:function(w){var v,z,y=null,x;v=this.get(Y);if(u.isNumber(w)){x=w;}else{if(w instanceof A.AccordionItem){x=this.getItemIndex(w);}else{return null;}}if(x>=0){this.fire(D,{item:w});y=v.splice(x,1)[0];this._removeItemHandles(y);z=y.get(n);z.remove();this._adjustStretchItems();this.fire(i,{item:w});}return y;},getItem:function(x){var v=this.get(Y),w=null;if(u.isNumber(x)){w=v[x];return(w instanceof A.AccordionItem)?w:null;}else{if(x instanceof V){A.Array.some(v,function(AC,AB,z){var y,AA;y=AC.get(a);AA=AC.get(n);if(y===x){w=AC;return true;}else{if(AA===x){w=AC;return true;}else{return false;}}},this);}}return w;},getItemIndex:function(x){var w=-1,v;if(x instanceof A.AccordionItem){v=this.get(Y); -A.Array.some(v,function(AA,z,y){if(AA===x){w=z;return true;}else{return false;}},this);}return w;}});A.Accordion=M;}());(function(){function Z(w){Z.superclass.constructor.apply(this,arguments);}var v=A.Lang,Q=A.Base,b=A.Node,O=A.JSON,c=A.WidgetStdMod,e="accordion-item",M=A.ClassNameManager.getClassName,D=M(e,"iconexpanded","expanding"),t=M(e,"iconexpanded","collapsing"),N=M(e,"icon"),K=M(e,"label"),p=M(e,"iconalwaysvisible"),g=M(e,"icons"),n=M(e,"iconexpanded"),k=M(e,"iconclose"),P=M(e,"iconclose","hidden"),S=M(e,"iconexpanded","on"),L=M(e,"iconexpanded","off"),E=M(e,"iconalwaysvisible","on"),l=M(e,"iconalwaysvisible","off"),X=M(e,"expanded"),V=M(e,"closable"),i=M(e,"alwaysvisible"),j=M(e,"contentheight"),m="title",C="strings",d="contentBox",r="rendered",I="className",f="auto",J="stretch",W="fixed",T=".yui-widget-hd",Y=".",o=".yui-widget-hd "+Y,s="innerHTML",u="iconsContainer",h="icon",R="nodeLabel",H="iconAlwaysVisible",a="iconExpanded",B="iconClose",q="href",U="#",G="yuiConfig",F="headerContent";Z.NAME=e;Z.ATTRS={icon:{value:null,validator:function(w){return w instanceof b;}},label:{value:" ",validator:v.isString},nodeLabel:{value:null,validator:function(w){return w instanceof b;}},iconsContainer:{value:null,validator:function(w){return w instanceof b;}},iconExpanded:{value:null,validator:function(w){return w instanceof b;}},iconAlwaysVisible:{value:null,validator:function(w){return w instanceof b;}},iconClose:{value:null,validator:function(w){return w instanceof b;}},expanded:{value:false,validator:v.isBoolean},contentHeight:{value:{method:f},validator:function(w){if(v.isObject(w)){if(w.method===f){return true;}else{if(w.method===J){return true;}else{if(w.method===W&&v.isNumber(w.height)&&w.height>=0){return true;}}}}return false;}},alwaysVisible:{value:false,validator:v.isBoolean},animation:{value:{},validator:v.isObject},strings:{value:{title_always_visible_off:"Click to set always visible on",title_always_visible_on:"Click to set always visible off",title_iconexpanded_off:"Click to expand",title_iconexpanded_on:"Click to collapse",title_iconclose:"Click to close"}},closable:{value:false,validator:v.isBoolean}};Z.HTML_PARSER={icon:function(w){var x,y;y=o+N;x=w.query(y);return x;},label:function(w){var y,z,x;x=this._getConfigDOMAttribute(w);if(x&&v.isValue(x.label)){return x.label;}z=o+K;y=w.query(z);return(y)?y.get(s):null;},nodeLabel:function(w){var x,y;y=o+K;x=w.query(y);return x;},iconsContainer:function(w){var y,x;x=o+g;y=w.query(x);return y;},iconAlwaysVisible:function(w){var x,y;y=o+p;x=w.query(y);return x;},iconExpanded:function(w){var y,x;x=o+n;y=w.query(x);return y;},iconClose:function(w){var y,x;x=o+k;y=w.query(x);return y;},expanded:function(w){var x=this._getConfigDOMAttribute(w);if(x&&v.isValue(x.expanded)){return x.expanded;}return w.hasClass(X);},alwaysVisible:function(w){var y,x;x=this._getConfigDOMAttribute(w);if(x&&v.isValue(x.alwaysVisible)){y=x.alwaysVisible;}else{y=w.hasClass(i);}if(v.isBoolean(y)&&y){this.set("expanded",true,{internalCall:true});}return y;},closable:function(w){var x=this._getConfigDOMAttribute(w);if(x&&v.isValue(x.closable)){return x.closable;}return w.hasClass(V);},contentHeight:function(AD){var y,w,AE=0,AA,x,AB,z,AC;AC=this._getConfigDOMAttribute(AD);if(AC&&AC.contentHeight){return AC.contentHeight;}w=AD.get(I);y=j+"-";AB=w.indexOf(y,0);if(AB>=0){x=w.length;AB+=y.length;w=w.substring(AB);if(w.match(/^auto\s*/g)){return{method:f};}else{if(w.match(/^stretch\s*/g)){return{method:J};}else{if(w.match(/^fixed-\d+/g)){for(AA=6,x=w.length;AA',label:'',iconsContainer:'
',iconExpanded:[''].join(""),iconAlwaysVisible:[''].join(""),iconClose:[''].join("")};A.extend(Z,A.Widget,{_createHeader:function(){var AE,AC,AD,AA,AB,z,x,w,y;AB=this.get(h);z=this.get(R);x=this.get(a);w=this.get(H);y=this.get(B);AA=this.get(u);AD=this.get(C);AE=this.get("closable");AC=Z.TEMPLATES;if(!AB){AB=b.create(AC.icon);this.set(h,AB);}if(!z){z=b.create(AC.label);this.set(R,z);}else{if(!z.hasAttribute(q)){z.setAttribute(q,U);}}z.setContent(this.get("label"));if(!AA){AA=b.create(AC.iconsContainer);this.set(u,AA);}if(!w){w=b.create(AC.iconAlwaysVisible);w.setAttribute(m,AD.title_always_visible_off);this.set(H,w);}else{if(!w.hasAttribute(q)){w.setAttribute(q,U);}}if(!x){x=b.create(AC.iconExpanded);x.setAttribute(m,AD.title_iconexpanded_off);this.set(a,x);}else{if(!x.hasAttribute(q)){x.setAttribute(q,U);}}if(!y){y=b.create(AC.iconClose);y.setAttribute(m,AD.title_iconclose);this.set(B,y);}else{if(!y.hasAttribute(q)){y.setAttribute(q,U);}}if(AE){y.removeClass(P);}else{y.addClass(P);}this._addHeaderComponents();},_addHeaderComponents:function(){var AC,x,AB,y,AA,z,w;x=this.get(h);AB=this.get(R);AA=this.get(a);z=this.get(H);w=this.get(B);y=this.get(u);AC=this.get(F);if(!AC){AC=new b(document.createDocumentFragment());AC.appendChild(x);AC.appendChild(AB);AC.appendChild(y);y.appendChild(z);y.appendChild(AA);y.appendChild(w);this.setStdModContent(c.HEADER,AC,c.REPLACE);}else{if(!AC.contains(x)){if(AC.contains(AB)){AC.insertBefore(x,AB);}else{AC.appendChild(x);}}if(!AC.contains(AB)){AC.appendChild(AB);}if(!AC.contains(y)){AC.appendChild(y);}if(!y.contains(z)){y.appendChild(z);}if(!y.contains(AA)){y.appendChild(AA);}if(!y.contains(w)){y.appendChild(w);}}},_labelChanged:function(x){var w;if(this.get(r)){w=this.get(R);w.set(s,["",x.newVal,""].join(""));}},_closableChanged:function(z){var w,y,x;if(this.get(r)){x=this.get(d);w=o+k;y=x.query(w);if(z.newVal){y.removeClass(P);}else{y.addClass(P);}}},initializer:function(w){this.after("labelChange",A.bind(this._labelChanged,this));this.after("closableChange",A.bind(this._closableChanged,this));},destructor:function(){},renderUI:function(){this._createHeader(); -},bindUI:function(){var w;w=this.get(d);w.delegate("click",A.bind(this._onLinkClick,this),T+" a");},_onLinkClick:function(w){w.preventDefault();},markAsAlwaysVisible:function(x){var y,w;y=this.get(H);w=this.get(C);if(x){if(!y.hasClass(E)){y.replaceClass(l,E);y.set(m,w.title_always_visible_on);return true;}}else{if(y.hasClass(E)){y.replaceClass(E,l);y.set(m,w.title_always_visible_off);return true;}}return false;},markAsExpanded:function(x){var w,y;y=this.get(a);w=this.get(C);if(x){if(!y.hasClass(S)){y.replaceClass(L,S);y.set(m,w.title_iconexpanded_on);return true;}}else{if(y.hasClass(S)){y.replaceClass(S,L);y.set(m,w.title_iconexpanded_off);return true;}}return false;},markAsExpanding:function(x){var w=this.get(a);if(x){if(!w.hasClass(D)){w.addClass(D);return true;}}else{if(w.hasClass(D)){w.removeClass(D);return true;}}return false;},markAsCollapsing:function(w){var x=this.get(a);if(w){if(!x.hasClass(t)){x.addClass(t);return true;}}else{if(x.hasClass(t)){x.removeClass(t);return true;}}return false;},_getConfigDOMAttribute:function(w){if(!this._parsedCfg){this._parsedCfg=w.getAttribute(G);if(this._parsedCfg){this._parsedCfg=O.parse(this._parsedCfg);}}return this._parsedCfg;}});Q.build(Z.NAME,Z,[c],{dynamic:false});A.AccordionItem=Z;}());},"gallery-2009.10.27",{requires:["event","anim-easing","dd-constrain","dd-proxy","dd-drop","widget","widget-stdmod","json-parse"]}); \ No newline at end of file +A.Array.some(v,function(AA,z,y){if(AA===x){w=z;return true;}else{return false;}},this);}return w;}});A.Accordion=M;}());(function(){function c(AA){c.superclass.constructor.apply(this,arguments);}var y=A.Lang,S=A.Base,e=A.Node,Q=A.JSON,f=A.WidgetStdMod,h="accordion-item",O=A.ClassNameManager.getClassName,D=O(h,"iconexpanded","expanding"),w=O(h,"iconexpanded","collapsing"),P=O(h,"icon"),L=O(h,"label"),s=O(h,"iconalwaysvisible"),j=O(h,"icons"),q=O(h,"iconexpanded"),n=O(h,"iconclose"),R=O(h,"iconclose","hidden"),U=O(h,"iconexpanded","on"),M=O(h,"iconexpanded","off"),E=O(h,"iconalwaysvisible","on"),o=O(h,"iconalwaysvisible","off"),a=O(h,"expanded"),X=O(h,"closable"),l=O(h,"alwaysvisible"),m=O(h,"contentheight"),p="title",C="strings",g="contentBox",u="rendered",I="className",i="auto",K="stretch",Y="fixed",V=".yui-widget-hd",b=".",r=".yui-widget-hd "+b,v="innerHTML",x="iconsContainer",k="icon",T="nodeLabel",H="iconAlwaysVisible",d="iconExpanded",B="iconClose",t="href",W="#",G="yuiConfig",F="headerContent",J=/^(?:true|yes|1)$/,N=/^auto\s*/,z=/^stretch\s*/,Z=/^fixed-\d+/;c.NAME=h;c.ATTRS={icon:{value:null,validator:function(AA){return AA instanceof e;}},label:{value:" ",validator:y.isString},nodeLabel:{value:null,validator:function(AA){return AA instanceof e;}},iconsContainer:{value:null,validator:function(AA){return AA instanceof e;}},iconExpanded:{value:null,validator:function(AA){return AA instanceof e;}},iconAlwaysVisible:{value:null,validator:function(AA){return AA instanceof e;}},iconClose:{value:null,validator:function(AA){return AA instanceof e;}},expanded:{value:false,validator:y.isBoolean},contentHeight:{value:{method:i},validator:function(AA){if(y.isObject(AA)){if(AA.method===i){return true;}else{if(AA.method===K){return true;}else{if(AA.method===Y&&y.isNumber(AA.height)&&AA.height>=0){return true;}}}}return false;}},alwaysVisible:{value:false,validator:y.isBoolean},animation:{value:{},validator:y.isObject},strings:{value:{title_always_visible_off:"Click to set always visible on",title_always_visible_on:"Click to set always visible off",title_iconexpanded_off:"Click to expand",title_iconexpanded_on:"Click to collapse",title_iconclose:"Click to close"}},closable:{value:false,validator:y.isBoolean}};c.HTML_PARSER={icon:function(AA){var AB,AC;AC=r+P;AB=AA.query(AC);return AB;},label:function(AA){var AD,AE,AC,AB;AC=this._getConfigDOMAttribute(AA);if(AC&&y.isValue(AC.label)){return AC.label;}AB=AA.getAttribute("data-label");if(AB){return AB;}AE=r+L;AD=AA.query(AE);return(AD)?AD.get(v):null;},nodeLabel:function(AA){var AB,AC;AC=r+L;AB=AA.query(AC);return AB;},iconsContainer:function(AA){var AC,AB;AB=r+j;AC=AA.query(AB);return AC;},iconAlwaysVisible:function(AA){var AB,AC;AC=r+s;AB=AA.query(AC);return AB;},iconExpanded:function(AA){var AC,AB;AB=r+q;AC=AA.query(AB);return AC;},iconClose:function(AA){var AC,AB;AB=r+n;AC=AA.query(AB);return AC;},expanded:function(AA){var AC,AB;AC=this._getConfigDOMAttribute(AA);if(AC&&y.isBoolean(AC.expanded)){return AC.expanded;}AB=AA.getAttribute("data-expanded");if(AB){return J.test(AB);}return AA.hasClass(a);},alwaysVisible:function(AB){var AC,AA;AC=this._getConfigDOMAttribute(AB);if(AC&&y.isBoolean(AC.alwaysVisible)){AA=AC.alwaysVisible;}else{AA=AB.getAttribute("data-alwaysvisible");if(AA){AA=J.test(AA);}else{AA=AB.hasClass(l);}}if(AA){this.set("expanded",true,{internalCall:true});}return AA;},closable:function(AA){var AC,AB;AC=this._getConfigDOMAttribute(AA);if(AC&&y.isBoolean(AC.closable)){return AC.closable;}AB=AA.getAttribute("data-closable");if(AB){return J.test(AB);}return AA.hasClass(X);},contentHeight:function(AB){var AF,AG,AA=0,AC,AE,AD;AE=this._getConfigDOMAttribute(AB);if(AE&&AE.contentHeight){return AE.contentHeight;}AD=AB.getAttribute("data-contentheight");if(N.test(AD)){return{method:i};}else{if(z.test(AD)){return{method:K};}else{if(Z.test(AD)){AA=this._extractFixedMethodValue(AD);return{method:Y,height:AA};}}}AG=AB.get(I);AF=m+"-";AC=AG.indexOf(AF,0);if(AC>=0){AC+=AF.length;AG=AG.substring(AC);if(N.test(AG)){return{method:i};}else{if(z.test(AG)){return{method:K};}else{if(Z.test(AG)){AA=this._extractFixedMethodValue(AG);return{method:Y,height:AA};}}}}return null;}};c.TEMPLATES={icon:'',label:'',iconsContainer:'
',iconExpanded:[''].join(""),iconAlwaysVisible:[''].join(""),iconClose:[''].join("")};A.extend(c,A.Widget,{_createHeader:function(){var AI,AG,AH,AE,AF,AD,AB,AA,AC;AF=this.get(k);AD=this.get(T);AB=this.get(d);AA=this.get(H);AC=this.get(B);AE=this.get(x);AH=this.get(C);AI=this.get("closable");AG=c.TEMPLATES;if(!AF){AF=e.create(AG.icon);this.set(k,AF);}if(!AD){AD=e.create(AG.label);this.set(T,AD);}else{if(!AD.hasAttribute(t)){AD.setAttribute(t,W);}}AD.setContent(this.get("label"));if(!AE){AE=e.create(AG.iconsContainer);this.set(x,AE);}if(!AA){AA=e.create(AG.iconAlwaysVisible);AA.setAttribute(p,AH.title_always_visible_off);this.set(H,AA);}else{if(!AA.hasAttribute(t)){AA.setAttribute(t,W);}}if(!AB){AB=e.create(AG.iconExpanded);AB.setAttribute(p,AH.title_iconexpanded_off);this.set(d,AB);}else{if(!AB.hasAttribute(t)){AB.setAttribute(t,W);}}if(!AC){AC=e.create(AG.iconClose);AC.setAttribute(p,AH.title_iconclose);this.set(B,AC);}else{if(!AC.hasAttribute(t)){AC.setAttribute(t,W);}}if(AI){AC.removeClass(R);}else{AC.addClass(R);}this._addHeaderComponents();},_addHeaderComponents:function(){var AG,AB,AF,AC,AE,AD,AA;AB=this.get(k);AF=this.get(T);AE=this.get(d);AD=this.get(H);AA=this.get(B);AC=this.get(x);AG=this.get(F);if(!AG){AG=new e(document.createDocumentFragment());AG.appendChild(AB);AG.appendChild(AF);AG.appendChild(AC);AC.appendChild(AD);AC.appendChild(AE);AC.appendChild(AA);this.setStdModContent(f.HEADER,AG,f.REPLACE);}else{if(!AG.contains(AB)){if(AG.contains(AF)){AG.insertBefore(AB,AF);}else{AG.appendChild(AB);}}if(!AG.contains(AF)){AG.appendChild(AF);}if(!AG.contains(AC)){AG.appendChild(AC); +}if(!AC.contains(AD)){AC.appendChild(AD);}if(!AC.contains(AE)){AC.appendChild(AE);}if(!AC.contains(AA)){AC.appendChild(AA);}}},_labelChanged:function(AB){var AA;if(this.get(u)){AA=this.get(T);AA.set(v,AB.newVal);}},_closableChanged:function(AD){var AA,AC,AB;if(this.get(u)){AB=this.get(g);AA=r+n;AC=AB.query(AA);if(AD.newVal){AC.removeClass(R);}else{AC.addClass(R);}}},initializer:function(AA){this.after("labelChange",A.bind(this._labelChanged,this));this.after("closableChange",A.bind(this._closableChanged,this));},destructor:function(){},renderUI:function(){this._createHeader();},bindUI:function(){var AA;AA=this.get(g);AA.delegate("click",A.bind(this._onLinkClick,this),V+" a");},_onLinkClick:function(AA){AA.preventDefault();},markAsAlwaysVisible:function(AB){var AC,AA;AC=this.get(H);AA=this.get(C);if(AB){if(!AC.hasClass(E)){AC.replaceClass(o,E);AC.set(p,AA.title_always_visible_on);return true;}}else{if(AC.hasClass(E)){AC.replaceClass(E,o);AC.set(p,AA.title_always_visible_off);return true;}}return false;},markAsExpanded:function(AB){var AA,AC;AC=this.get(d);AA=this.get(C);if(AB){if(!AC.hasClass(U)){AC.replaceClass(M,U);AC.set(p,AA.title_iconexpanded_on);return true;}}else{if(AC.hasClass(U)){AC.replaceClass(U,M);AC.set(p,AA.title_iconexpanded_off);return true;}}return false;},markAsExpanding:function(AB){var AA=this.get(d);if(AB){if(!AA.hasClass(D)){AA.addClass(D);return true;}}else{if(AA.hasClass(D)){AA.removeClass(D);return true;}}return false;},markAsCollapsing:function(AA){var AB=this.get(d);if(AA){if(!AB.hasClass(w)){AB.addClass(w);return true;}}else{if(AB.hasClass(w)){AB.removeClass(w);return true;}}return false;},_getConfigDOMAttribute:function(AA){if(!this._parsedCfg){this._parsedCfg=AA.getAttribute(G);if(this._parsedCfg){this._parsedCfg=Q.parse(this._parsedCfg);}}return this._parsedCfg;},_extractFixedMethodValue:function(AE){var AB,AD,AC,AA=null;for(AB=6,AD=AE.length;AB= 0 ){ - length = classValue.length; index += contentHeightClass.length; classValue = classValue.substring( index ); - if( classValue.match( /^auto\s*/g ) ){ + if( REGEX_AUTO.test( classValue ) ){ return { method: AUTO }; - } else if( classValue.match( /^stretch\s*/g ) ){ + } else if( REGEX_STRETCH.test( classValue ) ){ return { method: STRETCH }; - } else if( classValue.match( /^fixed-\d+/g ) ){ - for( i = 6, length = classValue.length; i < length; i++ ){ // 6 = "fixed-".length - chr = classValue.charAt(i); - chr = parseInt( chr, 10 ); - - if( Lang.isNumber( chr ) ){ - height = (height * 10) + chr; - } else { - break; - } - } - + } else if( REGEX_FIXED.test( classValue ) ){ + height = this._extractFixedMethodValue( classValue ); + return { method: FIXED, height: height @@ -2363,7 +2407,7 @@ Y.extend( AccordionItem, Y.Widget, { if( this.get( RENDERED ) ){ label = this.get( NODE_LABEL ); - label.set( INNER_HTML, ["", params.newVal, "" ].join('') ); + label.set( INNER_HTML, params.newVal ); } }, @@ -2578,9 +2622,9 @@ Y.extend( AccordionItem, Y.Widget, { * This function will be replaced with more clever solution when YUI 3.1 becomes available * * @method _getConfigDOMAttribute - * @private * @param {Node} contentBox Widget's contentBox * @return {Object} The parsed yuiConfig value + * @private */ _getConfigDOMAttribute: function( contentBox ) { if( !this._parsedCfg ){ @@ -2592,6 +2636,33 @@ Y.extend( AccordionItem, Y.Widget, { } return this._parsedCfg; + }, + + + /** + * Parses and returns the value of contentHeight property, if set method "fixed". + * The value must be in this format: fixed-X, where X is integer + * + * @method _extractFixedMethodValue + * @param {String} value The value to be parsed + * @return {Number} The parsed value or null + * @protected + */ + _extractFixedMethodValue: function( value ){ + var i, length, chr, height = null; + + for( i = 6, length = value.length; i < length; i++ ){ // 6 = "fixed-".length + chr = value.charAt(i); + chr = parseInt( chr, 10 ); + + if( Lang.isNumber( chr ) ){ + height = (height * 10) + chr; + } else { + break; + } + } + + return height; } }); @@ -2607,4 +2678,4 @@ Y.AccordionItem = AccordionItem; -}, 'gallery-2009.10.27' ,{requires:['event', 'anim-easing', 'dd-constrain', 'dd-proxy', 'dd-drop', 'widget', 'widget-stdmod', 'json-parse']}); +}, 'gallery-2009.11.09-19' ,{requires:['event', 'anim-easing', 'dd-constrain', 'dd-proxy', 'dd-drop', 'widget', 'widget-stdmod', 'json-parse']}); diff --git a/build/gallery-badge/gallery-badge-debug.js b/build/gallery-badge/gallery-badge-debug.js new file mode 100644 index 0000000000..0dcb1395e2 --- /dev/null +++ b/build/gallery-badge/gallery-badge-debug.js @@ -0,0 +1,145 @@ +YUI.add('gallery-badge', function(Y) { + + + var B = function(config) { + this._lazyAddAttrs = false; + config.node = config.host; + B.superclass.constructor.call(this, config); + }; + + B.NAME = "gallery-badge"; + + B.NS = "badge"; + + B.ATTRS = { + node: { + setter: function(node) { + return Y.one(node); + } + }, + type: { + value: 'popular', + setter: function(v) { + if (v !== 'user') { + this.set('username', ''); + } + return v; + } + }, + header: { + value: '', + getter: function() { + var h = 'Popular Gallery Items'; + switch (this.get('type')) { + case 'oncdn': + h = 'Gallery Items on the CDN'; + break; + case 'featured': + h = 'Featured Gallery Items'; + break; + case 'random': + h = 'Random Gallery Item'; + break; + case 'user': + h = 'My Gallery Items'; + break; + } + return h; + } + }, + username: { + value: '', + setter: function(v) { + if (v) { + this.set('header', 'My Gallery Items'); + this._setStateVal('type', 'user'); + } + return v; + } + }, + render: { + value: true + }, + data: { + value: {} + } + }; + + Y.extend(B, Y.Base, { + buildUI: function(e) { + this.set('data', e.data); + if (e.data.error) { + return; + } + var data = e.data, + type = this.get('type'), + header = Y.Node.create('

' + this.get('header') + '

'), + node = Y.Node.create('
    '), + n = this.get('node'); + + if (type != 'random') { + if (type == 'user') { + header = Y.Node.create('

    ' + data.userinfo.fullname + '\'s Gallery Items

    '); + } + Y.each(data.modules, function(v) { + node.append('
  • ' + v.title + '
  • '); + }); + } else { + node.append('
  • ' + data.modules.title + '
  • '); + } + + n.set('innerHTML', ''); + n.append(header); + n.append(node); + }, + getSQL: function() { + var type = this.get('type'), sql; + if (type == 'user') { + sql = "select * from yui.gallery.user where (username = '" + this.get('username') + "')"; + } else { + sql = "select * from yui.gallery." + type; + } + return sql; + }, + updateUserUI: function(e) { + if (e.newVal !== '') { + this.updateUI(); + } + }, + updateUI: function(e) { + var sql = this.getSQL(), q; + + q = new Y.yql(sql, Y.bind(function(r) { + if (r.query && r.query.results && r.query.results.json) { + this.fire('dataReturned', { data: r.query.results.json }); + } + }, this), { env: 'http:/'+'/yuilibrary.com/yql/yui.env' }); + + }, + initializer: function() { + this.after('typeChange', Y.bind(this.updateUI, this)); + this.after('usernameChange', Y.bind(this.updateUserUI, this)); + this.publish('dataReturned', { + type: 'datareturned', + defaultFn: this.buildUI + }); + if (this.get('render')) { + this.render(); + } + }, + render: function() { + this.updateUI(); + }, + refresh: function() { + this.updateUI(); + }, + destructor: function() { + this.get('node').set('innerHTML', ''); + } + }); + Y.namespace('Plugin'); + Y.Plugin.Badge = B; + + + +}, 'gallery-2009.11.09-19' ,{requires:['node','event','gallery-yql','stylesheet']}); diff --git a/build/gallery-badge/gallery-badge-min.js b/build/gallery-badge/gallery-badge-min.js new file mode 100644 index 0000000000..b9aa478e98 --- /dev/null +++ b/build/gallery-badge/gallery-badge-min.js @@ -0,0 +1 @@ +YUI.add("gallery-badge",function(A){var C=function(B){this._lazyAddAttrs=false;B.node=B.host;C.superclass.constructor.call(this,B);};C.NAME="gallery-badge";C.NS="badge";C.ATTRS={node:{setter:function(B){return A.one(B);}},type:{value:"popular",setter:function(B){if(B!=="user"){this.set("username","");}return B;}},header:{value:"",getter:function(){var B="Popular Gallery Items";switch(this.get("type")){case"oncdn":B="Gallery Items on the CDN";break;case"featured":B="Featured Gallery Items";break;case"random":B="Random Gallery Item";break;case"user":B="My Gallery Items";break;}return B;}},username:{value:"",setter:function(B){if(B){this.set("header","My Gallery Items");this._setStateVal("type","user");}return B;}},render:{value:true},data:{value:{}}};A.extend(C,A.Base,{buildUI:function(F){this.set("data",F.data);if(F.data.error){return;}var E=F.data,B=this.get("type"),H=A.Node.create("

    "+this.get("header")+"

    "),D=A.Node.create("
      "),G=this.get("node");if(B!="random"){if(B=="user"){H=A.Node.create('

      '+E.userinfo.fullname+"'s Gallery Items

      ");}A.each(E.modules,function(I){D.append('
    • '+I.title+"
    • ");});}else{D.append('
    • '+E.modules.title+"
    • ");}G.set("innerHTML","");G.append(H);G.append(D);},getSQL:function(){var B=this.get("type"),D;if(B=="user"){D="select * from yui.gallery.user where (username = '"+this.get("username")+"')";}else{D="select * from yui.gallery."+B;}return D;},updateUserUI:function(B){if(B.newVal!==""){this.updateUI();}},updateUI:function(D){var E=this.getSQL(),B;B=new A.yql(E,A.bind(function(F){if(F.query&&F.query.results&&F.query.results.json){this.fire("dataReturned",{data:F.query.results.json});}},this),{env:"http:/"+"/yuilibrary.com/yql/yui.env"});},initializer:function(){this.after("typeChange",A.bind(this.updateUI,this));this.after("usernameChange",A.bind(this.updateUserUI,this));this.publish("dataReturned",{type:"datareturned",defaultFn:this.buildUI});if(this.get("render")){this.render();}},render:function(){this.updateUI();},refresh:function(){this.updateUI();},destructor:function(){this.get("node").set("innerHTML","");}});A.namespace("Plugin");A.Plugin.Badge=C;},"gallery-2009.11.09-19",{requires:["node","event","gallery-yql","stylesheet"]}); \ No newline at end of file diff --git a/build/gallery-badge/gallery-badge.js b/build/gallery-badge/gallery-badge.js new file mode 100644 index 0000000000..0dcb1395e2 --- /dev/null +++ b/build/gallery-badge/gallery-badge.js @@ -0,0 +1,145 @@ +YUI.add('gallery-badge', function(Y) { + + + var B = function(config) { + this._lazyAddAttrs = false; + config.node = config.host; + B.superclass.constructor.call(this, config); + }; + + B.NAME = "gallery-badge"; + + B.NS = "badge"; + + B.ATTRS = { + node: { + setter: function(node) { + return Y.one(node); + } + }, + type: { + value: 'popular', + setter: function(v) { + if (v !== 'user') { + this.set('username', ''); + } + return v; + } + }, + header: { + value: '', + getter: function() { + var h = 'Popular Gallery Items'; + switch (this.get('type')) { + case 'oncdn': + h = 'Gallery Items on the CDN'; + break; + case 'featured': + h = 'Featured Gallery Items'; + break; + case 'random': + h = 'Random Gallery Item'; + break; + case 'user': + h = 'My Gallery Items'; + break; + } + return h; + } + }, + username: { + value: '', + setter: function(v) { + if (v) { + this.set('header', 'My Gallery Items'); + this._setStateVal('type', 'user'); + } + return v; + } + }, + render: { + value: true + }, + data: { + value: {} + } + }; + + Y.extend(B, Y.Base, { + buildUI: function(e) { + this.set('data', e.data); + if (e.data.error) { + return; + } + var data = e.data, + type = this.get('type'), + header = Y.Node.create('

      ' + this.get('header') + '

      '), + node = Y.Node.create('
        '), + n = this.get('node'); + + if (type != 'random') { + if (type == 'user') { + header = Y.Node.create('

        ' + data.userinfo.fullname + '\'s Gallery Items

        '); + } + Y.each(data.modules, function(v) { + node.append('
      • ' + v.title + '
      • '); + }); + } else { + node.append('
      • ' + data.modules.title + '
      • '); + } + + n.set('innerHTML', ''); + n.append(header); + n.append(node); + }, + getSQL: function() { + var type = this.get('type'), sql; + if (type == 'user') { + sql = "select * from yui.gallery.user where (username = '" + this.get('username') + "')"; + } else { + sql = "select * from yui.gallery." + type; + } + return sql; + }, + updateUserUI: function(e) { + if (e.newVal !== '') { + this.updateUI(); + } + }, + updateUI: function(e) { + var sql = this.getSQL(), q; + + q = new Y.yql(sql, Y.bind(function(r) { + if (r.query && r.query.results && r.query.results.json) { + this.fire('dataReturned', { data: r.query.results.json }); + } + }, this), { env: 'http:/'+'/yuilibrary.com/yql/yui.env' }); + + }, + initializer: function() { + this.after('typeChange', Y.bind(this.updateUI, this)); + this.after('usernameChange', Y.bind(this.updateUserUI, this)); + this.publish('dataReturned', { + type: 'datareturned', + defaultFn: this.buildUI + }); + if (this.get('render')) { + this.render(); + } + }, + render: function() { + this.updateUI(); + }, + refresh: function() { + this.updateUI(); + }, + destructor: function() { + this.get('node').set('innerHTML', ''); + } + }); + Y.namespace('Plugin'); + Y.Plugin.Badge = B; + + + +}, 'gallery-2009.11.09-19' ,{requires:['node','event','gallery-yql','stylesheet']}); diff --git a/build/gallery-beforeunload/gallery-beforeunload-debug.js b/build/gallery-beforeunload/gallery-beforeunload-debug.js new file mode 100644 index 0000000000..1a71e7d851 --- /dev/null +++ b/build/gallery-beforeunload/gallery-beforeunload-debug.js @@ -0,0 +1,63 @@ +YUI.add('gallery-beforeunload', function(Y) { + +/** + * DOM0 beforeunload event listener support. + * @module gallery-beforeunload + */ + +var INTERNAL_EVENT_NAME = 'gallery-dom0beforeunload', + supplantedHandler = window.onbeforeunload; + +window.onbeforeunload = function(ev) { + var e = ev || window.event; + if (supplantedHandler) { + supplantedHandler(e); + } + var facade = new Y.DOMEventFacade(e), retVal; + Y.fire(INTERNAL_EVENT_NAME, facade); + retVal = facade.returnValue; + e.returnValue = retVal; + return retVal; +}; + +/** + *

        + * The beforeunload event is not standard, yet it is useful enough that + * most browsers support it to some degree. But they are not consistent + * about how it operates. This module supplants any existing DOM0 + * onbeforelistener because DOM2 style listeners won't work across + * the A grade at this time. + *

        + * + *

        + * You can attempt to prevent the user from leaving the page by calling + * preventDefault on the event object. The user will be presented with + * a dialog to see whether or not they want to allow this. You can provide + * a message to preventDefault, and this will override the default message + * in the dialog if it is provided. + *

        + * + * + * Y.on('beforeunload', function(e) { + * &nbws;&nbws;e.returnValue = "Please don't go."; + * }); + * + * + * @event beforeunload + * @for YUI + * @param type {string} 'beforeunload' + * @param fn {function} the callback function to execute. + * @param context optional argument that specifies what 'this' refers to. + * @param args* 0..n additional arguments to pass on to the callback function. + * These arguments will be added after the event object. + */ +Y.Env.evt.plugins.beforeunload = { + on: function(type, fn) { + var a = Y.Array(arguments, 0, true); + a[0] = INTERNAL_EVENT_NAME; + return Y.on.apply(Y, a); + } +}; + + +}, 'gallery-2009.11.02-20' ,{requires:['event-base']}); diff --git a/build/gallery-beforeunload/gallery-beforeunload-min.js b/build/gallery-beforeunload/gallery-beforeunload-min.js new file mode 100644 index 0000000000..7cac896151 --- /dev/null +++ b/build/gallery-beforeunload/gallery-beforeunload-min.js @@ -0,0 +1 @@ +YUI.add("gallery-beforeunload",function(C){var B="gallery-dom0beforeunload",A=window.onbeforeunload;window.onbeforeunload=function(E){var G=E||window.event;if(A){A(G);}var D=new C.DOMEventFacade(G),F;C.fire(B,D);F=D.returnValue;G.returnValue=F;return F;};C.Env.evt.plugins.beforeunload={on:function(F,E){var D=C.Array(arguments,0,true);D[0]=B;return C.on.apply(C,D);}};},"gallery-2009.11.02-20",{requires:["event-base"]}); \ No newline at end of file diff --git a/build/gallery-beforeunload/gallery-beforeunload.js b/build/gallery-beforeunload/gallery-beforeunload.js new file mode 100644 index 0000000000..1a71e7d851 --- /dev/null +++ b/build/gallery-beforeunload/gallery-beforeunload.js @@ -0,0 +1,63 @@ +YUI.add('gallery-beforeunload', function(Y) { + +/** + * DOM0 beforeunload event listener support. + * @module gallery-beforeunload + */ + +var INTERNAL_EVENT_NAME = 'gallery-dom0beforeunload', + supplantedHandler = window.onbeforeunload; + +window.onbeforeunload = function(ev) { + var e = ev || window.event; + if (supplantedHandler) { + supplantedHandler(e); + } + var facade = new Y.DOMEventFacade(e), retVal; + Y.fire(INTERNAL_EVENT_NAME, facade); + retVal = facade.returnValue; + e.returnValue = retVal; + return retVal; +}; + +/** + *

        + * The beforeunload event is not standard, yet it is useful enough that + * most browsers support it to some degree. But they are not consistent + * about how it operates. This module supplants any existing DOM0 + * onbeforelistener because DOM2 style listeners won't work across + * the A grade at this time. + *

        + * + *

        + * You can attempt to prevent the user from leaving the page by calling + * preventDefault on the event object. The user will be presented with + * a dialog to see whether or not they want to allow this. You can provide + * a message to preventDefault, and this will override the default message + * in the dialog if it is provided. + *

        + * + * + * Y.on('beforeunload', function(e) { + * &nbws;&nbws;e.returnValue = "Please don't go."; + * }); + * + * + * @event beforeunload + * @for YUI + * @param type {string} 'beforeunload' + * @param fn {function} the callback function to execute. + * @param context optional argument that specifies what 'this' refers to. + * @param args* 0..n additional arguments to pass on to the callback function. + * These arguments will be added after the event object. + */ +Y.Env.evt.plugins.beforeunload = { + on: function(type, fn) { + var a = Y.Array(arguments, 0, true); + a[0] = INTERNAL_EVENT_NAME; + return Y.on.apply(Y, a); + } +}; + + +}, 'gallery-2009.11.02-20' ,{requires:['event-base']}); diff --git a/build/gallery-bitly/gallery-bitly-debug.js b/build/gallery-bitly/gallery-bitly-debug.js new file mode 100644 index 0000000000..8c5f4a4543 --- /dev/null +++ b/build/gallery-bitly/gallery-bitly-debug.js @@ -0,0 +1,108 @@ +YUI.add('gallery-bitly', function(Y) { + + + + var B = function(config) { + B.superclass.constructor.call(this, config); + }; + + B.NAME = 'bitly'; + + B.ATTRS = { + username: { + value: '' + }, + key: { + value: '' + } + }; + + Y.extend(B, Y.Base, { + api: 'http:/'+'/api.bit.ly/', + initializer: function() { + if (!this.get('key') || !this.get('username')) { + Y.error('You must give a username and an API key. If you do not have an apiKey, sign up for a bitly account and go to your Account page to get your apiKey. (http:/'+'/bit.ly/)'); + } + }, + destructor: function() { + }, + _buildURL: function(path, param) { + return this.api + path + '?version=2.0.1&login=' + this.get('username') + '&apiKey=' + this.get('key') + '&' + param; + }, + _handleAPI: function(name, url, cb) { + Y.log('handleAPI: ' + name + ' : ' + url, 'info'); + + var stamp = Y.guid().replace(/-/g,'_'); + + YUI[stamp] = Y.bind(function(e) { + if (e.results) { + if (name == 'stats') { + this.fire(name, e.results); + if (cb) { + cb = Y.bind(cb, this); + cb(e.results); + } + } else { + Y.each(e.results, function(v) { + this.fire(name, v); + if (cb) { + cb = Y.bind(cb, this); + cb(v); + } + }, this); + } + } + delete YUI[stamp]; + }, this); + + Y.Get.script(url + '&callback=YUI.' + stamp); + }, + shorten: function(url, cb) { + var api = this._buildURL('shorten', 'longUrl=' + encodeURIComponent(url)); + this._handleAPI('shorten', api, cb); + }, + expand: function(p, cb) { + var path = ((p.url) ? 'shortUrl=' + encodeURIComponent(p.url) : 'hash=' + p.hash), + api = this._buildURL('expand', path); + + this._handleAPI('expand', api, cb); + + }, + expandByURL: function(v, cb) { + return this.expand({ url: v }, cb); + }, + expandByHash: function(v, cb) { + return this.expand({ hash: v }, cb); + }, + info: function(p, cb) { + var path = ((p.url) ? 'shortUrl=' + encodeURIComponent(p.url) : 'hash=' + p.hash), + api = this._buildURL('info', path); + + this._handleAPI('info', api, cb); + + }, + infoByURL: function(v, cb) { + return this.info({ url: v }, cb); + }, + infoByHash: function(v, cb) { + return this.info({ hash: v }, cb); + }, + stats: function(p, cb) { + var path = ((p.url) ? 'shortUrl=' + encodeURIComponent(p.url) : 'hash=' + p.hash), + api = this._buildURL('stats', path); + + this._handleAPI('stats', api, cb); + + }, + statsByURL: function(v, cb) { + return this.stats({ url: v }, cb); + }, + statsByHash: function(v, cb) { + return this.stats({ hash: v }, cb); + } + }); + + Y.bitly = B; + + +}, 'gallery-2009.11.09-19' ,{requires:['base','get']}); diff --git a/build/gallery-bitly/gallery-bitly-min.js b/build/gallery-bitly/gallery-bitly-min.js new file mode 100644 index 0000000000..765d647a02 --- /dev/null +++ b/build/gallery-bitly/gallery-bitly-min.js @@ -0,0 +1 @@ +YUI.add("gallery-bitly",function(A){var C=function(B){C.superclass.constructor.call(this,B);};C.NAME="bitly";C.ATTRS={username:{value:""},key:{value:""}};A.extend(C,A.Base,{api:"http:/"+"/api.bit.ly/",initializer:function(){if(!this.get("key")||!this.get("username")){A.error("You must give a username and an API key. If you do not have an apiKey, sign up for a bitly account and go to your Account page to get your apiKey. (http:/"+"/bit.ly/)");}},destructor:function(){},_buildURL:function(B,D){return this.api+B+"?version=2.0.1&login="+this.get("username")+"&apiKey="+this.get("key")+"&"+D;},_handleAPI:function(E,D,B){var F=A.guid().replace(/-/g,"_");YUI[F]=A.bind(function(G){if(G.results){if(E=="stats"){this.fire(E,G.results);if(B){B=A.bind(B,this);B(G.results);}}else{A.each(G.results,function(H){this.fire(E,H);if(B){B=A.bind(B,this);B(H);}},this);}}delete YUI[F];},this);A.Get.script(D+"&callback=YUI."+F);},shorten:function(D,B){var E=this._buildURL("shorten","longUrl="+encodeURIComponent(D));this._handleAPI("shorten",E,B);},expand:function(F,B){var E=((F.url)?"shortUrl="+encodeURIComponent(F.url):"hash="+F.hash),D=this._buildURL("expand",E);this._handleAPI("expand",D,B);},expandByURL:function(D,B){return this.expand({url:D},B);},expandByHash:function(D,B){return this.expand({hash:D},B);},info:function(F,B){var E=((F.url)?"shortUrl="+encodeURIComponent(F.url):"hash="+F.hash),D=this._buildURL("info",E);this._handleAPI("info",D,B);},infoByURL:function(D,B){return this.info({url:D},B);},infoByHash:function(D,B){return this.info({hash:D},B);},stats:function(F,B){var E=((F.url)?"shortUrl="+encodeURIComponent(F.url):"hash="+F.hash),D=this._buildURL("stats",E);this._handleAPI("stats",D,B);},statsByURL:function(D,B){return this.stats({url:D},B);},statsByHash:function(D,B){return this.stats({hash:D},B);}});A.bitly=C;},"gallery-2009.11.09-19",{requires:["base","get"]}); \ No newline at end of file diff --git a/build/gallery-bitly/gallery-bitly.js b/build/gallery-bitly/gallery-bitly.js new file mode 100644 index 0000000000..4210902d13 --- /dev/null +++ b/build/gallery-bitly/gallery-bitly.js @@ -0,0 +1,107 @@ +YUI.add('gallery-bitly', function(Y) { + + + + var B = function(config) { + B.superclass.constructor.call(this, config); + }; + + B.NAME = 'bitly'; + + B.ATTRS = { + username: { + value: '' + }, + key: { + value: '' + } + }; + + Y.extend(B, Y.Base, { + api: 'http:/'+'/api.bit.ly/', + initializer: function() { + if (!this.get('key') || !this.get('username')) { + Y.error('You must give a username and an API key. If you do not have an apiKey, sign up for a bitly account and go to your Account page to get your apiKey. (http:/'+'/bit.ly/)'); + } + }, + destructor: function() { + }, + _buildURL: function(path, param) { + return this.api + path + '?version=2.0.1&login=' + this.get('username') + '&apiKey=' + this.get('key') + '&' + param; + }, + _handleAPI: function(name, url, cb) { + + var stamp = Y.guid().replace(/-/g,'_'); + + YUI[stamp] = Y.bind(function(e) { + if (e.results) { + if (name == 'stats') { + this.fire(name, e.results); + if (cb) { + cb = Y.bind(cb, this); + cb(e.results); + } + } else { + Y.each(e.results, function(v) { + this.fire(name, v); + if (cb) { + cb = Y.bind(cb, this); + cb(v); + } + }, this); + } + } + delete YUI[stamp]; + }, this); + + Y.Get.script(url + '&callback=YUI.' + stamp); + }, + shorten: function(url, cb) { + var api = this._buildURL('shorten', 'longUrl=' + encodeURIComponent(url)); + this._handleAPI('shorten', api, cb); + }, + expand: function(p, cb) { + var path = ((p.url) ? 'shortUrl=' + encodeURIComponent(p.url) : 'hash=' + p.hash), + api = this._buildURL('expand', path); + + this._handleAPI('expand', api, cb); + + }, + expandByURL: function(v, cb) { + return this.expand({ url: v }, cb); + }, + expandByHash: function(v, cb) { + return this.expand({ hash: v }, cb); + }, + info: function(p, cb) { + var path = ((p.url) ? 'shortUrl=' + encodeURIComponent(p.url) : 'hash=' + p.hash), + api = this._buildURL('info', path); + + this._handleAPI('info', api, cb); + + }, + infoByURL: function(v, cb) { + return this.info({ url: v }, cb); + }, + infoByHash: function(v, cb) { + return this.info({ hash: v }, cb); + }, + stats: function(p, cb) { + var path = ((p.url) ? 'shortUrl=' + encodeURIComponent(p.url) : 'hash=' + p.hash), + api = this._buildURL('stats', path); + + this._handleAPI('stats', api, cb); + + }, + statsByURL: function(v, cb) { + return this.stats({ url: v }, cb); + }, + statsByHash: function(v, cb) { + return this.stats({ hash: v }, cb); + } + }); + + Y.bitly = B; + + +}, 'gallery-2009.11.09-19' ,{requires:['base','get']}); diff --git a/build/gallery-chromahash/gallery-chromahash-debug.js b/build/gallery-chromahash/gallery-chromahash-debug.js new file mode 100644 index 0000000000..fbc2561d25 --- /dev/null +++ b/build/gallery-chromahash/gallery-chromahash-debug.js @@ -0,0 +1,104 @@ +YUI.add('gallery-chromahash', function(Y) { + +/* + * Chroma-Hash : A sexy, non-reversable live visualization of password field input + * Idea based on jQuery module by Mattt Thompson (http://github.com/mattt/Chroma-Hash/) + */ + +var LBL_TMPL = '', + _C = function(conf) { + _C.superclass.constructor.apply(this, arguments); + this._animations = []; + }; +_C.NAME = "ChromaHash"; +_C.ATTRS = + { + bars: { + value: 3, + validator: function(b) { + if (b < 1 || b > 4) { + return false; + } + } + }, + salt: { + value: "7be82b35cb0199120eea35a4507c9acf" + }, + minimum: { + value: 6 + }, + node: { + setter: function(node) { + var n = Y.get(node); + if (!n && !n.test('input[type=password]')) { + Y.fail('ChromaHash: Invalid node provided: ' + node); + } + return n; + } + } + }; +Y.extend(_C, Y.Widget, { + renderUI: function() { + var colors = ["primary", "secondary", "tertiary", "quaternary"].slice(0, this.get('bars')), + c = this.get('contentBox'), n = this.get('node'), i, lbl, width = n.get('clientWidth'), + height = n.get('clientHeight'), position = n.getXY(); + // Preferably, I'd be able to set the position on the boudningBox, but for now, this will function. + for (i = 0 ; i < colors.length ; i += 1) { + lbl = Y.Node.create(LBL_TMPL.replace(/\{id\}/g, n.get('id')).replace(/\{color\}/g, colors[i])); + lbl.setStyles({ + position: 'absolute', + height: height + "px", + width: "8px", + margin: "5px", + marginLeft: "0px", + backgroundColor: this.get('node').getStyle('backgroundColor') + }); + c.insert(lbl); + lbl.setXY([position[0] + width - 2 + (-8 * (i + 1)), position[1] + 3]); + this._animations.push(new Y.Anim({node: lbl, duration: 0.5})); + } + }, + bindUI: function() { + this._keyHandler = this.get('node').on('keyup', this._handleKey, this); + }, + hide: function() { + this.get('boundingBox').setStyle('display','none'); + }, + clear: function() { + this.hide(); + this.get('boundingBox').set('innerHTML',''); + }, + destroy: function() { + this._keyHandler.detach(); + this._clear(); + }, + _handleKey: function(e) { + var n = this.get('node'), value = n.get('value'), i, + col = [], bars = this.get('bars'), md5, g; + if(value === "" ) { + for( i = 0 ; i < bars ; i += 1) { col.push('fff'); } + } else { + md5 = Y.Crypto.MD5('' + value + ':' + this.get('salt')); + col = md5.match(/([\dABCDEF]{6})/ig); + if (value.length < this.get('minimum')) { + for (i = 0; i < bars ; i += 1) { + g = (parseInt(col[i], 0x10) % 0xF).toString(0x10); + col[i] = g + g + g; + } + } + } + + Y.Array.each(this._animations, + function(a, index) { + var c = a.get('node'); + a.stop(); + a.set('from', {backgroundColor: c.getStyle('backgroundColor')}); + a.set('to', {backgroundColor: '#' + col[index] }); + a.run(); + }); + } +}); +Y.ChromaHash = _C; + + +}, 'gallery-2009.11.09-19' ,{requires:['widget', 'stylesheet', 'collection', 'anim-color', 'gallery-crypto-md5']}); diff --git a/build/gallery-chromahash/gallery-chromahash-min.js b/build/gallery-chromahash/gallery-chromahash-min.js new file mode 100644 index 0000000000..499fe55621 --- /dev/null +++ b/build/gallery-chromahash/gallery-chromahash-min.js @@ -0,0 +1 @@ +YUI.add("gallery-chromahash",function(C){var B='',A=function(D){A.superclass.constructor.apply(this,arguments);this._animations=[];};A.NAME="ChromaHash";A.ATTRS={bars:{value:3,validator:function(D){if(D<1||D>4){return false;}}},salt:{value:"7be82b35cb0199120eea35a4507c9acf"},minimum:{value:6},node:{setter:function(D){var E=C.get(D);if(!E&&!E.test("input[type=password]")){C.fail("ChromaHash: Invalid node provided: "+D);}return E;}}};C.extend(A,C.Widget,{renderUI:function(){var F=["primary","secondary","tertiary","quaternary"].slice(0,this.get("bars")),K=this.get("contentBox"),J=this.get("node"),G,I,H=J.get("clientWidth"),E=J.get("clientHeight"),D=J.getXY();for(G=0;G 4) { + return false; + } + } + }, + salt: { + value: "7be82b35cb0199120eea35a4507c9acf" + }, + minimum: { + value: 6 + }, + node: { + setter: function(node) { + var n = Y.get(node); + if (!n && !n.test('input[type=password]')) { + Y.fail('ChromaHash: Invalid node provided: ' + node); + } + return n; + } + } + }; +Y.extend(_C, Y.Widget, { + renderUI: function() { + var colors = ["primary", "secondary", "tertiary", "quaternary"].slice(0, this.get('bars')), + c = this.get('contentBox'), n = this.get('node'), i, lbl, width = n.get('clientWidth'), + height = n.get('clientHeight'), position = n.getXY(); + // Preferably, I'd be able to set the position on the boudningBox, but for now, this will function. + for (i = 0 ; i < colors.length ; i += 1) { + lbl = Y.Node.create(LBL_TMPL.replace(/\{id\}/g, n.get('id')).replace(/\{color\}/g, colors[i])); + lbl.setStyles({ + position: 'absolute', + height: height + "px", + width: "8px", + margin: "5px", + marginLeft: "0px", + backgroundColor: this.get('node').getStyle('backgroundColor') + }); + c.insert(lbl); + lbl.setXY([position[0] + width - 2 + (-8 * (i + 1)), position[1] + 3]); + this._animations.push(new Y.Anim({node: lbl, duration: 0.5})); + } + }, + bindUI: function() { + this._keyHandler = this.get('node').on('keyup', this._handleKey, this); + }, + hide: function() { + this.get('boundingBox').setStyle('display','none'); + }, + clear: function() { + this.hide(); + this.get('boundingBox').set('innerHTML',''); + }, + destroy: function() { + this._keyHandler.detach(); + this._clear(); + }, + _handleKey: function(e) { + var n = this.get('node'), value = n.get('value'), i, + col = [], bars = this.get('bars'), md5, g; + if(value === "" ) { + for( i = 0 ; i < bars ; i += 1) { col.push('fff'); } + } else { + md5 = Y.Crypto.MD5('' + value + ':' + this.get('salt')); + col = md5.match(/([\dABCDEF]{6})/ig); + if (value.length < this.get('minimum')) { + for (i = 0; i < bars ; i += 1) { + g = (parseInt(col[i], 0x10) % 0xF).toString(0x10); + col[i] = g + g + g; + } + } + } + + Y.Array.each(this._animations, + function(a, index) { + var c = a.get('node'); + a.stop(); + a.set('from', {backgroundColor: c.getStyle('backgroundColor')}); + a.set('to', {backgroundColor: '#' + col[index] }); + a.run(); + }); + } +}); +Y.ChromaHash = _C; + + +}, 'gallery-2009.11.09-19' ,{requires:['widget', 'stylesheet', 'collection', 'anim-color', 'gallery-crypto-md5']}); diff --git a/build/gallery-crypto/gallery-crypto-md5-debug.js b/build/gallery-crypto/gallery-crypto-md5-debug.js new file mode 100644 index 0000000000..81535f040c --- /dev/null +++ b/build/gallery-crypto/gallery-crypto-md5-debug.js @@ -0,0 +1,227 @@ +YUI.add('gallery-crypto-md5', function(Y) { + +var _C = Y.Crypto || { }; + +Y.mix(_C, { + /** + * Adds two numerics as if they were 32-bit integers. + * + * @method add32Bit + * + * @param {int} first operand + * @param {int} second operand + * + * @static + */ + add32Bit: function (x,y) { + return (x + y) & 0xffffffff; + }, + /** + * Converts a standard JavaScript string (utf16) to a UTF8 string + * + * @method utf16ToUtf8 + * + * @param {string} The utf16 encoded string to convert to a utf8 array + * + * @static + */ + utf16ToUtf8: function(string) { + var output = "", cd, pr, i = 0; + + while (i < string.length) { + cd = string.charCodeAt(i); + pr = i + 1 < string.length ? string.charCodeAt(i + 1) : 0; + + if (0xd800 <= cd && cd <= 0xdbff && 0xdc00 <= pr && pr <= 0xdfff) { + // Surrogate Pair + cd = 0x10000 + ((cd & 0x3ff) + (pr & 0x03ff)); + i += 1; + } + + if (cd <= 0x007f) { + output += String.fromCharCode(cd); + } else if (cd <= 0x07ff) { + output += String.fromCharCode(0xc0 | ((cd >>> 6) & 15), + 0x80 | (cd & 63)); + } else if (cd <= 0xffff) { + output += String.fromCharCode(0xe0 | ((cd >>> 12) & 15 ), + 0x80 | ((cd >>> 6) & 63), + 0x80 | (cd & 63)); + } else if (cd <= 0x1fffff) { + output += String.fromCharCode(0xf0 | ((cd >>> 18) & 15), + 0x80 | ((cd >>> 12) & 63), + 0x80 | ((cd >>> 6) & 63), + 0x80 | (cd & 63)); + } + i += 1; + } + return output; + }, + /** + * Converts a utf8 encoded string to a byte array. It's important to use a + * utf8 encoded string, because otherwise characters with individual bytes + * greater than 255 will be silently dropped. + * + * @method utf8ToByteArray + * + * @param {string} The utf8 encoded string to convert to a byte array + * + * @static + */ + utf8ToByteArray: function(string) { + var output = Array(string.length >> 2), i, j; + for (i = 0 ; i < output.length ; i += 1) { output[i] = 0; } + for (i = 0 ; i < string.length ; i += 1) { + j = i * 8; + output[j >> 5] |= (string.charCodeAt(i) & 0xff) << (j % 32); + } + return output; + }, + byteArrayToString: function(array) { + var output = "", i, code; + for (i = 0 ; i < array.length * 32 ; i += 8) { + code = (array[i >> 5] >>> (i % 32)); + output += String.fromCharCode(code & 0xff); + } + return output; + }, + utf8ToHex: function(string) { + var output = "", i, cd, chars = "0123456789abcdef"; + for (i = 0 ; i < string.length ; i += 1) { + cd = string.charCodeAt(i); + output += chars.charAt(( cd >>> 4) & 0x0f) + + chars.charAt( cd & 0x0f); + } + return output; + }, + /** + * Converts a standard JavaScript string (utf16) to a Byte Array + * + * @method stringToByteArray + * + * @param {string} The utf16 encoded string to convert to a byte array + * + * @static + */ + stringToByteArray: function(string) { + return _C.utf8ToByteArray(_C.utf16ToUtf8(string)); + } +}); + +Y.Crypto = _C; + +_C = Y.Crypto; + +_C.MD5 = function (msg) { + var rotate_left = function(x, n) { + return ((x) << (n)) | (x >>> (32-n)); + }, + transform_common = function (v, a, b, x, s, ac) { + var t = _C.add32Bit(_C.add32Bit(_C.add32Bit(a, v), x || 0), ac); + return _C.add32Bit(rotate_left(t, s), b); + }, + FF = function(a, b, c, d, x, s, ac) { + return transform_common(((b & c) | ((~b) & d)), a, b, x, s, ac); + }, + GG = function(a, b, c, d, x, s, ac) { + return transform_common(((b & d) | (c & (~d))), a, b, x, s, ac); + }, + HH = function(a, b, c, d, x, s, ac) { + return transform_common((b ^ c ^ d), a, b, x, s, ac); + }, + II = function(a, b, c, d, x, s, ac) { + return transform_common((c ^ (b | (~d))), a, b, x, s, ac); + }, + data = _C.stringToByteArray(msg), + len = msg.length * 8, + a = 0x67452301, + b = 0xefcdab89, + c = 0x98badcfe, + d = 0x10325476, i, s1, s2, s3, s4; + data[len >> 5] |= 0x80 << ((len) % 32); + data[(((len + 64) >>> 9) << 4) + 14] = len; + for ( i = 0 ; i < data.length ; i += 16) { + s1 = a; s2 = b; s3 = c; s4 = d; + + /* Round 1 */ + a = FF (a, b, c, d, data[i + 0], 7, 0xd76aa478); /* 1 */ + d = FF (d, a, b, c, data[i + 1], 12, 0xe8c7b756); /* 2 */ + c = FF (c, d, a, b, data[i + 2], 17, 0x242070db); /* 3 */ + b = FF (b, c, d, a, data[i + 3], 22, 0xc1bdceee); /* 4 */ + a = FF (a, b, c, d, data[i + 4], 7, 0xf57c0faf); /* 5 */ + d = FF (d, a, b, c, data[i + 5], 12, 0x4787c62a); /* 6 */ + c = FF (c, d, a, b, data[i + 6], 17, 0xa8304613); /* 7 */ + b = FF (b, c, d, a, data[i + 7], 22, 0xfd469501); /* 8 */ + a = FF (a, b, c, d, data[i + 8], 7, 0x698098d8); /* 9 */ + d = FF (d, a, b, c, data[i + 9], 12, 0x8b44f7af); /* 10 */ + c = FF (c, d, a, b, data[i +10], 17, 0xffff5bb1); /* 11 */ + b = FF (b, c, d, a, data[i +11], 22, 0x895cd7be); /* 12 */ + a = FF (a, b, c, d, data[i +12], 7, 0x6b901122); /* 13 */ + d = FF (d, a, b, c, data[i +13], 12, 0xfd987193); /* 14 */ + c = FF (c, d, a, b, data[i +14], 17, 0xa679438e); /* 15 */ + b = FF (b, c, d, a, data[i +15], 22, 0x49b40821); /* 16 */ + + /* Round 2 */ + a = GG (a, b, c, d, data[i + 1], 5, 0xf61e2562); /* 17 */ + d = GG (d, a, b, c, data[i + 6], 9, 0xc040b340); /* 18 */ + c = GG (c, d, a, b, data[i +11], 14, 0x265e5a51); /* 19 */ + b = GG (b, c, d, a, data[i + 0], 20, 0xe9b6c7aa); /* 20 */ + a = GG (a, b, c, d, data[i + 5], 5, 0xd62f105d); /* 21 */ + d = GG (d, a, b, c, data[i +10], 9, 0x2441453); /* 22 */ + c = GG (c, d, a, b, data[i +15], 14, 0xd8a1e681); /* 23 */ + b = GG (b, c, d, a, data[i + 4], 20, 0xe7d3fbc8); /* 24 */ + a = GG (a, b, c, d, data[i + 9], 5, 0x21e1cde6); /* 25 */ + d = GG (d, a, b, c, data[i +14], 9, 0xc33707d6); /* 26 */ + c = GG (c, d, a, b, data[i + 3], 14, 0xf4d50d87); /* 27 */ + b = GG (b, c, d, a, data[i + 8], 20, 0x455a14ed); /* 28 */ + a = GG (a, b, c, d, data[i +13], 5, 0xa9e3e905); /* 29 */ + d = GG (d, a, b, c, data[i + 2], 9, 0xfcefa3f8); /* 30 */ + c = GG (c, d, a, b, data[i + 7], 14, 0x676f02d9); /* 31 */ + b = GG (b, c, d, a, data[i +12], 20, 0x8d2a4c8a); /* 32 */ + + /* Round 3 */ + a = HH (a, b, c, d, data[i + 5], 4, 0xfffa3942); /* 33 */ + d = HH (d, a, b, c, data[i + 8], 11, 0x8771f681); /* 34 */ + c = HH (c, d, a, b, data[i +11], 16, 0x6d9d6122); /* 35 */ + b = HH (b, c, d, a, data[i +14], 23, 0xfde5380c); /* 36 */ + a = HH (a, b, c, d, data[i + 1], 4, 0xa4beea44); /* 37 */ + d = HH (d, a, b, c, data[i + 4], 11, 0x4bdecfa9); /* 38 */ + c = HH (c, d, a, b, data[i + 7], 16, 0xf6bb4b60); /* 39 */ + b = HH (b, c, d, a, data[i +10], 23, 0xbebfbc70); /* 40 */ + a = HH (a, b, c, d, data[i +13], 4, 0x289b7ec6); /* 41 */ + d = HH (d, a, b, c, data[i + 0], 11, 0xeaa127fa); /* 42 */ + c = HH (c, d, a, b, data[i + 3], 16, 0xd4ef3085); /* 43 */ + b = HH (b, c, d, a, data[i + 6], 23, 0x4881d05); /* 44 */ + a = HH (a, b, c, d, data[i + 9], 4, 0xd9d4d039); /* 45 */ + d = HH (d, a, b, c, data[i +12], 11, 0xe6db99e5); /* 46 */ + c = HH (c, d, a, b, data[i +15], 16, 0x1fa27cf8); /* 47 */ + b = HH (b, c, d, a, data[i + 2], 23, 0xc4ac5665); /* 48 */ + + /* Round 4 */ + a = II (a, b, c, d, data[i + 0], 6, 0xf4292244); /* 49 */ + d = II (d, a, b, c, data[i + 7], 10, 0x432aff97); /* 50 */ + c = II (c, d, a, b, data[i +14], 15, 0xab9423a7); /* 51 */ + b = II (b, c, d, a, data[i + 5], 21, 0xfc93a039); /* 52 */ + a = II (a, b, c, d, data[i +12], 6, 0x655b59c3); /* 53 */ + d = II (d, a, b, c, data[i + 3], 10, 0x8f0ccc92); /* 54 */ + c = II (c, d, a, b, data[i +10], 15, 0xffeff47d); /* 55 */ + b = II (b, c, d, a, data[i + 1], 21, 0x85845dd1); /* 56 */ + a = II (a, b, c, d, data[i + 8], 6, 0x6fa87e4f); /* 57 */ + d = II (d, a, b, c, data[i +15], 10, 0xfe2ce6e0); /* 58 */ + c = II (c, d, a, b, data[i + 6], 15, 0xa3014314); /* 59 */ + b = II (b, c, d, a, data[i +13], 21, 0x4e0811a1); /* 60 */ + a = II (a, b, c, d, data[i + 4], 6, 0xf7537e82); /* 61 */ + d = II (d, a, b, c, data[i +11], 10, 0xbd3af235); /* 62 */ + c = II (c, d, a, b, data[i + 2], 15, 0x2ad7d2bb); /* 63 */ + b = II (b, c, d, a, data[i + 9], 21, 0xeb86d391); /* 64 */ + + a = _C.add32Bit(a, s1); + b = _C.add32Bit(b, s2); + c = _C.add32Bit(c, s3); + d = _C.add32Bit(d, s4); + } + return _C.utf8ToHex(_C.byteArrayToString([a, b, c, d])); +}; + + +}, 'gallery-2009.11.19-20' ); diff --git a/build/gallery-crypto/gallery-crypto-md5-min.js b/build/gallery-crypto/gallery-crypto-md5-min.js new file mode 100644 index 0000000000..38212fa14e --- /dev/null +++ b/build/gallery-crypto/gallery-crypto-md5-min.js @@ -0,0 +1 @@ +YUI.add("gallery-crypto-md5",function(B){var A=B.Crypto||{};B.mix(A,{add32Bit:function(C,D){return(C+D)&4294967295;},utf16ToUtf8:function(D){var C="",F,G,E=0;while(E>>6)&15),128|(F&63));}else{if(F<=65535){C+=String.fromCharCode(224|((F>>>12)&15),128|((F>>>6)&63),128|(F&63));}else{if(F<=2097151){C+=String.fromCharCode(240|((F>>>18)&15),128|((F>>>12)&63),128|((F>>>6)&63),128|(F&63));}}}}E+=1;}return C;},utf8ToByteArray:function(E){var C=Array(E.length>>2),F,D;for(F=0;F>5]|=(E.charCodeAt(F)&255)<<(D%32);}return C;},byteArrayToString:function(F){var C="",D,E;for(D=0;D>5]>>>(D%32));C+=String.fromCharCode(E&255);}return C;},utf8ToHex:function(D){var C="",E,G,F="0123456789abcdef";for(E=0;E>>4)&15)+F.charAt(G&15);}return C;},stringToByteArray:function(C){return A.utf8ToByteArray(A.utf16ToUtf8(C));}});B.Crypto=A;A=B.Crypto;A.MD5=function(H){var G=function(U,V){return((U)<<(V))|(U>>>(32-V));},J=function(X,W,V,U,Z,c){var Y=A.add32Bit(A.add32Bit(A.add32Bit(W,X),U||0),c);return A.add32Bit(G(Y,Z),V);},K=function(W,V,e,Z,U,X,Y){return J(((V&e)|((~V)&Z)),W,V,U,X,Y);},L=function(W,V,e,Z,U,X,Y){return J(((V&Z)|(e&(~Z))),W,V,U,X,Y);},S=function(W,V,e,Z,U,X,Y){return J((V^e^Z),W,V,U,X,Y);},I=function(W,V,e,Z,U,X,Y){return J((e^(V|(~Z))),W,V,U,X,Y);},T=A.stringToByteArray(H),N=H.length*8,R=1732584193,Q=4023233417,P=2562383102,O=271733878,M,F,E,D,C;T[N>>5]|=128<<((N)%32);T[(((N+64)>>>9)<<4)+14]=N;for(M=0;M>> 6) & 15), + 0x80 | (cd & 63)); + } else if (cd <= 0xffff) { + output += String.fromCharCode(0xe0 | ((cd >>> 12) & 15 ), + 0x80 | ((cd >>> 6) & 63), + 0x80 | (cd & 63)); + } else if (cd <= 0x1fffff) { + output += String.fromCharCode(0xf0 | ((cd >>> 18) & 15), + 0x80 | ((cd >>> 12) & 63), + 0x80 | ((cd >>> 6) & 63), + 0x80 | (cd & 63)); + } + i += 1; + } + return output; + }, + /** + * Converts a utf8 encoded string to a byte array. It's important to use a + * utf8 encoded string, because otherwise characters with individual bytes + * greater than 255 will be silently dropped. + * + * @method utf8ToByteArray + * + * @param {string} The utf8 encoded string to convert to a byte array + * + * @static + */ + utf8ToByteArray: function(string) { + var output = Array(string.length >> 2), i, j; + for (i = 0 ; i < output.length ; i += 1) { output[i] = 0; } + for (i = 0 ; i < string.length ; i += 1) { + j = i * 8; + output[j >> 5] |= (string.charCodeAt(i) & 0xff) << (j % 32); + } + return output; + }, + byteArrayToString: function(array) { + var output = "", i, code; + for (i = 0 ; i < array.length * 32 ; i += 8) { + code = (array[i >> 5] >>> (i % 32)); + output += String.fromCharCode(code & 0xff); + } + return output; + }, + utf8ToHex: function(string) { + var output = "", i, cd, chars = "0123456789abcdef"; + for (i = 0 ; i < string.length ; i += 1) { + cd = string.charCodeAt(i); + output += chars.charAt(( cd >>> 4) & 0x0f) + + chars.charAt( cd & 0x0f); + } + return output; + }, + /** + * Converts a standard JavaScript string (utf16) to a Byte Array + * + * @method stringToByteArray + * + * @param {string} The utf16 encoded string to convert to a byte array + * + * @static + */ + stringToByteArray: function(string) { + return _C.utf8ToByteArray(_C.utf16ToUtf8(string)); + } +}); + +Y.Crypto = _C; + +_C = Y.Crypto; + +_C.MD5 = function (msg) { + var rotate_left = function(x, n) { + return ((x) << (n)) | (x >>> (32-n)); + }, + transform_common = function (v, a, b, x, s, ac) { + var t = _C.add32Bit(_C.add32Bit(_C.add32Bit(a, v), x || 0), ac); + return _C.add32Bit(rotate_left(t, s), b); + }, + FF = function(a, b, c, d, x, s, ac) { + return transform_common(((b & c) | ((~b) & d)), a, b, x, s, ac); + }, + GG = function(a, b, c, d, x, s, ac) { + return transform_common(((b & d) | (c & (~d))), a, b, x, s, ac); + }, + HH = function(a, b, c, d, x, s, ac) { + return transform_common((b ^ c ^ d), a, b, x, s, ac); + }, + II = function(a, b, c, d, x, s, ac) { + return transform_common((c ^ (b | (~d))), a, b, x, s, ac); + }, + data = _C.stringToByteArray(msg), + len = msg.length * 8, + a = 0x67452301, + b = 0xefcdab89, + c = 0x98badcfe, + d = 0x10325476, i, s1, s2, s3, s4; + data[len >> 5] |= 0x80 << ((len) % 32); + data[(((len + 64) >>> 9) << 4) + 14] = len; + for ( i = 0 ; i < data.length ; i += 16) { + s1 = a; s2 = b; s3 = c; s4 = d; + + /* Round 1 */ + a = FF (a, b, c, d, data[i + 0], 7, 0xd76aa478); /* 1 */ + d = FF (d, a, b, c, data[i + 1], 12, 0xe8c7b756); /* 2 */ + c = FF (c, d, a, b, data[i + 2], 17, 0x242070db); /* 3 */ + b = FF (b, c, d, a, data[i + 3], 22, 0xc1bdceee); /* 4 */ + a = FF (a, b, c, d, data[i + 4], 7, 0xf57c0faf); /* 5 */ + d = FF (d, a, b, c, data[i + 5], 12, 0x4787c62a); /* 6 */ + c = FF (c, d, a, b, data[i + 6], 17, 0xa8304613); /* 7 */ + b = FF (b, c, d, a, data[i + 7], 22, 0xfd469501); /* 8 */ + a = FF (a, b, c, d, data[i + 8], 7, 0x698098d8); /* 9 */ + d = FF (d, a, b, c, data[i + 9], 12, 0x8b44f7af); /* 10 */ + c = FF (c, d, a, b, data[i +10], 17, 0xffff5bb1); /* 11 */ + b = FF (b, c, d, a, data[i +11], 22, 0x895cd7be); /* 12 */ + a = FF (a, b, c, d, data[i +12], 7, 0x6b901122); /* 13 */ + d = FF (d, a, b, c, data[i +13], 12, 0xfd987193); /* 14 */ + c = FF (c, d, a, b, data[i +14], 17, 0xa679438e); /* 15 */ + b = FF (b, c, d, a, data[i +15], 22, 0x49b40821); /* 16 */ + + /* Round 2 */ + a = GG (a, b, c, d, data[i + 1], 5, 0xf61e2562); /* 17 */ + d = GG (d, a, b, c, data[i + 6], 9, 0xc040b340); /* 18 */ + c = GG (c, d, a, b, data[i +11], 14, 0x265e5a51); /* 19 */ + b = GG (b, c, d, a, data[i + 0], 20, 0xe9b6c7aa); /* 20 */ + a = GG (a, b, c, d, data[i + 5], 5, 0xd62f105d); /* 21 */ + d = GG (d, a, b, c, data[i +10], 9, 0x2441453); /* 22 */ + c = GG (c, d, a, b, data[i +15], 14, 0xd8a1e681); /* 23 */ + b = GG (b, c, d, a, data[i + 4], 20, 0xe7d3fbc8); /* 24 */ + a = GG (a, b, c, d, data[i + 9], 5, 0x21e1cde6); /* 25 */ + d = GG (d, a, b, c, data[i +14], 9, 0xc33707d6); /* 26 */ + c = GG (c, d, a, b, data[i + 3], 14, 0xf4d50d87); /* 27 */ + b = GG (b, c, d, a, data[i + 8], 20, 0x455a14ed); /* 28 */ + a = GG (a, b, c, d, data[i +13], 5, 0xa9e3e905); /* 29 */ + d = GG (d, a, b, c, data[i + 2], 9, 0xfcefa3f8); /* 30 */ + c = GG (c, d, a, b, data[i + 7], 14, 0x676f02d9); /* 31 */ + b = GG (b, c, d, a, data[i +12], 20, 0x8d2a4c8a); /* 32 */ + + /* Round 3 */ + a = HH (a, b, c, d, data[i + 5], 4, 0xfffa3942); /* 33 */ + d = HH (d, a, b, c, data[i + 8], 11, 0x8771f681); /* 34 */ + c = HH (c, d, a, b, data[i +11], 16, 0x6d9d6122); /* 35 */ + b = HH (b, c, d, a, data[i +14], 23, 0xfde5380c); /* 36 */ + a = HH (a, b, c, d, data[i + 1], 4, 0xa4beea44); /* 37 */ + d = HH (d, a, b, c, data[i + 4], 11, 0x4bdecfa9); /* 38 */ + c = HH (c, d, a, b, data[i + 7], 16, 0xf6bb4b60); /* 39 */ + b = HH (b, c, d, a, data[i +10], 23, 0xbebfbc70); /* 40 */ + a = HH (a, b, c, d, data[i +13], 4, 0x289b7ec6); /* 41 */ + d = HH (d, a, b, c, data[i + 0], 11, 0xeaa127fa); /* 42 */ + c = HH (c, d, a, b, data[i + 3], 16, 0xd4ef3085); /* 43 */ + b = HH (b, c, d, a, data[i + 6], 23, 0x4881d05); /* 44 */ + a = HH (a, b, c, d, data[i + 9], 4, 0xd9d4d039); /* 45 */ + d = HH (d, a, b, c, data[i +12], 11, 0xe6db99e5); /* 46 */ + c = HH (c, d, a, b, data[i +15], 16, 0x1fa27cf8); /* 47 */ + b = HH (b, c, d, a, data[i + 2], 23, 0xc4ac5665); /* 48 */ + + /* Round 4 */ + a = II (a, b, c, d, data[i + 0], 6, 0xf4292244); /* 49 */ + d = II (d, a, b, c, data[i + 7], 10, 0x432aff97); /* 50 */ + c = II (c, d, a, b, data[i +14], 15, 0xab9423a7); /* 51 */ + b = II (b, c, d, a, data[i + 5], 21, 0xfc93a039); /* 52 */ + a = II (a, b, c, d, data[i +12], 6, 0x655b59c3); /* 53 */ + d = II (d, a, b, c, data[i + 3], 10, 0x8f0ccc92); /* 54 */ + c = II (c, d, a, b, data[i +10], 15, 0xffeff47d); /* 55 */ + b = II (b, c, d, a, data[i + 1], 21, 0x85845dd1); /* 56 */ + a = II (a, b, c, d, data[i + 8], 6, 0x6fa87e4f); /* 57 */ + d = II (d, a, b, c, data[i +15], 10, 0xfe2ce6e0); /* 58 */ + c = II (c, d, a, b, data[i + 6], 15, 0xa3014314); /* 59 */ + b = II (b, c, d, a, data[i +13], 21, 0x4e0811a1); /* 60 */ + a = II (a, b, c, d, data[i + 4], 6, 0xf7537e82); /* 61 */ + d = II (d, a, b, c, data[i +11], 10, 0xbd3af235); /* 62 */ + c = II (c, d, a, b, data[i + 2], 15, 0x2ad7d2bb); /* 63 */ + b = II (b, c, d, a, data[i + 9], 21, 0xeb86d391); /* 64 */ + + a = _C.add32Bit(a, s1); + b = _C.add32Bit(b, s2); + c = _C.add32Bit(c, s3); + d = _C.add32Bit(d, s4); + } + return _C.utf8ToHex(_C.byteArrayToString([a, b, c, d])); +}; + + +}, 'gallery-2009.11.19-20' ); diff --git a/build/gallery-event-drag/gallery-event-drag-debug.js b/build/gallery-event-drag/gallery-event-drag-debug.js new file mode 100644 index 0000000000..0f6b3b1e87 --- /dev/null +++ b/build/gallery-event-drag/gallery-event-drag-debug.js @@ -0,0 +1,289 @@ +YUI.add('gallery-event-drag', function(Y) { + +/** + *

        Provides subscribable drag events from Node or NodeLists. Subscribing + * to any of the events causes the node to be plugged with Y.Plugin.Drag. The + * config object passed can be used to set Drag instance attributes or add + * additional Plugins to the Drag instance such as Y.Plugin.DDProxy.

        + * + * Config properties are formatted and tested for a corresponding Y.Plugin.* as + * 'somePlugin' => Y.Plugin.DDSomePlugin if the property name doesn't already + * start with "DD". So { proxy: true } and { DDProxy: true } are functionally + * equivalent. Both add Y.Plugin.DDProxy to the Drag instance.

        + * + *
        node.on('drag:start', fn, { proxy: true, data: 123 });
        + * + *

        This adds Y.Plugin.DDProxy to the Drag instance and also set's the Drag instance's data attribute.

        + * + *

        Passing any value will result in the Plugin being added, but if you pass + * an object literal as the value, it will be sent to the Plugin's + * constructor.

        + * + *
        node.on('drag:end', fn, {
        + *     constrained: { constrain2node: '#container' }
        + * });
        + * + *

        This adds Y.Plugin.DDConstrained to the Drag instance using the specified + * configuration.

        + * + *

        A custom detach handle is returned, whose detach method unplugs the + * Y.Plugin.Drag from the node(s).

        + * + *

        Supported events are:

        + *
          + *
        • drag or drag:drag
        • + *
        • drag:start
        • + *
        • drag:end
        • + *
        • drag:mouseDown
        • + *
        • drag:afterMouseDown
        • + *
        • drag:removeHandle
        • + *
        • drag:addHandle
        • + *
        • drag:removeInvalid
        • + *
        • drag:addInvalid
        • + *
        • drag:align
        • + *
        + * + *

        Additionally, the default callback context is overridden to the + * subscribing Node unless otherwise specified during the subscription. + * So "this" in the callback will refer to the node. On the + * event object passed to subscribers, e.currentTarget is also the + * Node regardless of context override. The Drag instance is available from + * the Node as node.dd.

        + * + * @module event-drag + */ + +/** + * Also "drag:drag". Subscribes to the respective event on the generated Drag instance. + * + * @event drag + * @param type {String} 'drag' + * @param fn {Function} the callback function + * @param id {String|Node|etc} the element to bind (typically document) + * @param conf {Object} (optional) configuration to pass to Drag constructor + * @return {Event.Handle} the detach handle + */ +var eventPlugin = Y.Env.evt.plugins, + nodeEvents = Y.Node.DOM_EVENTS, + events = [ + 'drag', + 'drag:start', + 'drag:drag', + 'drag:end', + 'drag:mouseDown', + 'drag:afterMouseDown', + 'drag:removeHandle', + 'drag:addHandle', + 'drag:removeInvalid', + 'drag:addInvalid', + 'drag:align' + ], + eventDef, i; + +eventDef = { + on: function (type, fn, el, conf, ctx) { + var nodes = eventPlugin.drag._getNodes(el), + args = Y.Array(arguments, 4, true); + + args.unshift(fn); + + type = type.indexOf(':') > -1 ? type : 'drag:' + type; + + nodes.each(function (node) { + if (!node.dd) { + node.plug(Y.Plugin.Drag); + } + + eventPlugin.drag._applyConfig(node.dd, conf); + + args[1] = ctx || node; + + var callback = Y.rbind.apply(Y, args); + + node.dd.on(type, function (e) { + e.currentTarget = node; + callback(e); + }); + }); + + return { + detach: function () { + nodes.each(function (node) { + node.unplug(Y.Plugin.Drag); + }); + } + }; + }, + + /** + * Normalizes the third param of on() to a NodeList. The resulting list + * may be empty. + * + * @method _getNodes + * @param el {String|Node|NodeList|HTMLElement|Array} valid context for on() + * @return NodeList + * @protected + */ + _getNodes : function (el) { + el = el || []; + + if (el instanceof Y.NodeList) { + return el; + } else if (Y.Lang.isString(el) || Y.Event._isValidCollection(el)) { + return Y.all(el); + } else { + return Y.all(Y.Array(Y.Node.getDOMNode(el) || [])); + } + }, + + /** + * Applies the attribute values from the config object to the Drag instance. + * Also checks for loaded Plugins by the name of the property to apply. + * + * @method _applyConfig + * @param dd {Y.Plugin.Drag} the Drag plugin for the node + * @param conf {Object} the attribute configuration + * @protected + */ + _applyConfig : function (dd, conf) { + var k, plugin, pname, pconf; + + if (conf) { + for (k in conf) { + if (conf.hasOwnProperty(k)) { + pname = k; + if (k.indexOf('DD') !== 0) { + pname = 'DD' + k.charAt(0).toUpperCase() + k.slice(1); + } + pconf = Y.Lang.isObject(conf[k]) ? conf[k] : {}; + plugin = Y.Plugin[pname]; + + if (plugin) { + dd.plug(plugin, pconf); + } else { + dd.set(k, conf[k]); + } + } + } + } + } +}; + +// Add all the events, defined by the same algo. +for (i = events.length - 1; i >= 0; --i) { + eventPlugin[events[i]] = nodeEvents[events[i]] = eventDef; +} + + +/** + * Also "drag:drag". Subscribes to the respective event on the generated Drag instance. + * + * @event drag + * @param type {String} 'drag' + * @param fn {Function} the callback function + * @param id {String|Node|etc} the element to bind (typically document) + * @param conf {Object} (optional) configuration to pass to Drag constructor + * @return {Event.Handle} the detach handle + */ + +/** + * Subscribes to the respective event on the generated Drag instance. + * + * @event drag:start + * @param type {String} 'drag:start' + * @param fn {Function} the callback function + * @param id {String|Node|etc} the element to bind (typically document) + * @param conf {Object} (optional) configuration to pass to Drag constructor + * @return {Event.Handle} the detach handle + */ + +/** + * Subscribes to the respective event on the generated Drag instance. + * + * @event drag:end + * @param type {String} 'drag:end' + * @param fn {Function} the callback function + * @param id {String|Node|etc} the element to bind (typically document) + * @param conf {Object} (optional) configuration to pass to Drag constructor + * @return {Event.Handle} the detach handle + */ + +/** + * Subscribes to the respective event on the generated Drag instance. + * + * @event drag:mouseDown + * @param type {String} 'drag:mouseDown' + * @param fn {Function} the callback function + * @param id {String|Node|etc} the element to bind (typically document) + * @param conf {Object} (optional) configuration to pass to Drag constructor + * @return {Event.Handle} the detach handle + */ + +/** + * Subscribes to the respective event on the generated Drag instance. + * + * @event drag:afterMouseDown + * @param type {String} 'drag:afterMouseDown' + * @param fn {Function} the callback function + * @param id {String|Node|etc} the element to bind (typically document) + * @param conf {Object} (optional) configuration to pass to Drag constructor + * @return {Event.Handle} the detach handle + */ + +/** + * Subscribes to the respective event on the generated Drag instance. + * + * @event drag:removeHandle + * @param type {String} 'drag:removeHandle' + * @param fn {Function} the callback function + * @param id {String|Node|etc} the element to bind (typically document) + * @param conf {Object} (optional) configuration to pass to Drag constructor + * @return {Event.Handle} the detach handle + */ + +/** + * Subscribes to the respective event on the generated Drag instance. + * + * @event drag:addHandle + * @param type {String} 'drag:addHandle' + * @param fn {Function} the callback function + * @param id {String|Node|etc} the element to bind (typically document) + * @param conf {Object} (optional) configuration to pass to Drag constructor + * @return {Event.Handle} the detach handle + */ + +/** + * Subscribes to the respective event on the generated Drag instance. + * + * @event drag:removeInvalid + * @param type {String} 'drag:removeInvalid' + * @param fn {Function} the callback function + * @param id {String|Node|etc} the element to bind (typically document) + * @param conf {Object} (optional) configuration to pass to Drag constructor + * @return {Event.Handle} the detach handle + */ + +/** + * Subscribes to the respective event on the generated Drag instance. + * + * @event drag:addInvalid + * @param type {String} 'drag:addInvalid' + * @param fn {Function} the callback function + * @param id {String|Node|etc} the element to bind (typically document) + * @param conf {Object} (optional) configuration to pass to Drag constructor + * @return {Event.Handle} the detach handle + */ + +/** + * Subscribes to the respective event on the generated Drag instance. + * + * @event drag:align + * @param type {String} 'drag:align' + * @param fn {Function} the callback function + * @param id {String|Node|etc} the element to bind (typically document) + * @param conf {Object} (optional) configuration to pass to Drag constructor + * @return {Event.Handle} the detach handle + */ + + +}, 'gallery-2009.11.02-20' ,{optional:['dd-proxy','dd-constrain'], requires:['dd-plugin']}); diff --git a/build/gallery-event-drag/gallery-event-drag-min.js b/build/gallery-event-drag/gallery-event-drag-min.js new file mode 100644 index 0000000000..defd4b0acf --- /dev/null +++ b/build/gallery-event-drag/gallery-event-drag-min.js @@ -0,0 +1 @@ +YUI.add("gallery-event-drag",function(F){var E=F.Env.evt.plugins,A=F.Node.DOM_EVENTS,C=["drag","drag:start","drag:drag","drag:end","drag:mouseDown","drag:afterMouseDown","drag:removeHandle","drag:addHandle","drag:removeInvalid","drag:addInvalid","drag:align"],D,B;D={on:function(M,L,K,J,G){var H=E.drag._getNodes(K),I=F.Array(arguments,4,true);I.unshift(L);M=M.indexOf(":")>-1?M:"drag:"+M;H.each(function(N){if(!N.dd){N.plug(F.Plugin.Drag);}E.drag._applyConfig(N.dd,J);I[1]=G||N;var O=F.rbind.apply(F,I);N.dd.on(M,function(P){P.currentTarget=N;O(P);});});return{detach:function(){H.each(function(N){N.unplug(F.Plugin.Drag);});}};},_getNodes:function(G){G=G||[];if(G instanceof F.NodeList){return G;}else{if(F.Lang.isString(G)||F.Event._isValidCollection(G)){return F.all(G);}else{return F.all(F.Array(F.Node.getDOMNode(G)||[]));}}},_applyConfig:function(G,I){var H,L,K,J;if(I){for(H in I){if(I.hasOwnProperty(H)){K=H;if(H.indexOf("DD")!==0){K="DD"+H.charAt(0).toUpperCase()+H.slice(1);}J=F.Lang.isObject(I[H])?I[H]:{};L=F.Plugin[K];if(L){G.plug(L,J);}else{G.set(H,I[H]);}}}}}};for(B=C.length-1;B>=0;--B){E[C[B]]=A[C[B]]=D;}},"gallery-2009.11.02-20",{optional:["dd-proxy","dd-constrain"],requires:["dd-plugin"]}); \ No newline at end of file diff --git a/build/gallery-event-drag/gallery-event-drag.js b/build/gallery-event-drag/gallery-event-drag.js new file mode 100644 index 0000000000..0f6b3b1e87 --- /dev/null +++ b/build/gallery-event-drag/gallery-event-drag.js @@ -0,0 +1,289 @@ +YUI.add('gallery-event-drag', function(Y) { + +/** + *

        Provides subscribable drag events from Node or NodeLists. Subscribing + * to any of the events causes the node to be plugged with Y.Plugin.Drag. The + * config object passed can be used to set Drag instance attributes or add + * additional Plugins to the Drag instance such as Y.Plugin.DDProxy.

        + * + * Config properties are formatted and tested for a corresponding Y.Plugin.* as + * 'somePlugin' => Y.Plugin.DDSomePlugin if the property name doesn't already + * start with "DD". So { proxy: true } and { DDProxy: true } are functionally + * equivalent. Both add Y.Plugin.DDProxy to the Drag instance.

        + * + *
        node.on('drag:start', fn, { proxy: true, data: 123 });
        + * + *

        This adds Y.Plugin.DDProxy to the Drag instance and also set's the Drag instance's data attribute.

        + * + *

        Passing any value will result in the Plugin being added, but if you pass + * an object literal as the value, it will be sent to the Plugin's + * constructor.

        + * + *
        node.on('drag:end', fn, {
        + *     constrained: { constrain2node: '#container' }
        + * });
        + * + *

        This adds Y.Plugin.DDConstrained to the Drag instance using the specified + * configuration.

        + * + *

        A custom detach handle is returned, whose detach method unplugs the + * Y.Plugin.Drag from the node(s).

        + * + *

        Supported events are:

        + *
          + *
        • drag or drag:drag
        • + *
        • drag:start
        • + *
        • drag:end
        • + *
        • drag:mouseDown
        • + *
        • drag:afterMouseDown
        • + *
        • drag:removeHandle
        • + *
        • drag:addHandle
        • + *
        • drag:removeInvalid
        • + *
        • drag:addInvalid
        • + *
        • drag:align
        • + *
        + * + *

        Additionally, the default callback context is overridden to the + * subscribing Node unless otherwise specified during the subscription. + * So "this" in the callback will refer to the node. On the + * event object passed to subscribers, e.currentTarget is also the + * Node regardless of context override. The Drag instance is available from + * the Node as node.dd.

        + * + * @module event-drag + */ + +/** + * Also "drag:drag". Subscribes to the respective event on the generated Drag instance. + * + * @event drag + * @param type {String} 'drag' + * @param fn {Function} the callback function + * @param id {String|Node|etc} the element to bind (typically document) + * @param conf {Object} (optional) configuration to pass to Drag constructor + * @return {Event.Handle} the detach handle + */ +var eventPlugin = Y.Env.evt.plugins, + nodeEvents = Y.Node.DOM_EVENTS, + events = [ + 'drag', + 'drag:start', + 'drag:drag', + 'drag:end', + 'drag:mouseDown', + 'drag:afterMouseDown', + 'drag:removeHandle', + 'drag:addHandle', + 'drag:removeInvalid', + 'drag:addInvalid', + 'drag:align' + ], + eventDef, i; + +eventDef = { + on: function (type, fn, el, conf, ctx) { + var nodes = eventPlugin.drag._getNodes(el), + args = Y.Array(arguments, 4, true); + + args.unshift(fn); + + type = type.indexOf(':') > -1 ? type : 'drag:' + type; + + nodes.each(function (node) { + if (!node.dd) { + node.plug(Y.Plugin.Drag); + } + + eventPlugin.drag._applyConfig(node.dd, conf); + + args[1] = ctx || node; + + var callback = Y.rbind.apply(Y, args); + + node.dd.on(type, function (e) { + e.currentTarget = node; + callback(e); + }); + }); + + return { + detach: function () { + nodes.each(function (node) { + node.unplug(Y.Plugin.Drag); + }); + } + }; + }, + + /** + * Normalizes the third param of on() to a NodeList. The resulting list + * may be empty. + * + * @method _getNodes + * @param el {String|Node|NodeList|HTMLElement|Array} valid context for on() + * @return NodeList + * @protected + */ + _getNodes : function (el) { + el = el || []; + + if (el instanceof Y.NodeList) { + return el; + } else if (Y.Lang.isString(el) || Y.Event._isValidCollection(el)) { + return Y.all(el); + } else { + return Y.all(Y.Array(Y.Node.getDOMNode(el) || [])); + } + }, + + /** + * Applies the attribute values from the config object to the Drag instance. + * Also checks for loaded Plugins by the name of the property to apply. + * + * @method _applyConfig + * @param dd {Y.Plugin.Drag} the Drag plugin for the node + * @param conf {Object} the attribute configuration + * @protected + */ + _applyConfig : function (dd, conf) { + var k, plugin, pname, pconf; + + if (conf) { + for (k in conf) { + if (conf.hasOwnProperty(k)) { + pname = k; + if (k.indexOf('DD') !== 0) { + pname = 'DD' + k.charAt(0).toUpperCase() + k.slice(1); + } + pconf = Y.Lang.isObject(conf[k]) ? conf[k] : {}; + plugin = Y.Plugin[pname]; + + if (plugin) { + dd.plug(plugin, pconf); + } else { + dd.set(k, conf[k]); + } + } + } + } + } +}; + +// Add all the events, defined by the same algo. +for (i = events.length - 1; i >= 0; --i) { + eventPlugin[events[i]] = nodeEvents[events[i]] = eventDef; +} + + +/** + * Also "drag:drag". Subscribes to the respective event on the generated Drag instance. + * + * @event drag + * @param type {String} 'drag' + * @param fn {Function} the callback function + * @param id {String|Node|etc} the element to bind (typically document) + * @param conf {Object} (optional) configuration to pass to Drag constructor + * @return {Event.Handle} the detach handle + */ + +/** + * Subscribes to the respective event on the generated Drag instance. + * + * @event drag:start + * @param type {String} 'drag:start' + * @param fn {Function} the callback function + * @param id {String|Node|etc} the element to bind (typically document) + * @param conf {Object} (optional) configuration to pass to Drag constructor + * @return {Event.Handle} the detach handle + */ + +/** + * Subscribes to the respective event on the generated Drag instance. + * + * @event drag:end + * @param type {String} 'drag:end' + * @param fn {Function} the callback function + * @param id {String|Node|etc} the element to bind (typically document) + * @param conf {Object} (optional) configuration to pass to Drag constructor + * @return {Event.Handle} the detach handle + */ + +/** + * Subscribes to the respective event on the generated Drag instance. + * + * @event drag:mouseDown + * @param type {String} 'drag:mouseDown' + * @param fn {Function} the callback function + * @param id {String|Node|etc} the element to bind (typically document) + * @param conf {Object} (optional) configuration to pass to Drag constructor + * @return {Event.Handle} the detach handle + */ + +/** + * Subscribes to the respective event on the generated Drag instance. + * + * @event drag:afterMouseDown + * @param type {String} 'drag:afterMouseDown' + * @param fn {Function} the callback function + * @param id {String|Node|etc} the element to bind (typically document) + * @param conf {Object} (optional) configuration to pass to Drag constructor + * @return {Event.Handle} the detach handle + */ + +/** + * Subscribes to the respective event on the generated Drag instance. + * + * @event drag:removeHandle + * @param type {String} 'drag:removeHandle' + * @param fn {Function} the callback function + * @param id {String|Node|etc} the element to bind (typically document) + * @param conf {Object} (optional) configuration to pass to Drag constructor + * @return {Event.Handle} the detach handle + */ + +/** + * Subscribes to the respective event on the generated Drag instance. + * + * @event drag:addHandle + * @param type {String} 'drag:addHandle' + * @param fn {Function} the callback function + * @param id {String|Node|etc} the element to bind (typically document) + * @param conf {Object} (optional) configuration to pass to Drag constructor + * @return {Event.Handle} the detach handle + */ + +/** + * Subscribes to the respective event on the generated Drag instance. + * + * @event drag:removeInvalid + * @param type {String} 'drag:removeInvalid' + * @param fn {Function} the callback function + * @param id {String|Node|etc} the element to bind (typically document) + * @param conf {Object} (optional) configuration to pass to Drag constructor + * @return {Event.Handle} the detach handle + */ + +/** + * Subscribes to the respective event on the generated Drag instance. + * + * @event drag:addInvalid + * @param type {String} 'drag:addInvalid' + * @param fn {Function} the callback function + * @param id {String|Node|etc} the element to bind (typically document) + * @param conf {Object} (optional) configuration to pass to Drag constructor + * @return {Event.Handle} the detach handle + */ + +/** + * Subscribes to the respective event on the generated Drag instance. + * + * @event drag:align + * @param type {String} 'drag:align' + * @param fn {Function} the callback function + * @param id {String|Node|etc} the element to bind (typically document) + * @param conf {Object} (optional) configuration to pass to Drag constructor + * @return {Event.Handle} the detach handle + */ + + +}, 'gallery-2009.11.02-20' ,{optional:['dd-proxy','dd-constrain'], requires:['dd-plugin']}); diff --git a/build/gallery-form/gallery-form-debug.js b/build/gallery-form/gallery-form-debug.js new file mode 100644 index 0000000000..7159deebdb --- /dev/null +++ b/build/gallery-form/gallery-form-debug.js @@ -0,0 +1,1387 @@ +YUI.add('gallery-form', function(Y) { + +/** + * Create a form object that can handle both client and server side validation + * + * @module form + */ + + +/** + * Creates an form which contains fields, and does clientside validation, as well + * as handling input from the server. + * + * @class Form + * @extends Widget + * @param config {Object} Configuration object + * @constructor + */ +function Form () { + Form.superclass.constructor.apply(this,arguments); +} + +Y.mix(Form, { + /** + * @property Form.NAME + * @type String + * @static + */ + NAME : 'form', + + /** + * @property Form.ATTRS + * @type Object + * @static + */ + ATTRS : { + /** + * @attribute method + * @type String + * @default 'post' + * @description The method by which the form should be transmitted. Valid values are 'get' and 'post' + */ + method : { + value : 'post', + validator : function (val) { + return this._validateMethod(val); + }, + setter : function (val) { + return val.toLowerCase(); + } + }, + + /** + * @attribute action + * @type String + * @default '' + * @description A url to which the validated form is to be sent + */ + action : { + value : '', + validator : Y.Lang.isString + }, + + /** + * @attribute fields + * @type Array + * @description An array of the fields to be rendered into the form. Each item in the array can either be + * a FormField instance or an object literal defining the properties of the field to be + * generated. Alternatively, this value will be parsed in from HTML + */ + fields : { + writeOnce : true, + validator : function (val) { + return this._validateFields(val); + }, + setter : function (val) { + return this._setFields(val); + } + + }, + + /** + * @attribute errors + * @type Array + * @description An array of errors to be pre-set on form fields. Each error is defined by an object + * literal containing the properties 'name' (corresponding to a form field) and 'message' + */ + errors : { + writeOnce : true, + value : [], + validator : function(val) { + return this._validateErrors(val); + } + } + }, + + /** + * @property Form.HTML_PARSER + * @type Object + * @static + */ + HTML_PARSER : { + action : function (contentBox) { + return this._parseAction(contentBox); + }, + method : function (contentBox) { + return this._parseMethod(contentBox); + }, + fields : function (contentBox) { + return this._parseFields(contentBox); + } + }, + + /** + * @property Form.FORM_TEMPLATE + * @type String + * @static + * @description The HTML used to create the form Node + */ + FORM_TEMPLATE : '
        ' +}); + +Y.extend(Form, Y.Widget, { + /** + * @property _formNode + * @type Y.Node + * @protected + * @description The Y.Node representing the form element + */ + _formNode : null, + + /** + * @property _ioIds + * @type Object + * @protected + * @description An object who's keys represent the IO request ids sent by this Y.Form instance + */ + _ioIds : null, + + /** + * @method _validateAction + * @private + * @param {String} val + * @description Validates the values of the 'action' attribute + */ + _validateMethod : function (val) { + if (!Y.Lang.isString(val)) { + return false; + } + if (val.toLowerCase() != 'get' && val.toLowerCase() != 'post') { + return false; + } + return true; + }, + + /** + * @method _validateFields + * @private + * @param {Array} val + * @description Validates the values of the 'fields' attribute + */ + _validateFields : function (val) { + if (!Y.Lang.isArray(val)) { + return false; + } + + for (var i=0,l=val.length;i', + + /** + * @property FormField.TEXTAREA_TEMPLATE + * @type String + * @description Template used to draw a textarea node + */ + TEXTAREA_TEMPLATE : '', + + /** + * @property FormField.LABEL_TEMPLATE + * @type String + * @description Template used to draw a label node + */ + LABEL_TEMPLATE : '', + + /** + * @property FormField.SELECT_TEMPLATE + * @type String + * @description Template used to draw a select node + */ + SELECT_TEMPLATE : '' +}); + +Y.extend(FormField, Y.Widget, { + /** + * @property _labelNode + * @protected + * @type Object + * @description The label node for this form field + */ + _labelNode : null, + + /** + * @property _fieldNode + * @protected + * @type Object + * @description The form field itself + */ + _fieldNode : null, + + /** + * @property _errorNode + * @protected + * @type Object + * @description If a validation error occurs, it will be displayed in this node + */ + _errorNode : null, + + /** + * @property _nodeType + * @protected + * @type String + * @description The type of form field to draw + */ + _nodeType : 'text', + + /** + * @method _renderLabelNode + * @protected + * @description Draws the form field's label node into the contentBox + */ + _renderLabelNode : function () { + var contentBox = this.get('contentBox'), + labelNode = contentBox.query('label'); + + if (!labelNode || labelNode.get('for') != this.get('id')) { + labelNode = Y.Node.create(FormField.LABEL_TEMPLATE); + contentBox.appendChild(labelNode); + } + + this._labelNode = labelNode; + }, + + /** + * @method _renderFieldNode + * @protected + * @description Draws the field node into the contentBox + */ + _renderFieldNode : function () { + var contentBox = this.get('contentBox'), + field = contentBox.query('#' + this.get('id')); + + if (!field) { + field = Y.Node.create(FormField.INPUT_TEMPLATE); + contentBox.appendChild(field); + } + + this._fieldNode = field; + }, + + /** + * @method _syncLabelNode + * @protected + * @description Syncs the the label node and this instances attributes + */ + _syncLabelNode : function () { + if (this._labelNode) { + this._labelNode.setAttrs({ + innerHTML : this.get('label') + }); + this._labelNode.setAttribute('for', this.get('id')); + } + }, + + /** + * @method _syncLabelNode + * @protected + * @description Syncs the fieldNode and this instances attributes + */ + _syncFieldNode : function () { + this._fieldNode.setAttrs({ + name : this.get('name'), + type : this._nodeType, + id : this.get('id'), + value : this.get('value') + }); + this._fieldNode.setAttribute('tabindex', FormField.tabIndex); + FormField.tabIndex++; + }, + + /** + * @method _checkRequired + * @private + * @description if the required attribute is set to true, returns whether or not a value has been set + * @return {Boolean} + */ + _checkRequired : function () { + if (this.get('required') === true && this.get('value').length === 0) { + return false; + } + return true; + }, + + /** + * @method showError + * @param {String} errMsg + * @description Adds an error node with the supplied message + */ + showError : function (errMsg) { + var contentBox = this.get('contentBox'), + errorNode = Y.Node.create('' + errMsg + ''); + + errorNode.addClass('error'); + contentBox.insertBefore(errorNode,this._labelNode); + + this._errorNode = errorNode; + }, + + /** + * @method clearError + * @description Removes the error node from this field + */ + clearError : function () { + if (this._errorNode) { + var contentBox = this.get('contentBox'); + contentBox.removeChild(this._errorNode); + this._errorNode = null; + } + }, + + /** + * @method validate + * @description Runs the validation functions of this form field + * @return {Boolean} + */ + validate : function () { + var value = this.get('value'), + validator = this.get('validator'); + + this.clearError(); + + if (!this._checkRequired()) { + this.showError('This field is required'); + return false; + } + + return validator.call(this, value); + }, + + /** + * @method clear + * @description Clears the value of this field + */ + clear : function () { + this.set('value', ''); + this._fieldNode.set('value', ''); + }, + + initializer : function () { + this.publish('blur'); + this.publish('change'); + this.publish('focus'); + }, + + destructor : function (config) { + + }, + + renderUI : function () { + this._renderLabelNode(); + this._renderFieldNode(); + }, + + bindUI : function () { + this._fieldNode.on('change', Y.bind(function (e) { + this.set('value', this._fieldNode.get('value')); + }, this)); + + this.on('valueChange', Y.bind(function (e) { + this._fieldNode.set('value', e.newVal); + }, this)); + }, + + syncUI : function () { + this.get('boundingBox').removeAttribute('tabindex'); + this._syncLabelNode(); + this._syncFieldNode(); + } +}); + +Y.FormField = FormField; +/** + * @class TextField + * @extends FormField + * @param config {Object} Configuration object + * @constructor + * @description A text field node + */ +function TextField () { + TextField.superclass.constructor.apply(this,arguments); +} + +Y.mix(TextField, { + NAME : 'text-field' +}); + +Y.extend(TextField, Y.FormField, { + _nodeType : 'text' +}); + +Y.TextField = TextField; +/** + * @class CheckboxField + * @extends FormField + * @param config {Object} Configuration object + * @constructor + * @description A checkbox field node + */ +function CheckboxField () { + CheckboxField.superclass.constructor.apply(this,arguments); +} + +Y.mix(CheckboxField, { + NAME : 'checkbox-field' +}); + +Y.extend(CheckboxField, Y.FormField, { + _nodeType : 'checkbox' +}); + +Y.CheckboxField = CheckboxField; +/** + * @class HiddenField + * @extends FormField + * @param config {Object} Configuration object + * @constructor + * @description A hidden field node + */ +function HiddenField () { + HiddenField.superclass.constructor.apply(this,arguments); +} + +Y.mix(HiddenField, { + /** + * @property HiddenField.NAME + * @type String + * @static + */ + NAME : 'hidden-field', + + /** + * @property HiddenField.ATTRS + * @type Object + * @static + */ + ATTRS : { + /** + * @attribute displayValue + * @type Boolean + * @default false + * @writeOnce + * @description Set to true to render this field with node displaying the current value + */ + displayValue : { + value : false, + writeOnce : true, + validator : Y.Lang.isBoolean + } + } + +}); + +Y.extend(HiddenField, Y.FormField, { + _nodeType : 'hidden', + + /** + * @property _valueDisplayNode + * @protected + * @type Y.Node + * @description Node used to display the value of this field + */ + _valueDisplayNode : null, + + _renderValueDisplayNode : function() { + if (this.get('displayValue') === true) { + var div = Y.Node.create('
        '), + contentBox = this.get('contentBox'); + + contentBox.appendChild(div); + this._valueDisplayNode = div; + } + }, + + renderUI : function () { + HiddenField.superclass.renderUI.apply(this, arguments); + this._renderValueDisplayNode(); + }, + + bindUI : function () { + HiddenField.superclass.bindUI.apply(this, arguments); + + if (this.get('displayValue') === true) { + this.after('valueChange', Y.bind(function(m, e) { + this._valueDisplayNode.set('innerHTML', e.newVal); + }, this, true)); + } + } +}); + +Y.HiddenField = HiddenField; +/** + * @class TextareaField + * @extends FormField + * @param config {Object} Configuration object + * @constructor + * @description A hidden field node + */ +function TextareaField () { + TextareaField.superclass.constructor.apply(this,arguments); +} + +Y.mix(TextareaField, { + NAME : 'textarea-field', + + /** + * @property TextareaField.NODE_TEMPLATE + * @type String + * @description Template used to draw a textarea node + */ + NODE_TEMPLATE : '' +}); + +Y.extend(TextareaField, Y.FormField, { + _renderFieldNode : function () { + var contentBox = this.get('contentBox'), + field = contentBox.query('#' + this.get('id')); + + if (!field) { + field = Y.Node.create(Y.substitute(TextareaField.NODE_TEMPLATE, { + name : this.get('name'), + type : 'text', + id : this.get('id'), + value : this.get('value') + })); + contentBox.appendChild(field); + } + + field.setAttribute('tabindex', Y.FormField.tabIndex); + Y.FormField.tabIndex++; + + this._fieldNode = field; + } +}); + +Y.TextareaField = TextareaField; +/** + * @class ChoiceField + * @extends FormField + * @param config {Object} Configuration object + * @constructor + * @description A form field which allows one or multiple values from a + * selection of choices + */ +function ChoiceField() { + ChoiceField.superclass.constructor.apply(this,arguments); +} + +Y.mix(ChoiceField, { + NAME : 'choice-field', + + ATTRS : { + /** + * @attribute choices + * @type Array + * @description The choices to render into this field + */ + choices : { + validator : function (val) { + return this._validateChoices(val); + } + }, + + /** + * @attribute multiple + * @type Boolean + * @default false + * @description Set to true to allow multiple values to be selected + */ + multiple : { + validator : Y.Lang.isBoolean, + value : false + } + } +}); + +Y.extend(ChoiceField, Y.FormField, { + /** + * @method _validateChoices + * @protected + * @param {Object} val + * @description Validates the value passe to the choices attribute + */ + _validateChoices : function (val) { + if (!Y.Lang.isArray(val)) { + return false; + } + + for (var i=0, l=val.length;i' + this.get('label') + ''); + + contentBox.appendChild(titleNode); + + this._labelNode = titleNode; + }, + + _renderFieldNode : function () { + var contentBox = this.get('contentBox'), + choices = this.get('choices'), + i=0, l=choices.length, + elLabel, elField; + + for(;i', + + /** + * @property SelectField.OPTION_TEMPLATE + * @type String + * @description Template used to draw an option node + */ + OPTION_TEMPLATE : '' +}); + +Y.extend(SelectField, Y.ChoiceField, { + /** + * @method _renderFieldNode + * @protected + * @description Draws the select node into the contentBox + */ + _renderFieldNode : function () { + var contentBox = this.get('contentBox'), + field = contentBox.query('#' + this.get('id')); + + if (!field) { + field = Y.Node.create(SelectField.NODE_TEMPLATE); + contentBox.appendChild(field); + } + + this._fieldNode = field; + + this._renderOptionNodes(); + }, + + /** + * @method _renderOptionNodes + * @protected + * @description Renders the option nodes into the select node + */ + _renderOptionNodes : function () { + var choices = this.get('choices'), + i=0, l=choices.length, + elOption; + + for(;i<=l;i++) { + elOption = Y.Node.create(SelectField.OPTION_TEMPLATE); + this._fieldNode.appendChild(elOption); + } + }, + + /** + * @method _syncFieldNode + * @protected + * @description Syncs the select node with the instance attributes + */ + _syncFieldNode : function () { + this._fieldNode.setAttrs({ + name : this.get('name'), + id : this.get('id'), + multiple : (this.get('multiple') === true ? 'multiple' : '') + }); + }, + + /** + * @method _syncOptionNodes + * @protected + * @description Syncs the option nodes with the choices attribute + */ + _syncOptionNodes : function () { + var choices = this.get('choices'), + contentBox = this.get('contentBox'), + options = contentBox.all('option'); + + options.each(function(node, index, nodeList) { + var label = (index === 0 ? 'Choose one' : choices[index - 1].label), + val = (index === 0 ? '' : choices[index - 1].value); + + node.setAttrs({ + innerHTML : label, + value : val + }); + }, this); + }, + + /** + * @method clear + * @description Restores the selected option to the default + */ + clear : function () { + this._fieldNode.value = ''; + }, + + bindUI : function () { + SelectField.superclass.constructor.superclass.bindUI.apply(this, arguments); + }, + + syncUI : function () { + SelectField.superclass.syncUI.apply(this, arguments); + this._syncOptionNodes(); + } +}); + +Y.SelectField = SelectField; +function Button () { + Button.superclass.constructor.apply(this,arguments); +} + +Y.mix(Button, { + NAME : 'button', + + HTML_PARSER : { + + }, + + ATTRS : { + onclick : { + validator : function (val) { + if (Y.Lang.isObject(val) === false) { + return false; + } + if (typeof val.fn == 'undefined' || + Y.Lang.isFunction(val.fn) === false) { + return false; + } + return true; + }, + value : { + fn : function (e) { + + } + }, + setter : function (val) { + val.scope = val.scope || this; + val.argument = val.argument || {}; + return val; + } + } + }, + + NODE_TEMPLATE : '' +}); + +Y.extend(Button, Y.FormField, { + _renderButtonNode : function () { + var contentBox = this.get('contentBox'), bn; + + bn = Y.Node.create(Button.NODE_TEMPLATE); + contentBox.appendChild(bn); + this._fieldNode = bn; + }, + + _syncLabelNode: function () {}, + + _syncFieldNode : function () { + this._fieldNode.setAttrs({ + innerHTML : this.get('label'), + id : this.get('id') + }); + }, + + _setClickHandler : function () { + if (!this._fieldNode) { + return; + } + + var oc = this.get('onclick'); + Y.Event.purgeElement(this._fieldNode, true, 'click'); + Y.on('click', Y.bind(oc.fn, oc.scope, true), this._fieldNode); + }, + + renderUI : function () { + this._renderButtonNode(); + }, + + bindUI : function () { + this.after('onclickChange', Y.bind(this._setClickHandler, this, true)); + this._setClickHandler(); + } +}); + +Y.Button = Button; + + +}, 'gallery-2009.11.09-19' ,{requires:['node', 'attribute', 'widget', 'io-form', 'substitute']}); diff --git a/build/gallery-form/gallery-form-min.js b/build/gallery-form/gallery-form-min.js new file mode 100644 index 0000000000..98972df49d --- /dev/null +++ b/build/gallery-form/gallery-form-min.js @@ -0,0 +1,3 @@ +YUI.add("gallery-form",function(B){function G(){G.superclass.constructor.apply(this,arguments);}B.mix(G,{NAME:"form",ATTRS:{method:{value:"post",validator:function(K){return this._validateMethod(K);},setter:function(K){return K.toLowerCase();}},action:{value:"",validator:B.Lang.isString},fields:{writeOnce:true,validator:function(K){return this._validateFields(K);},setter:function(K){return this._setFields(K);}},inlineValidation:{value:false,validator:B.Lang.isBoolean}},HTML_PARSER:{action:function(K){return this._parseAction(K);},method:function(K){return this._parseMethod(K);},fields:function(K){return this._parseFields(K);}},FORM_TEMPLATE:"
        "});B.extend(G,B.Widget,{_formNode:null,_ioIds:null,_validateMethod:function(K){if(!B.Lang.isString(K)){return false;}if(K.toLowerCase()!="get"&&K.toLowerCase()!="post"){return false;}return true;},_validateFields:function(L){if(!B.Lang.isArray(L)){return false;}var K=true;B.Array.each(L,function(O,N,M){if((!O instanceof B.FormField)||(!B.Lang.isObject(O))){K=false;}});return K;},_setFields:function(K){K=K||[];var L,M;B.Array.each(K,function(P,O,N){if(!P._classes){M=P.type;if(B.Lang.isFunction(M)){L=M;}else{if(M=="hidden"){L=B.HiddenField;}else{if(M=="checkbox"){L=B.CheckboxField;}else{if(M=="textarea"){L=B.TextareaField;}else{if(M=="select"){L=B.SelectField;}else{if(M=="choice"){L=B.ChoiceField;}else{if(M=="button"||M=="submit"||M=="reset"){L=B.Button;if(M=="submit"){P.onclick={fn:this.submit,scope:this};}else{if(M=="reset"){P.onclick={fn:this.reset,scope:this};}}}else{L=B.TextField;}}}}}}}K[O]=new L(P);}},this);return K;},_parseAction:function(K){var L=K.one("form");return L.get("action");},_parseMethod:function(K){var L=K.one("form");return L.get("method");},_parseFields:function(L){var M=L.all("*"),N=L.all("label"),K=[];M.each(function(Q,P,O){var U=Q.get("nodeName"),S=Q.get("id"),R,T=[];if(U=="INPUT"){R={type:Q.get("type"),name:Q.get("name"),value:Q.get("value")};if(R.type=="submit"||R.type=="reset"||R.type=="button"){R.label=Q.get("value");}}else{if(U=="BUTTON"){R={type:"button",name:Q.get("name"),label:Q.get("innerHTML")};}else{if(U=="SELECT"){Q.all("option").each(function(W,X,V){T.push({label:W.get("innerHTML"),value:W.get("value")});});R={type:"select",name:Q.get("name"),choices:T};}}}if(R){if(S){R.id=S;N.some(function(W,X,V){if(W.get("htmlFor")==S){R.label=W.get("innerHTML");}});}K.push(R);}Q.remove();});return K;},_renderFormNode:function(){var K=this.get("contentBox"),L=K.query("form");if(!L){L=B.Node.create(G.FORM_TEMPLATE);K.appendChild(L);}this._formNode=L;},_renderFormFields:function(){var K=this.get("fields");B.Array.each(K,function(N,M,L){N.render(this._formNode);},this);},_syncFormAttributes:function(){this._formNode.setAttrs({action:this.get("action"),method:this.get("method"),id:this.get("id")});},_runValidation:function(){var K=this.get("fields"),L=true;B.Array.each(K,function(O,N,M){O.set("error",null);if(O.validateField()===false){L=false;}});return L;},_enableInlineValidation:function(){var K=this.get("fields");B.Array.each(K,function(N,M,L){N.set("validateInline",true);});},_disableInlineValidation:function(){var K=this.get("fields");B.Array.each(K,function(N,M,L){N.set("validateInline",false);});},_handleIOSuccess:function(K,L){if(typeof this._ioIds[K]!="undefined"){this.reset();this.fire("success",{response:L});delete this._ioIds[K];}},_handleIOFailure:function(K,L){if(typeof this._ioIds[K]!="undefined"){this.fire("failure",{response:L});delete this._ioIds[K];}},reset:function(){this._formNode.reset();var K=this.get("fields");B.Array.each(K,function(N,M,L){N.clear();});},submit:function(){if(this._runValidation()){var N=this.get("action"),O=this.get("method"),K=this.get("fields"),M="",P,L;B.Array.each(K,function(S,R,Q){if(S.get("name")!==undefined){M+=encodeURIComponent(S.get("name"))+"="+(encodeURIComponent(S.get("value"))||"")+"&";}});L={method:O,data:M};P=B.io(N,L);this._ioIds[P.id]=P;}},getField:function(L){var K=this.get("fields"),M;if(B.Lang.isNumber(L)){return K[L];}else{if(B.Lang.isString(L)){B.Array.each(K,function(P,O,N){if(P.get("name")==L){M=P;}});return M;}}},initializer:function(K){this._ioIds={};this.publish("submit");this.publish("reset");this.publish("success");this.publish("failure");},destructor:function(){this._formNode=null;},renderUI:function(){this._renderFormNode();this._renderFormFields();},bindUI:function(){this._formNode.on("submit",B.bind(function(K){K.halt();},this));this.after("inlineValidationChange",B.bind(function(K){if(K.newValue===true){this._enableInlineValidation();}else{this._disableInlineValidation();}},this));B.on("io:success",B.bind(this._handleIOSuccess,this));B.on("io:failure",B.bind(this._handleIOFailure,this));},syncUI:function(){this._syncFormAttributes();if(this.get("inlineValidation")===true){this._enableInlineValidation();}}});B.Form=G;function C(){C.superclass.constructor.apply(this,arguments);}B.mix(C,{NAME:"form-field",ATTRS:{id:{value:B.guid(),validator:B.Lang.isString,writeOnce:true},name:{validator:B.Lang.isString,writeOnce:true},value:{value:"",validator:B.Lang.isString},label:{value:"",validator:B.Lang.isString},validator:{value:function(K){return true;},validator:function(K){return this._validateValidator(K);},setter:function(K){return this._setValidator(K);}},error:{value:false,validator:function(K){return this._validateError(K);}},required:{value:false,validator:B.Lang.isBoolean},validateInline:{value:false,validator:B.Lang.isBoolean}},tabIndex:0,VALIDATE_EMAIL_ADDRESS:function(M,L){var K=/^([\w]+(?:\.[\w]+)*)@((?:[\w]+\.)*\w[\w]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i;if(K.test(M)===false){L.set("error",C.INVALID_EMAIL_MESSAGE);return false;}return true;},INVALID_EMAIL_MESSAGE:"Please enter a valid email address",VALIDATE_PHONE_NUMBER:function(M,L){var K=/^((\+\d{1,3}(-| )?\(?\d\)?(-| )?\d{1,5})|(\(?\d{2,6}\)?))(-| )?(\d{3,4})(-| )?(\d{4})(( x| ext)\d{1,5}){0,1}$/;if(K.test(M)===false){L.set("error",C.INVALID_PHONE_NUMBER);return false;}return true;},INVALID_PHONE_NUMBER:"Please enter a valid phone number",VALIDATE_IP_ADDRESS:function(O,N){var L=/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/,K,M=true; +if(L.test(O)===false){M=false;}K=O.split(".");B.Array.each(K,function(Q,R,P){var S=parseInt(Q,10);if(S<0||S>255){M=false;}});if(M===false){N.set("error",C.INVALID_IP_MESSAGE);}return M;},INVALID_IP_MESSAGE:"Please enter a valid IP address",VALIDATE_DATE:function(M,L){var K=/^([1-9]|1[0-2])(\-|\/)([0-2][0-9]|3[0-1])(\-|\/)(\d{4}|\d{2})$/;if(K.test(M)===false){L.set("error",C.INVALID_DATE_MESSAGE);return false;}return true;},INVALID_DATE_MESSAGE:"Please enter a a valid date",VALIDATE_TIME:function(M,L){var K=/^([1-9]|1[0-2]):[0-5]\d(:[0-5]\d(\.\d{1,3})?)?$/;if(K.test(M)===false){L.set("error",C.INVALID_TIME_MESSAGE);return false;}return true;},INVALID_TIME_MESSAGE:"Please enter a valid time",VALIDATE_POSTAL_CODE:function(N,M){var K,L=true;if(N.length==6||N.length==7){K=/^[a-zA-Z]\d[a-zA-Z](-|\s)?\d[a-zA-Z]\d$/;}else{if(N.length==5||N.length==10){K=/^\d{5}((-|\s)\d{4})?$/;}else{if(N.length>0){L=false;}}}if(L===false||(K&&K.test(N)===false)){M.set("error",C.INVALID_POSTAL_CODE_MESSAGE);return false;}return true;},INVALID_POSTAL_CODE_MESSAGE:"Please enter a valid postal code",VALIDATE_NO_SPECIAL_CHARS:function(M,L){var K=/^[a-zA-Z0-9]*$/;if(K.test(M)===false){L.set("error",C.INVALID_SPECIAL_CHARS);return false;}return true;},INVALID_SPECIAL_CHARS:"Please use only letters and numbers",INPUT_TEMPLATE:"",TEXTAREA_TEMPLATE:"",LABEL_TEMPLATE:"",SELECT_TEMPLATE:""});B.extend(C,B.Widget,{_labelNode:null,_fieldNode:null,_errorNode:null,_nodeType:"text",_validateError:function(K){if(B.Lang.isString(K)){return true;}if(K===null||typeof K=="undefined"){return true;}return false;},_validateValidator:function(L){if(B.Lang.isString(L)){var K=/^(email|phone|ip|date|time|postal|special)$/;if(K.test(L)===true){return true;}}if(B.Lang.isFunction(L)){return true;}return false;},_setValidator:function(K){if(K=="email"){return C.VALIDATE_EMAIL_ADDRESS;}else{if(K=="phone"){return C.VALIDATE_PHONE_NUMBER;}else{if(K=="ip"){return C.VALIDATE_IP_ADDRESS;}else{if(K=="date"){return C.VALIDATE_DATE;}else{if(K=="time"){return C.VALIDATE_TIME;}else{if(K=="postal"){return C.VALIDATE_POSTAL_CODE;}else{if(K=="special"){return C.VALIDATE_NO_SPECIAL_CHARS;}}}}}}}return K;},_renderLabelNode:function(){var K=this.get("contentBox"),L=K.query("label");if(!L||L.get("for")!=this.get("id")){L=B.Node.create(C.LABEL_TEMPLATE);K.appendChild(L);}this._labelNode=L;},_renderFieldNode:function(){var K=this.get("contentBox"),L=K.query("#"+this.get("id"));if(!L){L=B.Node.create(C.INPUT_TEMPLATE);K.appendChild(L);}this._fieldNode=L;},_syncLabelNode:function(){if(this._labelNode){this._labelNode.setAttrs({innerHTML:this.get("label")});this._labelNode.setAttribute("for",this.get("id"));}},_syncFieldNode:function(){this._fieldNode.setAttrs({name:this.get("name"),type:this._nodeType,id:this.get("id"),value:this.get("value")});this._fieldNode.setAttribute("tabindex",C.tabIndex);C.tabIndex++;},_syncError:function(){var K=this.get("error");if(K){this._showError(K);}},_checkRequired:function(){if(this.get("required")===true&&this.get("value").length===0){return false;}return true;},_showError:function(L){var K=this.get("contentBox"),M=B.Node.create(""+L+"");M.addClass("error");K.insertBefore(M,this._labelNode);this._errorNode=M;},_clearError:function(){if(this._errorNode){var K=this.get("contentBox");K.removeChild(this._errorNode);this._errorNode=null;}},_enableInlineValidation:function(){this.after("valueChange",B.bind(this.validateField,this));},_disableInlineValidation:function(){this.detach("valueChange",this.validateField,this);},validateField:function(M){var L=this.get("value"),K=this.get("validator");this.set("error",null);if(M&&M.src!="ui"){return false;}if(!this._checkRequired()){this.set("error","This field is required");return false;}return K.call(this,L,this);},clear:function(){this.set("value","");this._fieldNode.set("value","");},initializer:function(){this.publish("blur");this.publish("change");this.publish("focus");},destructor:function(K){},renderUI:function(){this._renderLabelNode();this._renderFieldNode();},bindUI:function(){this._fieldNode.on("change",B.bind(function(K){this.set("value",this._fieldNode.get("value"),{src:"ui"});this.fire("change",K);},this));this.on("valueChange",B.bind(function(K){if(K.src!="ui"){this._fieldNode.set("value",K.newVal);}},this));this._fieldNode.on("blur",B.bind(function(K){this.set("value",this._fieldNode.get("value"),{src:"ui"});this.fire("blur",K);},this));this._fieldNode.on("focus",B.bind(function(K){this.fire("focus",K);},this));this.on("errorChange",B.bind(function(K){if(K.newVal){this._showError(K.newVal);}else{this._clearError();}},this));this.on("validateInlineChange",B.bind(function(K){if(K.newVal===true){this._enableInlineValidation();}else{this._disableInlineValidation();}},this));},syncUI:function(){this.get("boundingBox").removeAttribute("tabindex");this._syncLabelNode();this._syncFieldNode();this._syncError();if(this.get("validateInline")===true){this._enableInlineValidation();}}});B.FormField=C;function H(){H.superclass.constructor.apply(this,arguments);}B.mix(H,{NAME:"text-field"});B.extend(H,B.FormField,{_nodeType:"text"});B.TextField=H;function D(){D.superclass.constructor.apply(this,arguments);}B.mix(D,{NAME:"checkbox-field"});B.extend(D,B.FormField,{_nodeType:"checkbox",_getValue:function(L,K){if(this._fieldNode.get("checked")===true){return L;}else{return"";}},initializer:function(){D.superclass.initializer.apply(this,arguments);this.modifyAttr("value",{getter:function(L,K){return this._getValue(L,K);},writeOnce:true});}});B.CheckboxField=D;function J(){J.superclass.constructor.apply(this,arguments);}B.mix(J,{NAME:"hidden-field",ATTRS:{displayValue:{value:false,writeOnce:true,validator:B.Lang.isBoolean}}});B.extend(J,B.FormField,{_nodeType:"hidden",_valueDisplayNode:null,_renderValueDisplayNode:function(){if(this.get("displayValue")===true){var L=B.Node.create("
        "),K=this.get("contentBox");K.appendChild(L); +this._valueDisplayNode=L;}},renderUI:function(){J.superclass.renderUI.apply(this,arguments);this._renderValueDisplayNode();},bindUI:function(){J.superclass.bindUI.apply(this,arguments);if(this.get("displayValue")===true){this.after("valueChange",B.bind(function(K,L){this._valueDisplayNode.set("innerHTML",L.newVal);},this,true));}}});B.HiddenField=J;function E(){E.superclass.constructor.apply(this,arguments);}B.mix(E,{NAME:"textarea-field",NODE_TEMPLATE:""});B.extend(E,B.FormField,{_renderFieldNode:function(){var K=this.get("contentBox"),L=K.query("#"+this.get("id"));if(!L){L=B.Node.create(B.substitute(E.NODE_TEMPLATE,{name:this.get("name"),type:"text",id:this.get("id"),value:this.get("value")}));K.appendChild(L);}L.setAttribute("tabindex",B.FormField.tabIndex);B.FormField.tabIndex++;this._fieldNode=L;}});B.TextareaField=E;function I(){I.superclass.constructor.apply(this,arguments);}B.mix(I,{NAME:"choice-field",ATTRS:{choices:{validator:function(K){return this._validateChoices(K);}},multiple:{validator:B.Lang.isBoolean,value:false}}});B.extend(I,B.FormField,{_validateChoices:function(L){if(!B.Lang.isArray(L)){return false;}var K=true;B.Array.each(L,function(O,N,M){if(!B.Lang.isObject(O)){K=false;return;}if(!O.label||!B.Lang.isString(O.label)||!O.value||!B.Lang.isString(O.value)){K=false;return;}});return K;},_renderLabelNode:function(){var K=this.get("contentBox"),L=B.Node.create(""+this.get("label")+"");K.appendChild(L);this._labelNode=L;},_renderFieldNode:function(){var K=this.get("contentBox"),N=this.get("choices"),L,M;B.Array.each(N,function(Q,P,O){L=B.Node.create(C.LABEL_TEMPLATE);K.appendChild(L);M=B.Node.create(C.INPUT_TEMPLATE);K.appendChild(M);});this._fieldNode=K.all("input");},_syncFieldNode:function(){var N=this.get("choices"),K=this.get("contentBox"),M=K.all("label"),L=(this.get("multiple")===true?"checkbox":"radio");M.each(function(P,O,Q){P.setAttrs({innerHTML:N[O].label});P.setAttribute("for",(this.get("id")+"_choice"+O));},this);this._fieldNode.each(function(Q,O,R){Q.setAttrs({value:N[O].value,id:(this.get("id")+"_choice"+O),name:this.get("name"),type:L});var P=B.Node.getDOMNode(Q);P.value=N[O].value;},this);},clear:function(){this._fieldNode.each(function(L,K,M){L.setAttribute("checked",false);},this);this.set("value","");},bindUI:function(){this._fieldNode.on("change",B.bind(function(K){this._fieldNode.each(function(M,L,N){if(M.get("checked")===true){this.set("value",M.get("value"));}},this);},this));}});B.ChoiceField=I;function A(){A.superclass.constructor.apply(this,arguments);}B.mix(A,{NAME:"select-field",NODE_TEMPLATE:"",OPTION_TEMPLATE:""});B.extend(A,B.ChoiceField,{_renderFieldNode:function(){var K=this.get("contentBox"),L=K.query("#"+this.get("id"));if(!L){L=B.Node.create(A.NODE_TEMPLATE);K.appendChild(L);}this._fieldNode=L;this._renderOptionNodes();},_renderOptionNodes:function(){var L=this.get("choices"),K;B.Array.each(L,function(O,N,M){K=B.Node.create(A.OPTION_TEMPLATE);this._fieldNode.appendChild(K);},this);},_syncFieldNode:function(){this._fieldNode.setAttrs({name:this.get("name"),id:this.get("id"),multiple:(this.get("multiple")===true?"multiple":"")});},_syncOptionNodes:function(){var M=this.get("choices"),K=this.get("contentBox"),L=K.all("option");L.each(function(Q,P,O){var N=(P===0?"Choose one":M[P-1].label),R=(P===0?"":M[P-1].value);Q.setAttrs({innerHTML:N,value:R});},this);},clear:function(){this._fieldNode.value="";},bindUI:function(){A.superclass.constructor.superclass.bindUI.apply(this,arguments);},syncUI:function(){A.superclass.syncUI.apply(this,arguments);this._syncOptionNodes();}});B.SelectField=A;function F(){F.superclass.constructor.apply(this,arguments);}B.mix(F,{NAME:"button",HTML_PARSER:{},ATTRS:{onclick:{validator:function(K){if(B.Lang.isObject(K)===false){return false;}if(typeof K.fn=="undefined"||B.Lang.isFunction(K.fn)===false){return false;}return true;},value:{fn:function(K){}},setter:function(K){K.scope=K.scope||this;K.argument=K.argument||{};return K;}}},NODE_TEMPLATE:""});B.extend(F,B.FormField,{_renderButtonNode:function(){var K=this.get("contentBox"),L;L=B.Node.create(F.NODE_TEMPLATE);K.appendChild(L);this._fieldNode=L;},_syncLabelNode:function(){},_syncFieldNode:function(){this._fieldNode.setAttrs({innerHTML:this.get("label"),id:this.get("id")});},_setClickHandler:function(){if(!this._fieldNode){return;}var K=this.get("onclick");B.Event.purgeElement(this._fieldNode,true,"click");B.on("click",B.bind(K.fn,K.scope,true),this._fieldNode);},renderUI:function(){this._renderButtonNode();},bindUI:function(){this.after("onclickChange",B.bind(this._setClickHandler,this,true));this._setClickHandler();}});B.Button=F;},"gallery-2009.11.19-20",{requires:["node","widget","io-base"]}); \ No newline at end of file diff --git a/build/gallery-form/gallery-form.js b/build/gallery-form/gallery-form.js new file mode 100644 index 0000000000..9382f984c6 --- /dev/null +++ b/build/gallery-form/gallery-form.js @@ -0,0 +1,1386 @@ +YUI.add('gallery-form', function(Y) { + +/** + * Create a form object that can handle both client and server side validation + * + * @module form + */ + + +/** + * Creates an form which contains fields, and does clientside validation, as well + * as handling input from the server. + * + * @class Form + * @extends Widget + * @param config {Object} Configuration object + * @constructor + */ +function Form () { + Form.superclass.constructor.apply(this,arguments); +} + +Y.mix(Form, { + /** + * @property Form.NAME + * @type String + * @static + */ + NAME : 'form', + + /** + * @property Form.ATTRS + * @type Object + * @static + */ + ATTRS : { + /** + * @attribute method + * @type String + * @default 'post' + * @description The method by which the form should be transmitted. Valid values are 'get' and 'post' + */ + method : { + value : 'post', + validator : function (val) { + return this._validateMethod(val); + }, + setter : function (val) { + return val.toLowerCase(); + } + }, + + /** + * @attribute action + * @type String + * @default '' + * @description A url to which the validated form is to be sent + */ + action : { + value : '', + validator : Y.Lang.isString + }, + + /** + * @attribute fields + * @type Array + * @description An array of the fields to be rendered into the form. Each item in the array can either be + * a FormField instance or an object literal defining the properties of the field to be + * generated. Alternatively, this value will be parsed in from HTML + */ + fields : { + writeOnce : true, + validator : function (val) { + return this._validateFields(val); + }, + setter : function (val) { + return this._setFields(val); + } + + }, + + /** + * @attribute errors + * @type Array + * @description An array of errors to be pre-set on form fields. Each error is defined by an object + * literal containing the properties 'name' (corresponding to a form field) and 'message' + */ + errors : { + writeOnce : true, + value : [], + validator : function(val) { + return this._validateErrors(val); + } + } + }, + + /** + * @property Form.HTML_PARSER + * @type Object + * @static + */ + HTML_PARSER : { + action : function (contentBox) { + return this._parseAction(contentBox); + }, + method : function (contentBox) { + return this._parseMethod(contentBox); + }, + fields : function (contentBox) { + return this._parseFields(contentBox); + } + }, + + /** + * @property Form.FORM_TEMPLATE + * @type String + * @static + * @description The HTML used to create the form Node + */ + FORM_TEMPLATE : '
        ' +}); + +Y.extend(Form, Y.Widget, { + /** + * @property _formNode + * @type Y.Node + * @protected + * @description The Y.Node representing the form element + */ + _formNode : null, + + /** + * @property _ioIds + * @type Object + * @protected + * @description An object who's keys represent the IO request ids sent by this Y.Form instance + */ + _ioIds : null, + + /** + * @method _validateAction + * @private + * @param {String} val + * @description Validates the values of the 'action' attribute + */ + _validateMethod : function (val) { + if (!Y.Lang.isString(val)) { + return false; + } + if (val.toLowerCase() != 'get' && val.toLowerCase() != 'post') { + return false; + } + return true; + }, + + /** + * @method _validateFields + * @private + * @param {Array} val + * @description Validates the values of the 'fields' attribute + */ + _validateFields : function (val) { + if (!Y.Lang.isArray(val)) { + return false; + } + + for (var i=0,l=val.length;i', + + /** + * @property FormField.TEXTAREA_TEMPLATE + * @type String + * @description Template used to draw a textarea node + */ + TEXTAREA_TEMPLATE : '', + + /** + * @property FormField.LABEL_TEMPLATE + * @type String + * @description Template used to draw a label node + */ + LABEL_TEMPLATE : '', + + /** + * @property FormField.SELECT_TEMPLATE + * @type String + * @description Template used to draw a select node + */ + SELECT_TEMPLATE : '' +}); + +Y.extend(FormField, Y.Widget, { + /** + * @property _labelNode + * @protected + * @type Object + * @description The label node for this form field + */ + _labelNode : null, + + /** + * @property _fieldNode + * @protected + * @type Object + * @description The form field itself + */ + _fieldNode : null, + + /** + * @property _errorNode + * @protected + * @type Object + * @description If a validation error occurs, it will be displayed in this node + */ + _errorNode : null, + + /** + * @property _nodeType + * @protected + * @type String + * @description The type of form field to draw + */ + _nodeType : 'text', + + /** + * @method _renderLabelNode + * @protected + * @description Draws the form field's label node into the contentBox + */ + _renderLabelNode : function () { + var contentBox = this.get('contentBox'), + labelNode = contentBox.query('label'); + + if (!labelNode || labelNode.get('for') != this.get('id')) { + labelNode = Y.Node.create(FormField.LABEL_TEMPLATE); + contentBox.appendChild(labelNode); + } + + this._labelNode = labelNode; + }, + + /** + * @method _renderFieldNode + * @protected + * @description Draws the field node into the contentBox + */ + _renderFieldNode : function () { + var contentBox = this.get('contentBox'), + field = contentBox.query('#' + this.get('id')); + + if (!field) { + field = Y.Node.create(FormField.INPUT_TEMPLATE); + contentBox.appendChild(field); + } + + this._fieldNode = field; + }, + + /** + * @method _syncLabelNode + * @protected + * @description Syncs the the label node and this instances attributes + */ + _syncLabelNode : function () { + if (this._labelNode) { + this._labelNode.setAttrs({ + innerHTML : this.get('label') + }); + this._labelNode.setAttribute('for', this.get('id')); + } + }, + + /** + * @method _syncLabelNode + * @protected + * @description Syncs the fieldNode and this instances attributes + */ + _syncFieldNode : function () { + this._fieldNode.setAttrs({ + name : this.get('name'), + type : this._nodeType, + id : this.get('id'), + value : this.get('value') + }); + this._fieldNode.setAttribute('tabindex', FormField.tabIndex); + FormField.tabIndex++; + }, + + /** + * @method _checkRequired + * @private + * @description if the required attribute is set to true, returns whether or not a value has been set + * @return {Boolean} + */ + _checkRequired : function () { + if (this.get('required') === true && this.get('value').length === 0) { + return false; + } + return true; + }, + + /** + * @method showError + * @param {String} errMsg + * @description Adds an error node with the supplied message + */ + showError : function (errMsg) { + var contentBox = this.get('contentBox'), + errorNode = Y.Node.create('' + errMsg + ''); + + errorNode.addClass('error'); + contentBox.insertBefore(errorNode,this._labelNode); + + this._errorNode = errorNode; + }, + + /** + * @method clearError + * @description Removes the error node from this field + */ + clearError : function () { + if (this._errorNode) { + var contentBox = this.get('contentBox'); + contentBox.removeChild(this._errorNode); + this._errorNode = null; + } + }, + + /** + * @method validate + * @description Runs the validation functions of this form field + * @return {Boolean} + */ + validate : function () { + var value = this.get('value'), + validator = this.get('validator'); + + this.clearError(); + + if (!this._checkRequired()) { + this.showError('This field is required'); + return false; + } + + return validator.call(this, value); + }, + + /** + * @method clear + * @description Clears the value of this field + */ + clear : function () { + this.set('value', ''); + this._fieldNode.set('value', ''); + }, + + initializer : function () { + this.publish('blur'); + this.publish('change'); + this.publish('focus'); + }, + + destructor : function (config) { + + }, + + renderUI : function () { + this._renderLabelNode(); + this._renderFieldNode(); + }, + + bindUI : function () { + this._fieldNode.on('change', Y.bind(function (e) { + this.set('value', this._fieldNode.get('value')); + }, this)); + + this.on('valueChange', Y.bind(function (e) { + this._fieldNode.set('value', e.newVal); + }, this)); + }, + + syncUI : function () { + this.get('boundingBox').removeAttribute('tabindex'); + this._syncLabelNode(); + this._syncFieldNode(); + } +}); + +Y.FormField = FormField; +/** + * @class TextField + * @extends FormField + * @param config {Object} Configuration object + * @constructor + * @description A text field node + */ +function TextField () { + TextField.superclass.constructor.apply(this,arguments); +} + +Y.mix(TextField, { + NAME : 'text-field' +}); + +Y.extend(TextField, Y.FormField, { + _nodeType : 'text' +}); + +Y.TextField = TextField; +/** + * @class CheckboxField + * @extends FormField + * @param config {Object} Configuration object + * @constructor + * @description A checkbox field node + */ +function CheckboxField () { + CheckboxField.superclass.constructor.apply(this,arguments); +} + +Y.mix(CheckboxField, { + NAME : 'checkbox-field' +}); + +Y.extend(CheckboxField, Y.FormField, { + _nodeType : 'checkbox' +}); + +Y.CheckboxField = CheckboxField; +/** + * @class HiddenField + * @extends FormField + * @param config {Object} Configuration object + * @constructor + * @description A hidden field node + */ +function HiddenField () { + HiddenField.superclass.constructor.apply(this,arguments); +} + +Y.mix(HiddenField, { + /** + * @property HiddenField.NAME + * @type String + * @static + */ + NAME : 'hidden-field', + + /** + * @property HiddenField.ATTRS + * @type Object + * @static + */ + ATTRS : { + /** + * @attribute displayValue + * @type Boolean + * @default false + * @writeOnce + * @description Set to true to render this field with node displaying the current value + */ + displayValue : { + value : false, + writeOnce : true, + validator : Y.Lang.isBoolean + } + } + +}); + +Y.extend(HiddenField, Y.FormField, { + _nodeType : 'hidden', + + /** + * @property _valueDisplayNode + * @protected + * @type Y.Node + * @description Node used to display the value of this field + */ + _valueDisplayNode : null, + + _renderValueDisplayNode : function() { + if (this.get('displayValue') === true) { + var div = Y.Node.create('
        '), + contentBox = this.get('contentBox'); + + contentBox.appendChild(div); + this._valueDisplayNode = div; + } + }, + + renderUI : function () { + HiddenField.superclass.renderUI.apply(this, arguments); + this._renderValueDisplayNode(); + }, + + bindUI : function () { + HiddenField.superclass.bindUI.apply(this, arguments); + + if (this.get('displayValue') === true) { + this.after('valueChange', Y.bind(function(m, e) { + this._valueDisplayNode.set('innerHTML', e.newVal); + }, this, true)); + } + } +}); + +Y.HiddenField = HiddenField; +/** + * @class TextareaField + * @extends FormField + * @param config {Object} Configuration object + * @constructor + * @description A hidden field node + */ +function TextareaField () { + TextareaField.superclass.constructor.apply(this,arguments); +} + +Y.mix(TextareaField, { + NAME : 'textarea-field', + + /** + * @property TextareaField.NODE_TEMPLATE + * @type String + * @description Template used to draw a textarea node + */ + NODE_TEMPLATE : '' +}); + +Y.extend(TextareaField, Y.FormField, { + _renderFieldNode : function () { + var contentBox = this.get('contentBox'), + field = contentBox.query('#' + this.get('id')); + + if (!field) { + field = Y.Node.create(Y.substitute(TextareaField.NODE_TEMPLATE, { + name : this.get('name'), + type : 'text', + id : this.get('id'), + value : this.get('value') + })); + contentBox.appendChild(field); + } + + field.setAttribute('tabindex', Y.FormField.tabIndex); + Y.FormField.tabIndex++; + + this._fieldNode = field; + } +}); + +Y.TextareaField = TextareaField; +/** + * @class ChoiceField + * @extends FormField + * @param config {Object} Configuration object + * @constructor + * @description A form field which allows one or multiple values from a + * selection of choices + */ +function ChoiceField() { + ChoiceField.superclass.constructor.apply(this,arguments); +} + +Y.mix(ChoiceField, { + NAME : 'choice-field', + + ATTRS : { + /** + * @attribute choices + * @type Array + * @description The choices to render into this field + */ + choices : { + validator : function (val) { + return this._validateChoices(val); + } + }, + + /** + * @attribute multiple + * @type Boolean + * @default false + * @description Set to true to allow multiple values to be selected + */ + multiple : { + validator : Y.Lang.isBoolean, + value : false + } + } +}); + +Y.extend(ChoiceField, Y.FormField, { + /** + * @method _validateChoices + * @protected + * @param {Object} val + * @description Validates the value passe to the choices attribute + */ + _validateChoices : function (val) { + if (!Y.Lang.isArray(val)) { + return false; + } + + for (var i=0, l=val.length;i' + this.get('label') + ''); + + contentBox.appendChild(titleNode); + + this._labelNode = titleNode; + }, + + _renderFieldNode : function () { + var contentBox = this.get('contentBox'), + choices = this.get('choices'), + i=0, l=choices.length, + elLabel, elField; + + for(;i', + + /** + * @property SelectField.OPTION_TEMPLATE + * @type String + * @description Template used to draw an option node + */ + OPTION_TEMPLATE : '' +}); + +Y.extend(SelectField, Y.ChoiceField, { + /** + * @method _renderFieldNode + * @protected + * @description Draws the select node into the contentBox + */ + _renderFieldNode : function () { + var contentBox = this.get('contentBox'), + field = contentBox.query('#' + this.get('id')); + + if (!field) { + field = Y.Node.create(SelectField.NODE_TEMPLATE); + contentBox.appendChild(field); + } + + this._fieldNode = field; + + this._renderOptionNodes(); + }, + + /** + * @method _renderOptionNodes + * @protected + * @description Renders the option nodes into the select node + */ + _renderOptionNodes : function () { + var choices = this.get('choices'), + i=0, l=choices.length, + elOption; + + for(;i<=l;i++) { + elOption = Y.Node.create(SelectField.OPTION_TEMPLATE); + this._fieldNode.appendChild(elOption); + } + }, + + /** + * @method _syncFieldNode + * @protected + * @description Syncs the select node with the instance attributes + */ + _syncFieldNode : function () { + this._fieldNode.setAttrs({ + name : this.get('name'), + id : this.get('id'), + multiple : (this.get('multiple') === true ? 'multiple' : '') + }); + }, + + /** + * @method _syncOptionNodes + * @protected + * @description Syncs the option nodes with the choices attribute + */ + _syncOptionNodes : function () { + var choices = this.get('choices'), + contentBox = this.get('contentBox'), + options = contentBox.all('option'); + + options.each(function(node, index, nodeList) { + var label = (index === 0 ? 'Choose one' : choices[index - 1].label), + val = (index === 0 ? '' : choices[index - 1].value); + + node.setAttrs({ + innerHTML : label, + value : val + }); + }, this); + }, + + /** + * @method clear + * @description Restores the selected option to the default + */ + clear : function () { + this._fieldNode.value = ''; + }, + + bindUI : function () { + SelectField.superclass.constructor.superclass.bindUI.apply(this, arguments); + }, + + syncUI : function () { + SelectField.superclass.syncUI.apply(this, arguments); + this._syncOptionNodes(); + } +}); + +Y.SelectField = SelectField; +function Button () { + Button.superclass.constructor.apply(this,arguments); +} + +Y.mix(Button, { + NAME : 'button', + + HTML_PARSER : { + + }, + + ATTRS : { + onclick : { + validator : function (val) { + if (Y.Lang.isObject(val) === false) { + return false; + } + if (typeof val.fn == 'undefined' || + Y.Lang.isFunction(val.fn) === false) { + return false; + } + return true; + }, + value : { + fn : function (e) { + + } + }, + setter : function (val) { + val.scope = val.scope || this; + val.argument = val.argument || {}; + return val; + } + } + }, + + NODE_TEMPLATE : '' +}); + +Y.extend(Button, Y.FormField, { + _renderButtonNode : function () { + var contentBox = this.get('contentBox'), bn; + + bn = Y.Node.create(Button.NODE_TEMPLATE); + contentBox.appendChild(bn); + this._fieldNode = bn; + }, + + _syncLabelNode: function () {}, + + _syncFieldNode : function () { + this._fieldNode.setAttrs({ + innerHTML : this.get('label'), + id : this.get('id') + }); + }, + + _setClickHandler : function () { + if (!this._fieldNode) { + return; + } + + var oc = this.get('onclick'); + Y.Event.purgeElement(this._fieldNode, true, 'click'); + Y.on('click', Y.bind(oc.fn, oc.scope, true), this._fieldNode); + }, + + renderUI : function () { + this._renderButtonNode(); + }, + + bindUI : function () { + this.after('onclickChange', Y.bind(this._setClickHandler, this, true)); + this._setClickHandler(); + } +}); + +Y.Button = Button; + + +}, 'gallery-2009.11.09-19' ,{requires:['node', 'attribute', 'widget', 'io-form', 'substitute']}); diff --git a/build/gallery-formvalidator/gallery-formvalidator-debug.js b/build/gallery-formvalidator/gallery-formvalidator-debug.js new file mode 100644 index 0000000000..836e14bea4 --- /dev/null +++ b/build/gallery-formvalidator/gallery-formvalidator-debug.js @@ -0,0 +1,2925 @@ +YUI.add('gallery-formvalidator', function(Y) { + +/** + * @module Validator + * @title Form Validator Widget + * @requires yahoo.base, yahoo.dom, yahoo.event + * @namespace Validator + * This is the main form validator class. This widget will allow developers to easily + * transform a standard form into a fully interactive form that handle almost all + * invalid inputs gracefully and improve user experience. + * @class Form + */ + var YL = Y.Lang, S4 = function() { + return (((1+Math.random())*0x10000)|0).toString(16).substring(1); + }, + /** + * This will make a random guid id. This is useful for ensuring ajax requests + * don't get cached. + */ + guid = function() { + return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4()); + }, + /** + * THis will get all submit buttons in the form that are to be a part of the form validator. + * This will exclude forms inside of other forms that are going to use the form validator. + */ + GETSUBMITBUTTONS = function(parent){ + var rtVl = [],children,i; + if ((parent.tagName !== null && parent.tagName !== undefined) && (parent.tagName.toLowerCase() == 'input') && (parent.type == 'submit')){ + return [parent]; + } + children = parent.children; + if (children === null || children === undefined){ + children = parent.childNodes; + } + for (i = 0 ; i < children.length; ++i){ + rtVl = rtVl.concat(GETSUBMITBUTTONS(children[i])); + } + return rtVl; + }, + /** + * Returns true if the given dom is marked as a form (inline). + */ + ISFORM = function(dom){ + var formIndicator; + if (dom.getAttribute === null || dom.getAttribute === undefined){ + return false; + } + formIndicator = dom.getAttribute('formvalidator:Form'); + if (formIndicator === null || formIndicator === undefined){ + return false; + } + formIndicator = formIndicator.toLowerCase(); + return (formIndicator == 'yes') || (formIndicator == 'true'); + }, + /** + * This will collect all inputs with inline declarations. + */ + GETINLINEDECLARATIONS = function(parent){ + var rtVl = [],isGroup = false,children = null,i=0,inlineIndicator,groupIndicator; + if (parent.getAttribute !== null && parent.getAttribute !== undefined){ + inlineIndicator = parent.getAttribute('formvalidator:FormField'); + groupIndicator = parent.getAttribute('formvalidator:FormGroup'); + // add parent to the return value if the parent is a form field + + if ((inlineIndicator !== null && inlineIndicator !== undefined) && ((inlineIndicator.toLowerCase() == 'true') || (inlineIndicator.toLowerCase() == 'yes'))){ + rtVl[0] = parent; + } + if ((groupIndicator !== null && groupIndicator !== undefined) && ((groupIndicator.toLowerCase() == 'true') || (groupIndicator.toLowerCase() == 'yes'))){ + isGroup = true; + } + } + + children = parent.children; + if (children === null || children === undefined){ + children = parent.childNodes; + } + for (i = 0 ; i < children.length; ++i){ + // if the element in an inner form, we skip it, as we do not want to add + // that form's inputs to the this form + if (!ISFORM(children[i])){ + rtVl = rtVl.concat(GETINLINEDECLARATIONS(children[i])); + } + } + // groups need to have their members placed inside json structure so those + // inputs can be put into the group, instead of on the main level. + if (isGroup){ + return [ + { + isGroup:true, + groupDOM:parent, + members:rtVl + } + ]; + } + else{ + return rtVl; + } + }; + /** + * @constructor + * This will initailize the form validator with the given configuration json object + * @param {Object} config Configuration object containing everything for configuring the form validator object. + */ + function _Validator(config){ + _Validator.superclass.constructor.apply(this,arguments); + this._initializeEvents(); + this.initializeInputs(); + this.initializeButtons(); + this.checkFormValues(); + this.on('inputfield:onchanged',this.onFormValueChanged); + if (this.get('checkOnSubmit')){ + this.enableButtons(); + } + this.publish(_Validator.CE_ONSUBMIT); + } + _Validator.staticFunctions = { + /** + * Static function that will set a boolean value for a property + * @method BOOLEANSETTER + * @static + * @param {boolean|string} val value of yes/no/true/false + */ + BOOLEANSETTER:function(val){ + if (YL.isBoolean(val)){ + return val; + } + else if (YL.isString(val)){ + return val.toLowerCase() == 'true'; + } + else{ + return val !== null && val !== undefined; + } + } + }; + Y.augment(_Validator, Y.EventTarget); + // attributes + _Validator.ATTRS = { + /** + * This is used for custom inputs so their validator object can be stored in the form + * until the instance of is pulled out of an inline attribute + * @property customGlobal + * @type Object + */ + customGlobal:{ + value:{} + }, + /** + * This is the form DOM object (or any DOM object) that surrounds + * the inputs of the form. (NOTE: Not all inputs HAVE to be inside the form dom if your using the JSON method + * to declar your fields). + * @property form + * @type HTMLElement + */ + form:{ + setter:function(el){ + var rtVl = el; + if (YL.isString(el)){ + rtVl = Y.DOM.byId(el); + } + if (rtVl === null || rtVl === undefined){ + throw 'Invalid form: Form with id ' + el + ' does not exist'; + } + return rtVl; + }, + value:null + }, + /** + * Default value is false, when set to true, indicators on inputs, and the form's status will only + * update when the submit button is clicked. + * @property checkOnSubmit + * @type boolean + */ + checkOnSubmit:{ + value:false, + setter:_Validator.staticFunctions.BOOLEANSETTER + + }, + /** + * This is a function that is called after the form is checked, and is valid, but before + * the actual submit takes place. If this function returns true, the form will submit, + * if it returns false, the form submission will not proceed. + * @property onSubmit + * @type Function + */ + onSubmit:{ + value:null + }, + /** + * This is the scope in which the onSubmit function will be executed. If this + * is null, the function will be executed in the global scope. + * @property onSubmitScope + * @type Object + */ + onSubmitScope:{ + value:null + }, + /** + * This is a setting passed onto each field added to the validator. If + * this is true, the input field will create an incorrect indicator based + * setting in the configuration object passed to the field, or on the default settings passed to it + * from this class. + * @property createIncorrectIndicator + * @type boolean + */ + createIncorrectIndicator:{ + value:false + }, + /** + * This is a setting passed onto each field added to the validator. If + * this is true, the input field will create an correct indicator based + * setting in the configuration object passed to the field, or on the default settings passed to it + * from this class. + * @property createIncorrectIndicator + * @type boolean + */ + createCorrectIndicator:{ + value:false + }, + /** + * This is the name of the type of dom object used to create the indicators. + * The default is SPAN + * @property defaultIndicatorDomType + * @type string + */ + defaultIndicatorDomType:{ + value:'SPAN' + }, + /** + * The default css used when creating the incorrect indicator dynamically. This + * css value is '' by default, and will be passed to any input field added + * to the validator if they do not already have this property set in their configuration. + * @property defaultIncorrectIndicatorCss + * @type string + */ + defaultIncorrectIndicatorCss:{ + value:'' + }, + /** + * The default css used when creating the correct indicator dynamically. This + * css value is '' by default, and will be passed to any input field added + * to the validator if they do not already have this property set in their configuration. + * @property defaultCorrectIndicatorCss + * @type string + */ + defaultCorrectIndicatorCss:{ + value:'' + }, + /** + * The default text that will be used to set the innerHTML of the correct indicator + * when it is created dynamically. A non breakable space by default, and will be passed to any input field added + * to the validator if they do not already have this property set in their configuration. + * @property correctIndicatorText + * @type string + */ + correctIndicatorText:{ + value:' ' + }, + /** + * The default text that will be used to set the innerHTML of the incorrect indicator + * when it is created dynamically. A non breakable space by default, and will be passed to any input field added + * to the validator if they do not already have this property set in their configuration. + * @property correctIndicatorText + * @type string + */ + incorrectIndicatorText:{ + value:' ' + }, + /** + * This will hold the JSON configuration for all inputs passed to the form validator. + * This will be intereated through, and the actual input field objects created + * when the form is initialized. + * @property fieldJSON + * @type Object[] + */ + fieldJSON:{ + value:[] + }, + /** + * This will hold the JSON configuration for all buttons passed to the form validator. + * This will be intereated through, and the actual button objects created + * when the form is initialized. + * @property buttonJSON + * @type Object[] + */ + buttonJSON:{ + value:[] + }, + /** + * This is a list of input field objects that represent inputs that are to be validated using the validator. + * @property inputFields + * @type BaseInputField[] + */ + inputFields:{ + value:[] + }, + /** + * List of buttons that will only enable if all the inputs on the form + * validator are correct. + * @property buttons + * @type Button[] + */ + buttons:{ + value:[] + }, + /** + * List of ids of submit buttons that are exempt from the form validator. For instance, you may + * have a submit button that deletes the data pertaining to the record open in the form. + * Obviously the form should not have to be filled in correctly to do this. + * @property excludedButtons + * @type string[] + */ + excludedButtons:{ + value:[] + } + }; + _Validator.NAME = 'Validator'; + /** + * This is the event that is invoked the form is submitted. + * @event onsubmit + */ + _Validator.CE_ONSUBMIT = 'form:onsubmit'; + Y.extend(_Validator,Y.Base,{ + /** + * This function will process the input field configurations in the fieldJSON + * property, and have all fields instantiated and initialized and stored + * in the inputFields property. + * @method initializeInputs + */ + initializeInputs:function(){ + var fields = this.get('fieldJSON'),i,newField,syncDom; + for (i = 0 ; i < fields.length; ++i){ + newField = new fields[i].type(fields[i].atts,false); + syncDom = newField.getInputDOM(); + if (syncDom !== null && syncDom !== undefined){ + newField.synchronize(syncDom); + } + this.addInput(newField); + } + this.initializeInlineInputs(); // now do the inline, done after so that if some data is inline, and some and json, we don't re-add the input field from inline + }, + /** + * This will return an input with the given id. Any inputs with a null + * id will never be returned from this function. + * @method getInput + * @return {BaseInputField} field with the given id. + */ + getInput:function(id){ + if (id === null || id === undefined){ + return null; // if they use null, we don't want anything coming back, if we didn't exit here, an item with a null id might be returned, and that would be bad practice + } + var inputFields = this.get('inputFields'),i,rtVl; + for (i = 0 ; i < inputFields.length ; ++i){ + if (inputFields[i].getId() == id){ + return inputFields[i]; + } + if (inputFields[i].isGroup()){ + rtVl = inputFields[i].getInput(id); + if (rtVl !== null && rtVl !== undefined){ + return rtVl; + } + } + } + return null; + }, + /** + * This will clear all inputs. If silent mode is true, the indicators and buttons + * will not update until an input is changed, or a submit button is pressed. + * @method clear + * @param {boolean} silent If set to true, the form validator's status will not update. + */ + clear:function(silent){ + var inputFields = this.get('inputFields'),i; + for (i = 0 ; i < inputFields.length ; ++i){ + inputFields[i].clear(silent); + } + }, + /** + * This will take the new field that is to be added to the form, and apply any + * default settings from the Form's defaults to the input field, if they haven't been set. + * For instance, defaultCorrectIndicatorCss will be applied to correctIndicatorCss property + * in the input field if that property has not been set for the newField + * @method setupInput + * @param {BaseInputField} newField Input field that is to be added to the Form. + */ + setupInput:function(newField){ + // setup the defaults + newField.set('createIncorrectIndicator',this.get('createIncorrectIndicator')); + newField.set('createCorrectIndicator',this.get('createCorrectIndicator')); + this.checkAttribute(newField,'defaultIncorrectIndicatorCss','incorrectIndicatorCss'); + this.checkAttribute(newField,'defaultCorrectIndicatorCss','correctIndicatorCss'); + this.checkAttribute(newField,'defaultIndicatorDomType','indicatorType'); + this.checkAttribute(newField,'correctIndicatorText','correctIndicatorText'); + this.checkAttribute(newField,'incorrectIndicatorText','incorrectIndicatorText'); + newField.initializeInput(this); + // if we are only checking on submit, then the events on the inputs do not need to be listened for + // this will gain us some performance. + if (!this.get('checkOnSubmit')){ + newField.initializeEvents(); + } + newField.addTarget(this); + }, + /** + * This will add the given field to the form validator. It will first, setup + * the input using the setupInput function, then it will add it to it's list + * of input fields. + * @method addInput + * @param {BaseInputField} newField Input field that is to be added to the Form. + */ + addInput:function(newField){ + this.setupInput(newField); + var inputFields = this.get('inputFields'); + inputFields[inputFields.length] = newField; + }, + /** + * This function will initialize inputs that are declared inline. If they are already + * declared in the JSON, then their JSON definition is updated with any settings that are set inline. + * @method initializeInlineInputs + */ + initializeInlineInputs:function(){ + var inlineFields = GETINLINEDECLARATIONS(this.get('form')),i; + for (i = 0 ; i < inlineFields.length; ++i){ + this.addInlineInput(inlineFields[i]); + } + }, + /** + * This will add all the member doms to the given group JSON def. + * @method constructInlineGroup + */ + constructInlineGroup:function(group){ + var memberDOMS = group.members,theGroup = this.getInput(group.groupDOM.id),groupNew = false,i,tempInput,type,newField; + if (theGroup === null || theGroup === undefined){ + groupNew = true; + theGroup = new Y.GroupBaseField({ + groupDOM:group.groupDOM + },false); + theGroup.synchronize(group.groupDOM); + } + + for (i = 0 ; i < memberDOMS.length; ++i){ + tempInput = this.getInput(memberDOMS[i].id); + if (memberDOMS[i].isGroup){ + theGroup.addInput(this.constructInlineGroup(memberDOMS[i])); + } + // if the field exists already, don't do anything + // For inputs with no ids, they cannot be put in through JSON, only inline, so we don't need to worry + else if (tempInput === null || tempInput === undefined) { + type = Y[memberDOMS[i].getAttribute('formvalidator:type')]; + newField = new type({ + inputDOM:memberDOMS[i] + },false); + this.setupInput(newField); + theGroup.addInput(newField,this); + newField.synchronize(memberDOMS[i]); + } + } + if (groupNew){ + this.setupInput(theGroup); + } + return theGroup; + }, + /** + * This will take the given inputDOM, extract all form validator specific + * attributes from the DOM object, and create an input field from that and add + * it to this form validator. + * @method addInlineInput + * @param {HTMLEelement} inputDOM Element that has form validator attributes declared. + */ + addInlineInput:function(inputDOM){ + var newField = null,tempInput,type; + // if it is a group, only the group is added, the members belong only to the group + if (inputDOM.isGroup){ + newField = this.constructInlineGroup(inputDOM); + this.addInput(newField); + } + else{ + tempInput = this.getInput(inputDOM.id); + if (tempInput !== null && tempInput !== undefined){ + inputDOM.id = 'formvalidator:' + guid(); + } + type = Y[inputDOM.getAttribute('formvalidator:type')]; + newField = new type({ + inputDOM:inputDOM + },false); + this.addInput(newField); + newField.synchronize(inputDOM); + } + }, + /** + * This is used to check to see if an attributes has been set on the target, if + * not then the attName property is used to retreive a default value set in the Form's + * main object. + * @method checkAttribute + * @param {BaseInputField} target The input field who's attribute is being checked + * @param {string} attName Name of the attribute in the main form validator object that holds the Default value if the target's targetAttName has no value + * @param {strubg} targetAttName Name of the target's attribute that is being checked for a value + */ + checkAttribute:function(target,attName,targetAttName){ + var targetAttValue = target.get(targetAttName); + if (targetAttValue === null || targetAttValue === undefined){ + target.set(targetAttName,this.get(attName)); + } + }, + /** + * This will initialize all buttons given in the button list, well as + * any submit buttons in the form not in the exclude list + * @method initializeButtons + */ + initializeButtons:function(){ + var buttonJSON = this.get('buttonJSON'), + buttons = this.get('buttons'), + excludedButtons = this.get('excludedButtons'),i,j,submitButtons,found,buttonEl; + for (i = 0; i < buttonJSON.length; ++i){ + buttons[i] = new Y.Button(buttonJSON[i]); + buttonEl = buttons[i].get('buttonEl'); + if (buttonEl.type == 'button'){ + Y.Event.attach('click',this.submitForm,buttonEl,this,true); + } + } + // now we find all buttons in the form that are NOT excluded. + submitButtons = GETSUBMITBUTTONS(this.get('form')); + for (i = 0 ; i < submitButtons.length; ++i){ + found = false; + for (j = 0 ; j < excludedButtons.length ; ++j){ + if (excludedButtons[j] == submitButtons[i].id){ + found = true; + break; + } + } + if (!found){ + buttons[buttons.length] = new Y.Button({ + buttonEl:submitButtons[i] + }); + } + } + }, + /** + * This will call the form's submit method. If the form is not valid, the form + * will not submit. + * @method submitForm + */ + submitForm:function(){ + var form = this.get('form'); + if (form.submit !== null && form.submit !== undefined){ + form.submit(); + } + }, + /** + * This will initialize the submit and reset events on the form validator. The form + * validator will listen for these events and cancel any if need be. + * @method _initializeEvents + */ + _initializeEvents:function(){ + Y.Event.attach('submit',this._onFormSubmit,this.get('form'),this,true); + Y.Event.attach('reset',this._onFormReset,this.get('form'),this,true); + }, + /** + * This will get called when the form is submitted. This will prevent the event + * from succeeding if the form is invalid. + * @method _onFormSubmit + * @param {Event} ev Event that caused the submit + */ + _onFormSubmit:function(ev){ + var onSubmitFunc = this.get('onSubmit'), + onSubmitScope = this.get('onSubmitScope'), + rtVl = true; + if (onSubmitFunc !== null && onSubmitFunc !== undefined){ + if (onSubmitScope !== null && onSubmitScope !== undefined){ + onSubmitScope.anonymousCall = onSubmitFunc; + rtVl = onSubmitScope.anonymousCall(); + onSubmitScope.anonymousCall = null; + } + else{ + rtVl = onSubmitFunc(); + } + } + if (!this.checkFormValues()){ + ev.preventDefault(); + return; + } + else if (!rtVl){ + ev.preventDefault(); + } + this.fire(_Validator.CE_ONSUBMIT); + }, + /** + * This will get called when the form is reset, this will cause the form to recheck all it's values + * and show the proper indicators. + * @method _onFormReset + * @param {Event} ev Event that caused the reset. + */ + _onFormReset:function(ev){ + //console.debug('form reset'); + var that = this; + setTimeout(function(){ + that.checkFormValues(); + },100); + this.checkFormValues(); + }, + /** + * Called when a value in the form changes. This will determine if the submit buttons + * are enabled and disabled. + * @method onFormValueChanged + */ + onFormValueChanged:function(){ + var inputFields = this.get('inputFields'), + rtVl = true,i; + for (i = 0 ; i < inputFields.length; ++i){ + rtVl = rtVl && inputFields[i].isValid(); + } + // now if rtVl is false, we disable all buttons, otherwise, we enable all buttons. + if (rtVl){ + this.enableButtons(); + } + else{ + this.disableButtons(); + } + return rtVl; + }, + /** + * Checks the form values and makes sure the proper indicators are showing and returns + * true if the form is considered valid. + * @method checkFormValues + * @return {boolean} true if the form is valid. + */ + checkFormValues:function(){ + var inputFields = this.get('inputFields'), + rtVl = true,i; + for (i = 0 ; i < inputFields.length; ++i){ + rtVl = inputFields[i].checkIndicators() && rtVl; + } + // now if rtVl is false, we disable all buttons, otherwise, we enable all buttons. + if (rtVl){ + this.enableButtons(); + } + else{ + this.disableButtons(); + } + return rtVl; + }, + /** + * This will disable all submit buttons + * @method disableButtons + */ + disableButtons:function(){ + if (this.get('checkOnSubmit')){ + return; // don't disable buttons if its check on submit only + } + var buttons = this.get('buttons'),i; + for (i = 0 ; i < buttons.length; ++i){ + buttons[i].disable(); + } + }, + /** + * This will enable all submit buttons + * @method enableButtons + */ + enableButtons:function(){ + var buttons = this.get('buttons'),i; + for (i = 0 ; i < buttons.length; ++i){ + buttons[i].enable(); + } + } + }); + Y.Validator = _Validator; +/** + * @namespace Validator + * @requires yahoo.base, yahoo.dom, yahoo.event + * This is a button object that will represent a button that is controlled + * by the form validator. The buttons only function will be to enable and disable + * depending on the validity of the data entered on the form. + * @class Button + */ + /** + * @constructor + * This will initialize the button with the given configuration + * @param {Object} config Configuration for the button that will be applied to the properties of the button (Probably just a button el) + */ + function _Button(config){ + _Button.superclass.constructor.apply(this,arguments); + } + _Button.ATTRS = { + /** + * This is the button that will be enable/disabled by the form validator + * @property buttonEl + * @type {HTMLElement} + */ + buttonEl:{ + value:null, + setter:function(el){ + var rtVl = el; + if (YL.isString(el)){ + rtVl = Y.DOM.byId(el); + } + if (rtVl === null || rtVl === undefined){ + throw 'Invalid button: Button with id ' + el + ' does not exist'; + } + return rtVl; + } + } + }; + _Button.NAME = 'Button'; + Y.extend(_Button,Y.Base,{ + /** + * This will enable the button + * @method enable + */ + enable:function(){ + this.get('buttonEl').disabled = false; + }, + /** + * This will disable the button + * @method disable + */ + disable:function(){ + this.get('buttonEl').disabled = true; + } + }); + Y.Button = _Button; +/** + * @requires yahoo.base, yahoo.dom, yahoo.event + * @namespace Validator + * This class is what all INput fields used in the form validator will inherit from. This provides + * the basic general attributes and functions required by all input fields. + * @class BaseInputField + * @extends Base + */ + /** + * @constructor This will store all the attributes given as parameters + */ + function _BaseInputField(){ + _BaseInputField.superclass.constructor.apply(this,arguments); + this.publish(_BaseInputField.CE_ONCHANGE); + } + Y.augment(_BaseInputField, Y.EventTarget); + _BaseInputField.staticVariables = { + MAX_INTEGER:2147483647, + INTEGERREGEX:/(^-?\d\d*\.\d*$)|(^-?\d\d*$)|(^-?\.\d\d*$)/, + DOUBLEREGEX:/(^-?\d\d*\.\d+$)|(^-?\d\d*$)|(^-?\.\d\d*$)/, + EMAILREGEX:/^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/ + }; + + _BaseInputField.staticFunctions = { + /** + * This is the setter used for setting regular expression properties. IF the value is a string, + * it will parse the regular expression and return it. If it is a regex, it will simply return the + * value passed in. + * @method standardRegexSetter + * @param {String|Regex} val + * @static + */ + standardRegexSetter:function(val){ + if (YL.isString(val)){ + var valToUse = val; + if (valToUse.indexOf('/') === 0){ + valToUse = valToUse.substring(1); + } + if (valToUse.charAt(valToUse.length - 1) == '/'){ + valToUse = valToUse.substring(0,valToUse.length - 1); + } + return new RegExp(valToUse); + } + else{ + return val; + } + }, + /** + * Static function for setting an dom element as an attribute. This will + * allow that attribute to support an id or the element itself. + * @method standardElSetter + * @static + * @param {HTMLElement|String} el Id or el that is to be set for the property in question. + */ + standardElSetter:function(el){ + if (el === null || el === undefined){ + return null; + } + var rtVl = el; + if (YL.isString(el)){ + rtVl = Y.DOM.byId(el); + } + if (rtVl === null || rtVl === undefined){ + return el; + } + else{ + return rtVl; + } + }, + /** + * Static function that will set a boolean value for a property + * @method BOOLEANSETTER + * @static + * @param {boolean|string} val value of yes/no/true/false + */ + BOOLEANSETTER:function(val){ + if (YL.isBoolean(val)){ + return val; + } + else if (YL.isString(val)){ + return val.toLowerCase() == 'true'; + } + else{ + return val !== null && val !== undefined; + } + } + }; + _BaseInputField.ATTRS = { + /** + * This will be set to true if the Incorrect indicator is to be created + * upon instantiation of the input field. + * @property createIncorrectIndicator + * @type boolean + */ + createIncorrectIndicator:{ + value:false + }, + /** + * This will be set to true if the correct indicator is to be created + * upon instantiation of the input field. + * @property createCorrectIndicator + * @type boolean + */ + createCorrectIndicator:{ + value:false + }, + /** + * This is the DOM element type for the indicator. The default for + * this will be span. + * @property indicatorType + * @type string + */ + indicatorType:{ + value:null + }, + /** + * This is the css that is to be applied to the indicator. Default + * will be correctIndicator + * @property correctIndicatorCss + * @type string + */ + correctIndicatorCss:{ + value:null + }, + /** + * This is the css that is to be applied to the indicator. Default + * will be incorrectIndicator + * @property incorrectIndicatorCss + * @type string + */ + incorrectIndicatorCss:{ + value:null + }, + /** + * If set, this will be shown to indicate that the input is correct + * @property correctIndicator + * @type HTMLElement + */ + correctIndicator:{ + value:null, + setter:_BaseInputField.staticFunctions.standardElSetter + }, + /** + * This will be the text that will be used inside the DOM of the incorrect indicator. + * If none is provided, none will be used + * @property incorrectIndicatorText + * @type string + */ + incorrectIndicatorText:{ + value:null + }, + /** + * This will be the text that will be used inside the DOM of the correct indicator. + * If none is provided, none will be used + * @property correctIndicatorText + * @type string + */ + correctIndicatorText:{ + value:null + }, + /** + * If set, this will be shown to indicate that the input is incorrect + * @property incorrectIndicator + * @type HTMLElement + */ + incorrectIndicator:{ + value:null, + setter:_BaseInputField.staticFunctions.standardElSetter + }, + /** + * If set, this will be applied to the the input to signify that it is correct + * @property correctCss + * @type string + */ + correctCss:{ + value:null + }, + /** + * If set, this will be applied to the the input to signify that it is incorrect + * @property incorrectCss + * @type string + */ + incorrectCss:{ + value:null + }, + /** + * This is set to true when the input is considered disabled. False otherwise + * @property disabled + * @type boolean + */ + disabled:{ + value:false + }, + /** + * If false, this will signal that the input is considered off, and is not factored + * into the validation. + * @property isOn + * @type boolean + */ + isOn:{ + value:true + }, + /** + * If set, this will show that the input is considered optional, and if not filled + * in, won't cause the form to be invalid. + * @property optional + * @type boolean + */ + optional:{ + value:false + } + }; + _BaseInputField.NAME = 'BaseInputField'; + /** + * This is the even that is invoked when the input field is considered changed + * @event onchange + */ + _BaseInputField.CE_ONCHANGE = 'inputfield:onchanged'; + Y.extend(_BaseInputField,Y.Base,{ + /** + * To be overridden by subclasses. This will typically initialize all inidicators + * and any other initialization that is required + * @method initializeInput + * @param {Validator.Form} validator Validator to which the Base Input gets default values from. + */ + initializeInput:function(validator){}, + /** + * This will return the dom that represents the input. This will be overriden + * by all subclasses + * @method getInputDOM + * @return {HTMLElement} input dom for this field. + */ + getInputDOM:function(){return null;}, + /** + * Returns true if the given input is turned on. + * @method inputIsOn + * @return {boolean} true if the input is on. + */ + inputIsOn:function(){ + return this.get('isOn'); + }, + /** + * This will clear the input of all value. This is typically overriden + * by the subclasses + * @param {boolean} silent Set to true if the change event is NOT to be fired afterwards, as a result the form + * would not be updated to reflect any changes. + */ + clear:function(silent) {}, + isGroup:function(){return false;}, + /** + * This will set the disabled attribute to false. + * @method enable + */ + enable:function(){ + this.set('disabled',false); + }, + /** + * This will set the disabled attribute to true. + * @method disable + */ + disable:function(){ + this.set('disabled',true); + }, + /** + * This will syncronize all attributes found inline in the EL. + * Inline attributes will override JSON defined attributes. + * @method synchronize + * @param {HTMLElement} el Element that will have inline attributes pertaining to the input. + */ + synchronize:function(el){ + var attributes = this.getAttrs(false),value,key; + for (key in attributes){ + if (true){ // get rid of warning in YUI builder. + value = el.getAttribute('formvalidator:' + key); + if (value !== null && value !== undefined){ + this.set(key,value); + } + } + } + }, + /** + * Returns true if the input for the field is valid. This must + * be overridden by the subclasses. + * @method isValid + * @return {boolean} returns true if the input for the field is valid. + */ + isValid:function(){ + throw 'Plesae override the isValid function'; + }, + /** + * This will turn the input off. After this it will not be considered + * in determining if the form is valid. + * @method turnOff + */ + turnOff:function() { + this.set('isOn',false); + this._evtOnChange(); + }, + /** + * This will turn the input on. After this it WILL be considered + * in determining if the form is valid. + * @method turnOn + */ + turnOn:function() { + this.set('isOn',true); + this._evtOnChange(); + }, + /** + * This will ensure the proper css and/or dom is showing to indicate + * that the input is valid or invalid. + * @method checkIndicators + */ + checkIndicators:function(){}, + /** + * This will be overriden by subclasses, but this will hide the incorrect + * indicator and show the correct indicator if there is one. It will also + * apply the correct css to the input if there is correct css defined. + * @method showCorrectIndicator + */ + showCorrectIndicator:function(){}, + /** + * This will be overriden by subclasses, but this will hide the correct + * indicator and show the incorrect indicator if there is one. It will also + * apply the incorrect css to the input if there is incorrect css defined. + * @method showCorrectIndicator + */ + showIncorrectIndicator:function(){}, + /** + * This will be overriden by subclasses, but this will hide all indicators + * and remove all indicator css from the input. + */ + showNoIndicators:function(){}, + /** + * This function will setup the input field based on the attributes + * given in the constructor and attributes that may be inline in the DOM. + * @method setup + */ + setupIndicators:function(){ + var correctIndicator = this.get('correctIndicator'), + incorrectIndicator = this.get('incorrectIndicator'); + if (this.get('createCorrectIndicator')){ + this.set('correctIndicator',this.setupDomItem(correctIndicator,this.get('correctIndicatorText'),this.get('correctIndicatorCss'))); + } + if (this.get('createIncorrectIndicator')){ + this.set('incorrectIndicator',this.setupDomItem(incorrectIndicator,this.get('incorrectIndicatorText'),this.get('incorrectIndicatorCss'))); + } + }, + /** + * If the given EL is a string, it will create a dom object and have it inserted + * beside the input. It will then ensure that all the defaults are set on the dom. + * @method setupDomItem. + * @param {HTMLElement} el Element that will be used as an indicator. + * @param {String} html Html that will go inside the el + * @param {String} className className that is to be applied to the el. + */ + setupDomItem:function(el,html,className){ + var theDom = el; + // create the dom element, and then insert it beside the input dom. + if ((theDom === null || theDom === undefined) || YL.isString(theDom)){ + theDom = document.createElement(this.get('indicatorType')); + if (el !== null && el !== undefined){ + theDom.id = el; + } + this.insertBeside(theDom); + theDom.innerHTML = html; + } + if ((theDom.className === '' || theDom.className === null || theDom.className === undefined) && (className !== null && className !== undefined)){ + theDom.className = className; + } + return theDom; + }, + /** + * The input can optionally override this function so they can + * retrieve their particular input from the form validator. If they do + * not override this, then everyting will still work, they just won't be + * able to retreive it by name from the form validator. + * @method getId + * @return {number} the id of the input field + */ + getId:function() {return null;}, + /** + * Returns true if the input for the field is considered empty + * @method isEmpty + * @return {boolean} True if the input field is considered empty. + */ + isEmpty:function(){ + throw 'Plesae override the isEmpty function'; + }, + /** + * This will initialize the events that will notify this input if it was changed + * If target is null, then the target will be the Input Field. + * @method initializeEvents + */ + initializeEvents:function(target){}, + /** + * This will get called when the input is changed, which will in turn fire the inputChangedEvent. + * If the input is in a group, the group's event will get fired, not the field's + * @method _evtOnChange + */ + _evtOnChange:function(e){ + this.checkIndicators(); + this.fire(_BaseInputField.CE_ONCHANGE); + }, + /** + * This function will insert the given el beside the main input. This must be overriden + * in the subclasses. To implement this, simply find the parent of the current input + * starting from the body tag or the form tag and work your way down until you find it. + * @method insertBeside + * @param {HTMLElement} el DOM object to be insert beside the main input. + */ + insertBeside:function(el) {}, + /** + * Initializer, called before the class is instantiated + * @method initializer + */ + initializer:function() {}, + /** + * destructor, called after the class is destroyed + * @method destructor + */ + destructor:function(){} + }); + Y.BaseInputField = _BaseInputField; +/** + * @namespace Validator + * This class is the text base input field and will be extended by all + * Inputs who's values will be text based in anyway. Examples include a number input, + * select box or a text input. + * @class TextBaseField + * @extends BaseInputField + */ + /** + * @constructor + * This will setup the text base field with the all the settings. These settings + * are contained in the config parameter + * @param {Object} config Configuratino settings for the input. + * @param {boolean} initialize Flag that says whether or not to call the initialize input function + */ + function _TextBaseField(config,initialize){ + _TextBaseField.superclass.constructor.apply(this,arguments); + // next the correct and incorrect indicators need to be setup. + if (initialize){ + this.initializeInput(); + } + this.get('textType'); + } + _TextBaseField.ATTRS = { + /** + * This will state the maximum length of the string in the input. This + * value is 255 by default. + * @property maxLength + * @type Number + */ + maxLength:{ + value:255, + setter:function(val){ + if (val < 0){ + return 255; + } + else{ + return val; + } + } + }, + /** + * Formatter used for formatting the result typed into the text field. + * This can be a function that takes the input text and returns it in a + * desired format, or it could be an object that has a format function. + * @property formatter + * @type {Function|Object} + */ + formatter:{ + value:null, + setter:function(val){ + if (val === null || val === undefined){ + return null; + } + if (YL.isFunction(val)){ + return val; + } + else if (YL.isObject(val)){ + if (val.format === null || val === undefined){ + throw 'Formatter object must have a formatter function'; + } + return val; + } + else{ + throw 'Formatter must be an object or a function'; + } + } + }, + /** + * Regular expression the input must match in order for the input to be correct. + * If set to null, this is ignored in the isValid function. + * @property regex + * @type regex + */ + regex:{ + value:null, + setter:Y.BaseInputField.staticFunctions.standardRegexSetter + }, + /** + * This property is optional for those who wish to use the pre-canned regular expressions. + * This can be set to any of the following types
        + *
          + *
        • Email
        • + *
        • Phone
        • + *
        • CreditCard
        • + *
        • Zipcode
        • + *
        • Postalcode
        • + *
        + * @property textType + * @type string + */ + textType:{ + lazy:false, + value:null, + setter:function(val){ + if (val === null || val === undefined){ + return null; + } + else if (val.toLowerCase() == 'email'){ + this.set('regex',Y.BaseInputField.staticVariables.EMAILREGEX); + } + else if (val.toLowerCase() == 'phone'){ + this.set('regex',/^([(]?[2-9]\d{2}[)]?)[ ]*-?[ ]*(\d{3})[ ]*-?[ ]*(\d{4})$/); + } + else if (val.toLowerCase() == 'creditcard'){ + this.set('regex',/[0-9]{4} {0,1}[0-9]{4} {0,1}[0-9]{4} {0,1}[0-9]{4}/); + } + else if (val.toLowerCase() == 'zipcode'){ + this.set('regex',/^(\d{5})([\s]*-[\s]*\d{4})?$/); + } + else if (val.toLowerCase() == 'postalcode'){ + this.set('regex',/^[a-zA-Z]{1}[0-9]{1}[a-zA-Z]{1}[\s]*[0-9]{1}[a-zA-Z]{1}[0-9]{1}$/); + } + return val; + } + }, + /** + * This is the main input DOM that the validator will check input on. + * @property inputDOM + * @type HTMLElement + */ + inputDOM:{ + value:null, + setter:Y.BaseInputField.staticFunctions.standardElSetter + } + }; + _TextBaseField.NAME = 'TextBaseField'; + Y.extend(_TextBaseField,Y.BaseInputField,{ + /** + * This will setup the indicators for the input + * @method initializeInput + */ + initializeInput:function(){ + this.setupIndicators(); + }, + /** + * This will reset the text field to '' + * @method clear + * @param {boolean} silent Set to true if you do not want the clear to invoke a form validator change event. + */ + clear:function(silent){ + this.get('inputDOM').value = ''; + if (silent !== true){ + this._evtOnChange(); + } + }, + /** + * This will return the dom that represents the input. + * @method getInputDOM + * @return {HTMLElement} input dom for this field. + */ + getInputDOM:function(){return this.get('inputDOM');}, + /** + * This will disable the input. + * @method disable + */ + disable:function(){ + _TextBaseField.superclass.disable.call(this); + this.get('inputDOM').disabled = true; + }, + /** + * This will enable the input. + * @method enable + */ + enable:function(){ + _TextBaseField.superclass.enable.call(this); + this.get('inputDOM').disabled = false; + }, + /** + * Returns true only if the input is not empty, and it is not longer than the maximum length setting + * @method isValid + * @return {boolean} true if the input is not empty, and it is not longer than the maximum length setting + */ + isValid:function(){ + if (!this.get('isOn')){ + return true; // is always valid if off + } + if (this.get('optional') && this.isEmpty()){ + return true; + } + var regex = this.get('regex'), + value = this.get('inputDOM').value; + if ((regex !== null && regex !== undefined) && (!regex.test(value))){ + return false; // return false if the value does not match the format of the set regular expression + } + return !this.isEmpty() && value.length <= this.get('maxLength'); + }, + /** + * Returns the id of the input dom + * @method getId + * @return {string} id of the input dom. + */ + getId:function(){ + return this.get('inputDOM').id; + }, + /** + * Returns true if the input dom has an empty string value. + * @method isEmpty + * @return {boolean} true if the input is not '' + */ + isEmpty:function(){ + return (this.get('inputDOM').value === ''); + }, + /** + * This will ensure the input is formatted as desired using the formatter, but only + * if the input is valid. If it does not match the regular expression, this will not call + * the format method/object's format method. + * @method checkFormat + */ + checkFormat:function(){ + if (!this.isValid()){ + return; // input has to be valid first + } + if (!this.inputIsOn()){ + return; // if its off, who cares + } + var formatter = this.get('formatter'),inputDOM; + if (formatter === null || formatter === undefined){ + return; + } + inputDOM = this.get('inputDOM'); + if (YL.isFunction(formatter)){ + inputDOM.value = formatter(inputDOM.value); + } + else{ + inputDOM.value = formatter.format(inputDOM.value); + } + }, + /** + * This will ensure the proper css and/or dom is showing to indicate + * that the input is valid or invalid. + * @method checkIndicators + * @return {boolean} True if input is valid + */ + checkIndicators:function(){ + if (!this.inputIsOn()){ + this.showNoIndicators(); + return true; + } + else if (this.get('optional') && this.isEmpty()){ + this.showNoIndicators(); + return true; + } + else if (this.isValid()){ + this.showCorrectIndicator(); + this.checkFormat(); + return true; + } + else{ + this.showIncorrectIndicator(); + return false; + } + }, + /** + * This will ensure that the incorrect indicator is hidden and the incorrect css is not used, and will + * ensure that the correct indicator is showing, and the correct css is applied. + * @method showCorrectIndicator + */ + showCorrectIndicator:function(){ + var inputDom = this.get('inputDOM'), + correctIndicator = this.get('correctIndicator'), + incorrectIndicator = this.get('incorrectIndicator'); + + this.checkFormat(); + Y.DOM.removeClass(inputDom,this.get('incorrectCss')); + Y.DOM.addClass(inputDom,this.get('correctCss')); + if (correctIndicator !== null && correctIndicator !== undefined){ + correctIndicator.style.display = ''; + } + if (incorrectIndicator !== null && incorrectIndicator !== undefined){ + incorrectIndicator.style.display = 'none'; + } + }, + /** + * This will ensure that the correct indicator is hidden and the correct css is not used, and will + * ensure that the incorrect indicator is showing, and the correct css is applied. + * @method showCorrectIndicator + */ + showIncorrectIndicator:function(){ + var inputDom = this.get('inputDOM'), + correctIndicator = this.get('correctIndicator'), + incorrectIndicator = this.get('incorrectIndicator'); + Y.DOM.addClass(inputDom,this.get('incorrectCss')); + Y.DOM.removeClass(inputDom,this.get('correctCss')); + if (correctIndicator !== null && correctIndicator !== undefined){ + correctIndicator.style.display = 'none'; + } + if (incorrectIndicator !== null && incorrectIndicator !== undefined){ + incorrectIndicator.style.display = ''; + } + }, + /** + * This will ensure NO indicators are showing. + * method @showNoIndicators + */ + showNoIndicators:function(){ + var inputDom = this.get('inputDOM'), + correctIndicator = this.get('correctIndicator'), + incorrectIndicator = this.get('incorrectIndicator'); + Y.DOM.removeClass(inputDom,this.get('incorrectCss')); + Y.DOM.removeClass(inputDom,this.get('correctCss')); + if (correctIndicator !== null && correctIndicator !== undefined){ + correctIndicator.style.display = 'none'; + } + if (incorrectIndicator !== null && incorrectIndicator !== undefined){ + incorrectIndicator.style.display = 'none'; + } + }, + /** + * This function will insert the given el beside the inputDOM. + * @method insertBeside + * @param {HTMLElement} el DOM object to be insert beside the main input. + */ + insertBeside:function(el) { + Y.DOM.insertAfter(el,this.get('inputDOM')); + }, + /** + * This will attach the keyup event to the input dom. + * @method initializeEvents + * @param {HTMLElement} target The Object that will be listening to the key up and blur events of the input DOM. + */ + initializeEvents:function(target){ + var theTarget = target; + if (theTarget === null || theTarget === undefined){ + theTarget = this; + } + Y.Event.attach('keyup',theTarget._evtOnChange,this.get('inputDOM'),theTarget,true); + Y.Event.attach('blur',theTarget._evtOnChange,this.get('inputDOM'),theTarget,true); + } + }); + Y.TextBaseField = _TextBaseField; +/** + * @requires yahoo.base, yahoo.dom, yahoo.event + * @namespace Validator + * This will represent a checkbox field in the form validator. Checkbox + * field can be put into a group based field or left on its' own. + * + * @class CheckboxField + * @extends BaseInputField + */ + /** + * @constructor + * This will initialize the element with the given configuration. Most of this will + * be passed to the BaseInputField. + * @param {Object} config Configuration for the checkbox field. Probably just the validWhenChecked property + */ + function _CheckboxField(config){ + _CheckboxField.superclass.constructor.apply(this,arguments); + } + _CheckboxField.ATTRS = { + /** + * The dom that represents the checkbox + * @property inputDOM + * @type HTMLElement + */ + inputDOM:{ + value:null, + setter:Y.BaseInputField.staticFunctions.standardElSetter + }, + /** + * If set to true, this checkbox will be considered valid if checked + * @property validWhenChecked + * @type boolean + */ + validWhenChecked:{ + value:true + } + }; + _CheckboxField.NAME = 'CheckboxField'; + Y.extend(_CheckboxField,Y.BaseInputField,{ + /** + * Sets up the indicators + * @method initializeInput + */ + initializeInput:function(){ + this.setupIndicators(); + //this.initializeEvents(); + }, + /** + * This will return the dom that represents the input. + * @method getInputDOM + * @return {HTMLElement} input dom for this field. + */ + getInputDOM:function(){return this.get('inputDOM');}, + /** + * This will set the checkbox back to unchecked if checked is valid, and checked, if unchecked is valid. + * @method clear + */ + clear:function(silent){ + this.get('inputDOM').checked = !this.get('validWhenChecked'); + if (silent !== true){ + this._evtOnChange(); + } + }, + /** + * This disables the checkbox + * @method disable + */ + disable:function(){ + _CheckboxField.superclass.disable.call(this); + this.get('inputDOM').disabled = true; + }, + /** + * This enables the checkbox + * @method enable + */ + enable:function(){ + _CheckboxField.superclass.enable.call(this); + this.get('inputDOM').disabled = false; + }, + /** + * Returns true only if the input is not empty, and it is not longer than the maximum length setting + * @method isValid + * @return {boolean} true if the input is not empty, and it is not longer than the maximum length setting + */ + isValid:function(){ + if (!this.get('isOn')){ + return true; // is always valid if off + } + var validWhenChecked = this.get('validWhenChecked'), + checked = this.get('inputDOM').checked; + return (validWhenChecked && checked) || (!validWhenChecked && !checked); + }, + /** + * Returns the id of the group based field. + * @method getId + * @return {String} id of the checkbox dom. + */ + getId:function(){ + return this.get('inputDOM').id; + }, + /** + * Returns the true if the checkbox input is invalid. + * @method isEmpty + * @return {boolean} true if the checkbox is not valid. + */ + isEmpty:function(){ + return !this.isValid(); + }, + /** + * This will ensure no indicators are showing, or css applied to the input that + * would signify correctness or incorrectness. + * @method showNoIndicators + */ + showNoIndicators:function(){ + var dom = this.get('inputDOM'), + correctIndicator = this.get('correctIndicator'), + incorrectIndicator = this.get('incorrectIndicator'); + + Y.DOM.removeClass(dom,this.get('incorrectCss')); + Y.DOM.removeClass(dom,this.get('correctCss')); + if (correctIndicator !== null && correctIndicator !== undefined){ + correctIndicator.style.display = 'none'; + } + if (incorrectIndicator !== null && incorrectIndicator !== undefined){ + incorrectIndicator.style.display = 'none'; + } + }, + /** + * This will ensure the proper css and/or dom is showing to indicate + * that the input is valid or invalid. + * @method checkIndicators + * @return {boolean} True if input is valid + */ + checkIndicators:function(){ + var dom = this.get('inputDOM'), + correctIndicator = this.get('correctIndicator'), + incorrectIndicator = this.get('incorrectIndicator'); + if (!this.get('isOn')){ + this.showNoIndicators(); + return this.isValid(); + } + else if (this.isValid()){ + if (dom !== null && dom !== undefined){ + Y.DOM.removeClass(dom,this.get('incorrectCss')); + Y.DOM.addClass(dom,this.get('correctCss')); + } + if (correctIndicator !== null && correctIndicator !== undefined){ + correctIndicator.style.display = ''; + } + if (incorrectIndicator !== null && incorrectIndicator !== undefined){ + incorrectIndicator.style.display = 'none'; + } + return true; + } + else{ + if (dom !== null && dom !== undefined){ + Y.DOM.addClass(dom,this.get('incorrectCss')); + Y.DOM.removeClass(dom,this.get('correctCss')); + } + if (correctIndicator !== null && correctIndicator !== undefined){ + correctIndicator.style.display = 'none'; + } + if (incorrectIndicator !== null && incorrectIndicator !== undefined){ + incorrectIndicator.style.display = ''; + } + return false; + } + }, + /** + * Inidicators are usually applicable to checkboxes, so creating them dynamically doesn't + * make much sense, this method does nothing. + * @method insertBeside + * @param {HTMLElement} el DOM object to be insert beside the main input. + */ + insertBeside:function(el) { + }, + /** + * This will initialize the checkbox so the form status is updated when the checkbox is clicked. + * @method initializeEvents + * @param {HTMLElement} target The object that will be listening to the events of the click event of the input DOM + */ + initializeEvents:function(target){ + var theTarget = target; + if (theTarget === null || theTarget === undefined){ + theTarget = this; + } + Y.Event.attach('click',theTarget._evtOnChange,this.get('inputDOM'),theTarget,true); + } + }); + Y.CheckboxField = _CheckboxField; +/** + * @requires yahoo.base, yahoo.dom, yahoo.event + * @namespace Validator + * This is a catch all class for types of input that do not fit the existing input types. + * @class CustomField + * @extends BaseInputField + */ + /** + * @constructor + * Takes the given configuration and initializes the input field's properties. + */ + function _CustomField(config){ + _CustomField.superclass.constructor.apply(this,arguments); + } + _CustomField.ATTRS = { + /** + * This will be an object that can optionally implement all the + * functions used by an input field. This can also be a function, which + * retrieves the object, or a string, which can be the name of an instance + * object or function call to retreive it. + * @property emptyValue + * @type {Object} + */ + validatorObject:{ + setter:function(val){ + if (val === null || val === undefined){ + throw 'You must provide a validator object to the custom input'; + } + var rtVl = null; + if (YL.isString(val)){ + rtVl = validatorGlobal[val];//eval(val); + } + else if (YL.isFunction(val)){ + rtVl = val(); + } + else if (YL.isObject(val)){ + rtVl = val; + } + + if (rtVl === null || rtVl === undefined){ + throw 'Your validator object must be a object'; + } + else{ + return rtVl; + } + } + }, + /** + * Property that can be optional set for the custom input for looking up the object + */ + id:{ + value:null + } + }; + _CustomField.NAME = 'CustomField'; + Y.extend(_CustomField,Y.BaseInputField,{ + /** + * This will ensure the proper dom is showing to indicate + * that the input is valid or invalid. + * @method checkIndicators + * @return {boolean} True if input is valid + */ + checkIndicators:function(){ + var correctIndicator = this.get('correctIndicator'), + incorrectIndicator = this.get('incorrectIndicator'); + if (this.isValid()){ + if (correctIndicator !== null && correctIndicator !== undefined){ + correctIndicator.style.display = ''; + } + if (incorrectIndicator !== null && incorrectIndicator !== undefined){ + incorrectIndicator.style.display = 'none'; + } + return true; + } + else{ + if (correctIndicator !== null && correctIndicator !== undefined){ + correctIndicator.style.display = 'none'; + } + if (incorrectIndicator !== null && incorrectIndicator !== undefined){ + incorrectIndicator.style.display = ''; + } + return false; + } + }, + /** + * If the id is set, that value will be returned. If not, then it will + * see if the validator object + * @method getId + * @return {String} id The id that was set to the custom validator on initialization. + */ + getId:function(){ + var rtVl = this.get('id'); + if (rtVl === null || rtVl === undefined){ + rtVl = this.executeFunction('getId',null); + } + return rtVl; + }, + /** + * This will execute the function with the specified name on the validator object + * @method executeFunction + * @private + * @return {Object} value returned from the function call + */ + executeFunction:function(name,defaultReturn){ + var obj = this.get('validatorObject'); + if (YL.isFunction(obj[name])){ + return obj[name](); + } + return defaultReturn; + }, + /** + * This will execute the function with the given name with the assumption it returns nothing + * @method executeVoidFunction + * @private + */ + executeVoidFunction:function(name){ + var obj = this.get('validatorObject'); + if (YL.isFunction(obj[name])){ + obj[name](); + } + }, + /** + * Calls the disable function on the custom validator object if it exists. + * @method disable + */ + disable:function(){ + _CustomField.superclass.disable.call(this); + this.executeVoidFunction('disable'); + }, + /** + * Calls the enable function on the custom validator object if it exists. + * @method enable + */ + enable:function(){ + _CustomField.superclass.enable.call(this); + this.executeVoidFunction('enable'); + }, + /** + * Calls the turnOff function on the custom validator object if it exists. + * @method turnOff + */ + turnOff:function(){ + _CustomField.superclass.turnOff.call(this); + this.executeVoidFunction('turnOff'); + }, + /** + * Calls the turnOn function on the custom validator object if it exists. + * @method turnOn + */ + turnOn:function(){ + _CustomField.superclass.turnOn.call(this); + this.executeVoidFunction('turnOn'); + }, + /** + * Calls the clear function on the custom validator object if it exists. + * @method clear + * @param {boolean} silent True if the clear action will not invoke an on change event. + */ + clear:function(silent){ + var obj = this.get('validatorObject'); + if (YL.isFunction(obj.clear)){ + obj.clear(silent); + } + if (silent !== true){ + this._evtOnChange(); + } + }, + /** + * Executs the custom validator object's isEmpty function + * @return {boolean} returns what the validator object's empty function returns. + */ + isEmpty:function(){ + return this.executeFunction('isEmpty',false); + }, + /** + * Returns true if the value selected in the dom doesn't match the value + * set for the emptyValue property. + * @return {boolean} false if the value in the select input matches the specified empty value + */ + isValid:function(){ + return this.executeFunction('isValid',false); + }, + /** + * This will call the insertBeside function on the validator Object if it exists. It will + * pass the dom object that is to be inserted. + * @method insertBeside + * @param {HTMLElement} el DOM object to be insert beside the main input. + */ + insertBeside:function(el) { + var obj = this.get('validatorObject'); + if (YL.isFunction(obj.insertBeside)){ + obj.insertBeside(el); + } + } + }); + Y.CustomField = _CustomField; +/** + * @requires yahoo.base, yahoo.dom, yahoo.event + * @namespace Validator + * This input field is for text input of doubles or floats. + * @class DoubleField + * @extends TextBaseField + */ + /** + * @constructor + * This will initialize the double field, and its' base fields properties. + * @param {Object} config Configuration JSON. + */ + function _DoubleField(config){ + _DoubleField.superclass.constructor.apply(this,arguments); + this.set('regex',Y.BaseInputField.staticVariables.DOUBLEREGEX); + } + _DoubleField.ATTRS = { + /** + * This is set to true if the minimum allowed values boundary is inclusive + * @property minInclusive + * @type boolean + */ + minInclusive:{ + value:true, + setter:Y.BaseInputField.staticFunctions.BOOLEANSETTER + }, + /** + * This is set to true if the maximum allowed values boundary is inclusive + * @property maxInclusive + * @type boolean + */ + maxInclusive:{ + value:true, + setter:Y.BaseInputField.staticFunctions.BOOLEANSETTER + }, + /** + * This is the minimum allowed value in the double field. Default value + * is the minimum value for an integer + * @property min + * @type number + */ + min:{ + value:0, + setter:function(val){ + var rtVl = val; + if (!YL.isNumber(rtVl)){ + rtVl = parseFloat(rtVl); + } + if (!YL.isNumber(rtVl)){ + throw 'Invalid value given for min: ' + val; + } + if (rtVl < (-1)*Y.BaseInputField.staticVariables.MAX_INTEGER){ + return (-1)*Y.BaseInputField.staticVariables.MAX_INTEGER; + } + return rtVl; + } + }, + /** + * This is the maximum allowed value in the double field. Default value + * is the maximum value for an integer + * @property max + * @type number + */ + max:{ + value:Y.BaseInputField.staticVariables.MAX_INTEGER, + setter:function(val){ + var rtVl = val; + if (!YL.isNumber(rtVl)){ + rtVl = parseFloat(rtVl); + } + if (!YL.isNumber(rtVl)){ + throw 'Invalid value given for max: ' + val; + } + if (rtVl > Y.BaseInputField.staticVariables.MAX_INTEGER){ + return Y.BaseInputField.staticVariables.MAX_INTEGER; + } + return rtVl; + } + }, + /** + * If set, this will restrict the number of decimal places allowed on the double. This could + * be done with regular expression, but this makes it a bit easier for everyone. + * @property maxDecimalPlaces + * @type number + */ + maxDecimalPlaces:{ + value:-1, + setter:function(val){ + var rtVl = val; + if (!YL.isNumber(rtVl)){ + rtVl = parseInt(rtVl,10); + } + if (!YL.isNumber(rtVl)){ + throw 'Invalid value given for decimal places: ' + val; + } + else{ + return val; + } + } + } + }; + _DoubleField.NAME = 'DoubleField'; + Y.extend(_DoubleField,Y.TextBaseField,{ + /** + * This will return true if the input matches the double regular expression + * and, if max decimals are set, the number of decimals places. + * @method isValid + * @return {boolean} true if the input is a valid double. + */ + isValid:function(){ + if (!_DoubleField.superclass.isValid.call(this)){ + return false; // return false if it doesn't match the double regex + } + else if (this.get('optional') && this.isEmpty()){ + return true; + } + var value = this.get('inputDOM').value,numVal = 0,minInclusive,maxInclusive,min,max,decimals, + maxDecimals = this.get('maxDecimalPlaces'); + if ((maxDecimals != -1) && (value.indexOf('.') != -1)){ + decimals = value.split('.')[1]; + if (decimals.length > maxDecimals) { + return false; + } + } + try{numVal = parseFloat(value,10);} + catch(e){return false;} + + if (numVal.toString() === null || numVal.toString() === undefined){ + return false; + } + if (numVal.toString().toLowerCase() == 'nan'){ + return false; + } + + minInclusive = this.get('minInclusive'); + maxInclusive = this.get('maxInclusive'); + min = this.get('min'); + max = this.get('max'); + + if (minInclusive && (min > numVal)){ + return false; + } + else if (!minInclusive && (min >= numVal)){ + return false; + } + else if (maxInclusive && (max < numVal)){ + return false; + } + else if (!maxInclusive && (max <= numVal)){ + return false; + } + else{ + return true; + } + } + }); + Y.DoubleField = _DoubleField; +/** + * @requires yahoo.base, yahoo.dom, yahoo.event + * @namespace Validator + * This input field is for text input of whole numbers. + * @class IntegerField + * @extends DoubleField + */ + /** + * @constructor + * This will initialize the integer field, and its' base fields properties. This + * will also set the integer regular expression in the Base class. This will + * require all inputs to have an integer format. + * @param {Object} config Configuration JSON. + */ + function _IntegerField(config){ + _IntegerField.superclass.constructor.apply(this,arguments); + this.set('regex',Y.BaseInputField.staticVariables.INTEGERREGEX); + } + _IntegerField.ATTRS = {}; + _IntegerField.NAME = 'IntegerField'; + Y.extend(_IntegerField,Y.DoubleField,{ + /** + * This method returns true if the input in the input DOM's value matches + * the format required for an integer. + * @method isValid + * @return {boolean} true if the field is a valid integer + */ + isValid:function(){ + if (!_IntegerField.superclass.isValid.call(this)){ + return false; // return false if it doesn't match the double regex + } + else if (this.get('optional') && this.isEmpty()){ + return true; + } + + var value = this.get('inputDOM').value,theVal = 0; + if ( value.indexOf( '.' ) != -1 ){ + return false; // don't allow numbers with decimals + } + try{theVal = parseInt(value,10);} + catch(e){return false;} + + if ( theVal.toString().toLowerCase() == 'nan' ){ + return false; + } + else{ + return true; + } + } + }); + Y.IntegerField = _IntegerField; +/** + * @requires yahoo.base, yahoo.dom, yahoo.event + * @namespace Validator + * This class is the text base input field and will be extended by all + * Inputs who's values will be text based in anyway. Examples include a number input, + * select box or a text input. + * @class GroupBaseField + * @extends BaseInputField + */ + /** + * @constructor + * This will initialize the group with the given configuration json object. + * @param {Object} config JSON configuration object containing the properties of the GroupBaseField + */ + function _GroupBaseField(config){ + _GroupBaseField.superclass.constructor.apply(this,arguments); + } + _GroupBaseField.ATTRS = { + /** + * This is the dom that contains the group's child elements. This is optional + * as the group's child inputs do not have to be contained in a dom element + * @property groupDOM + * @type HTMLElement + */ + groupDOM:{ + value:null, + setter:Y.BaseInputField.staticFunctions.standardElSetter + }, + /** + * This will contain the raw json of the input fields that will belong + * to the group input. + * @property membersJSON + * @type Object[] + */ + membersJSON:{ + value:[] + }, + /** + * When the member inputs are process and instantiated they are placed + * in this collection where they are persisted. + * @property members + * @type BaseInputField[] + */ + members:{ + value:[], + setter:function(val){ + if (YL.isArray(val)){ + return val; + } + else{ + throw 'The members property of a group must be an array'; + } + } + }, + /** + * Minimum number of inputs that need to be properly filled in for this to be valid. + * 0 to state that none have to be filled in. If not set, this is property is not used. + * @property minValid + * @type {number} + */ + minValid:{ + value:null, + setter:function(val){ + if (val === null || val === undefined){ + return null; + } + var theVal = val,max; + if (!YL.isNumber(theVal)){ + theVal = parseInt(theVal,10); + } + if (theVal < 1){ + throw 'The minimum must be greater than 1'; + } + + max = this.get('maxValid'); + if ((max !== null && max !== undefined) && (theVal > max)){ + throw 'Minimum must be less than or equal to maximum'; + } + else{ + return theVal; + } + } + }, + /** + * Maximum nuymber of fields that can be filled in. Anymore (valid or not) and the group is invalid. + * If not set, this is property is not used. + * @property maxValid + * @type number + */ + maxValid:{ + value:null, + setter:function(val){ + if (val === null || val === undefined){ + return null; + } + if (val < 1){ + throw 'The maximum must be greater than 1'; + } + var min = this.get('minValid'); + if ((min !== null && min !== undefined) && (val < min)){ + throw 'Maximum must be greater than or equal to minimum'; + } + else{ + return val; + } + } + }, + /** + * Property that can be optional set for the custom input for looking up the object + * @property id + * @type string + */ + id:{ + value:null + } + }; + _GroupBaseField.NAME = 'GroupBaseField'; + Y.extend(_GroupBaseField,Y.BaseInputField,{ + /** + * Indicator function to help the form validator deal with the regular inputs and group + * inputs differently on some occasions. + * @method isGroup + * @return boolean True all the time, as this is infact a group + */ + isGroup:function(){return true;}, + /** + * This will return the dom that represents the input. + * @method getInputDOM + * @return {HTMLElement} input dom for this field. + */ + getInputDOM:function(){return this.get('groupDOM');}, + /** + * This will return any member input with the given id. + * @method getInput + * @param {string} id Id of an input + * @return {BaseInputField} The field with the given id, null if no such field exists + */ + getInput:function(id){ + var members = this.get('members'),rtVl,i; + for (i = 0; i < members.length; ++i){ + if (members[i].getId() == id){ + return members[i]; + } + if (members[i].isGroup()){ + rtVl = members[i].getInput(id); + if (rtVl !== null && rtVl !== undefined){ + return rtVl; + } + } + } + return null; + }, + /** + * This will call the clear method (in silent mode) on all member inputs. + * If silent is true, it will then fire its own on change event. + * @method clear + * @param {boolean} silent True if this will call the on change event + */ + clear:function(silent){ + var members = this.get('members'),i; + for (i = 0; i < members.length; ++i){ + members[i].clear(true); + } + if (silent !== true){ + this._evtOnChange(); + } + }, + /** + * This will disable all the inputs in the group + * @method disable + */ + disable:function(){ + _GroupBaseField.superclass.disable.call(this); + var members = this.get('members'),i; + for (i = 0; i < members.length; ++i){ + members[i].disable(); + } + }, + /** + * This will enable all the inputs in the group + * @method enable + */ + enable:function(){ + _GroupBaseField.superclass.enable.call(this); + var members = this.get('members'),i; + for (i = 0; i < members.length; ++i){ + members[i].enable(); + } + }, + /** + * This will turn on all the members of the group, including the group as well + * @method turnOn + */ + turnOn:function(){ + _GroupBaseField.superclass.turnOn.call(this); + var members = this.get('members'),i; + for (i = 0; i < members.length; ++i){ + members[i].turnOn(); + } + this.checkIndicators(); + }, + /** + * This will turn off all the members of the group, including the group as well + * @method turnOff + */ + turnOff:function(){ + _GroupBaseField.superclass.turnOff.call(this); + var members = this.get('members'),i; + for (i = 0; i < members.length; ++i){ + members[i].turnOff(); + } + }, + /** + * This will take the members json and initialize the inputs. Validator + * is required for setting up the field based on settings given to the + * main form validator. + * @method initializeInput + * @param {Validator} validator The validator this input gets it's default values from. + */ + initializeInput:function(validator){ + var membersJSON = this.get('membersJSON'), + members = this.get('members'),newField,i; + for (i = 0 ; i < membersJSON.length; ++i){ + newField = new membersJSON[i].type(membersJSON[i].atts,false); + validator.setupInput(newField); + members[members.length] = newField; + } + this.setupIndicators(); + this.checkIndicators(); + }, + /** + * This will add the given field to the group as an input. + * @method addInput + * @param {BaseInputField} newField The new field to be added to the group. + */ + addInput:function(newField){ + var members = this.get('members'); + members[members.length] = newField; + }, + /** + * Returns true only if the input is not empty, and it is not longer than the maximum length setting + * @method isValid + * @return {boolean} true if the input is not empty, and it is not longer than the maximum length setting + */ + isValid:function(){ + if (this.get('optional') && this.isEmpty()){ + return true; + } + var members = this.get('members'), + numValid = 0, + groupOn = this.get('isOn'),rtVl = true,empty,valid,minValid,maxValid,i; + for (i = 0; i < members.length; ++i){ + empty = members[i].isEmpty(); + valid = members[i].isValid(); + + if (!empty && valid){ + numValid++; + } + else if (!empty){ + return !groupOn; // if not empty, and not valid then the whole group is invalid (only if the group is on + } + } + if (groupOn){ + minValid = this.get('minValid'); + maxValid = this.get('maxValid'); + if (minValid !== null && minValid !== undefined){ + rtVl = minValid <= numValid; + } + if (maxValid !== null && maxValid !== undefined){ + rtVl = (maxValid >= numValid) && rtVl; + } + } + return rtVl; + }, + /** + * Returns the id of the group based field. + * @method getId + * @return {string} id of the group base field if the id property was set, null otherwise. + */ + getId:function(){ + var id = this.get('id'),groupDOM; + if (id !== null && id !== undefined){ + return id; + } + groupDOM = this.get('groupDOM'); + if (groupDOM !== null && groupDOM !== undefined){ + return groupDOM.id; + } + else{ + return null; + } + }, + /** + * Returns true all the members of the group are empty + * @method isEmpty + * @return {boolean} true if all members of the group are empty + */ + isEmpty:function(){ + var members = this.get('members'),i; + for (i = 0; i < members.length; ++i){ + if (!members[i].isEmpty()){ + return false; + } + } + return true; + }, + /** + * This will show the correct indicator, and apply the correct css to the input + * if they are set for the group. It will also ensure the incorrect indicator + * is not showing, and incorrect css is not applied. + * @method showCorrectIndicator + */ + showCorrectIndicator:function(){ + var groupDom = this.get('groupDOM'), + correctIndicator = this.get('correctIndicator'), + incorrectIndicator = this.get('incorrectIndicator'); + if (groupDom !== null && groupDom !== undefined){ + Y.DOM.removeClass(groupDom,this.get('incorrectCss')); + Y.DOM.addClass(groupDom,this.get('correctCss')); + } + if (correctIndicator !== null && correctIndicator !== undefined){ + correctIndicator.style.display = ''; + } + if (incorrectIndicator !== null && incorrectIndicator !== undefined){ + incorrectIndicator.style.display = 'none'; + } + }, + /** + * This will show the incorrect indicator, and apply the incorrect css to the input + * if they are set for the group. It will also ensure the correct indicator + * is not showing, and correct css is not applied. + * @method showIncorrectIndicator + */ + showIncorrectIndicator:function(){ + var groupDom = this.get('groupDOM'), + correctIndicator = this.get('correctIndicator'), + incorrectIndicator = this.get('incorrectIndicator'); + if (groupDom !== null && groupDom !== undefined){ + Y.DOM.addClass(groupDom,this.get('incorrectCss')); + Y.DOM.removeClass(groupDom,this.get('correctCss')); + } + if (correctIndicator !== null && correctIndicator !== undefined){ + correctIndicator.style.display = 'none'; + } + if (incorrectIndicator !== null && incorrectIndicator !== undefined){ + incorrectIndicator.style.display = ''; + } + }, + /** + * This will ensure all indicators are not showing, and no indicator css + * is applied to the input + * @method showNoIndicators + */ + showNoIndicators:function(){ + var groupDom = this.get('groupDOM'), + correctIndicator = this.get('correctIndicator'), + incorrectIndicator = this.get('incorrectIndicator'); + if (groupDom !== null && groupDom !== undefined){ + Y.DOM.removeClass(groupDom,this.get('incorrectCss')); + Y.DOM.removeClass(groupDom,this.get('correctCss')); + } + if (correctIndicator !== null && correctIndicator !== undefined){ + correctIndicator.style.display = 'none'; + } + if (incorrectIndicator !== null && incorrectIndicator !== undefined){ + incorrectIndicator.style.display = 'none'; + } + }, + /** + * This will ensure the proper css and/or dom is showing to indicate + * that the input is valid or invalid. This will also ensure that the + * right indicators are showing on the children of the group. Any change + * in a member of the group will cause this method in the group to be executed. + * @method checkIndicators + * @return {boolean} True if input is valid + */ + checkIndicators:function(){ + var members = this.get('members'), + numValid = 0, + oneInvalid = false, + groupOn = this.get('isOn'), + empty,i,valid,rtVl = true,minValid,maxValid; + for (i = 0; i < members.length; ++i){ + empty = members[i].isEmpty(); + valid = members[i].isValid(); + if (!groupOn){ + members[i].showNoIndicators(); + } + else if (this.get('optional') && this.isEmpty()){ + members[i].showNoIndicators(); + } + else if (!empty && valid){ + numValid++; + members[i].showCorrectIndicator(); + } + else if (!empty){ + members[i].showIncorrectIndicator(); + oneInvalid = true; + } + else{ + members[i].showNoIndicators(); + } + } + // if one was not empty and invalid, then return false; + if (!oneInvalid){ + minValid = this.get('minValid'); + maxValid = this.get('maxValid'); + if (minValid !== null && minValid !== undefined){ + rtVl = minValid <= numValid; + } + if (maxValid !== null && maxValid !== undefined){ + rtVl = (maxValid >= numValid) && rtVl; + } + } + else{ + rtVl = false; + } + if (!groupOn){ + this.showNoIndicators(); + return true; + } + else if (this.get('optional') && this.isEmpty()){ + this.showNoIndicators(); + return true; + } + else if (rtVl){ + this.showCorrectIndicator(); + return true; + } + else{ + this.showIncorrectIndicator(); + return false; + } + }, + /** + * Indicators are not created dynamically for groups at the moment. + * @method insertBeside + * @param {HTMLElement} el DOM object to be insert beside the main input. + */ + insertBeside:function(el) { + }, + /** + * This will go through each member and initialize their event. + * @method initializeEvents + * @param {Object} target The target that will be listening to the events of the members. + */ + initializeEvents:function(target){ + var theTarget = target,members = this.get('members'),i; + if (theTarget === null || theTarget === undefined){ + theTarget = this; + } + for (i = 0; i < members.length; ++i){ + members[i].initializeEvents(theTarget); // the members will use the parent target as their event catcher + // this will cause the events to bubble up in the group. + } + //this.checkIndicators(); + } + }); + Y.GroupBaseField = _GroupBaseField; +/** + * @requires yahoo.base, yahoo.dom, yahoo.event + * @namespace Validator + * This field is for password input on a form. This would be used for having users + * select a password, and requiring a minimum strength to be required. + * @class PasswordField + * @extends TextBaseField + */ + /** + * @requires yahoo.base, yahoo.dom, yahoo.event + * @namespace Validator + * This field is for password input on a form. This would be used for having users + * select a password, and requiring a minimum strength to be required. + * @class PasswordField + * @extends TextBaseField + */ + /** + * @constructor + * This will initialize the password field using the given json configuration + * @param {Object} config Configuration object + */ + function _PasswordField(config){ + _PasswordField.superclass.constructor.apply(this,arguments); + } + _PasswordField.staticVariables = { + // default strong password + StrongPassword:/^(?=.{8,})(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*\W).*$/, + // default medium password + MediumPassword:/^(?=.{7,})(((?=.*[A-Z])(?=.*[a-z]))|((?=.*[A-Z])(?=.*[0-9]))|((?=.*[a-z])(?=.*[0-9]))).*$/, + // default minimum password + MinimumPassword:/(?=.{6,}).*/ + }; + _PasswordField.ATTRS = { + /** + * This is the required level of strength for the password field to be + * considered valid. In this field, 1=min, 2=med, 3=max. You + * can set this property by using min, med, or max, or 1,2 or 3. The default + * strength level is medium. + * @property requiredLevel + * @type {string|number} + */ + requiredLevel:{ + value:2, + setter:function(val){ + if (val === null || val === undefined){ + return 'med'; // medium by default + } + if (YL.isNumber(val)){ + return val; + } + else if (YL.isString(val)){ + if (val != 'min' && val != 'med' && val != 'max'){ + throw 'Invalid level requirement, please use min, med or max'; + } + if (val == 'min'){ + return 1; + } + else if (val == 'med'){ + return 2; + } + else if (val == 'max'){ + return 3; + } + else{ + return 2; + } + } + throw 'Invalid level requirement, please use min, med or max'; + } + }, + /** + * The dom object that appears when the first level of strength has been met (min) + * @property minIndicator + * @type HTMLElement + */ + minIndicator:{ + value:null, + setter:Y.BaseInputField.staticFunctions.standardElSetter + }, + /** + * The dom object that appears when the second level of strength has been met (med) + * @property medIndicator + * @type HTMLElement + */ + medIndicator:{ + value:null, + setter:Y.BaseInputField.staticFunctions.standardElSetter + }, + /** + * The dom object that appears when the third level of strength has been met (max) + * @property medIndicator + * @type HTMLElement + */ + maxIndicator:{ + value:null, + setter:Y.BaseInputField.staticFunctions.standardElSetter + }, + /** + * This is the regular expression that determines if the minimum (first) level of + * strength has been met. The default is 8 alpha numeric characters. + * @property min + * @type regex + */ + min:{ + value:_PasswordField.staticVariables.MinimumPassword, + setter:Y.BaseInputField.staticFunctions.standardRegexSetter + }, + /** + * This is the regular expression that determines if the medium (second) level of + * strength has been met. The default is 8 alpha numeric characters with letters and symbols. + * @property med + * @type regex + */ + med:{ + value:_PasswordField.staticVariables.MediumPassword, + setter:Y.BaseInputField.staticFunctions.standardRegexSetter + }, + /** + * This is the regular expression that determines if the maximum (third) level of + * strength has been met. + * @property max + * @type regex + */ + max:{ + value:_PasswordField.staticVariables.StrongPassword, + setter:Y.BaseInputField.staticFunctions.standardRegexSetter + } + }; + _PasswordField.NAME = 'PasswordField'; + Y.extend(_PasswordField,Y.TextBaseField,{ + /** + * This will return true if the required level of strength has been + * met by the current input. + * @method isValid + * @return {boolean} true if the required level of password strength has been met. + */ + isValid:function(){ + if (!this.get('isOn')){ + return true; + } + else if (this.get('optional') && this.isEmpty()){ + return true; + } + var requiredLevel = this.get('requiredLevel'), + matchedLevel = this.getMatchedLevel(); + return requiredLevel <= matchedLevel; + }, + /** + * This will return the level which the input matches + * @method getMatchedLevel + * @return {number} the level of password strength that has been reached, 0 if none have. + */ + getMatchedLevel:function(){ + var value = this.get('inputDOM').value; + if (this.get('max').test(value)){ + return 3; + } + else if (this.get('med').test(value)){ + return 2; + } + else if (this.get('min').test(value)){ + return 1; + } + else{ + return 0; + } + }, + /** + * This will show the proper indicator based on the strength of the password + * put in the password field. + * @method showPasswordIndicator + */ + showPasswordIndicator:function(){ + var maxIndicator = this.get('maxIndicator'), + medIndicator = this.get('medIndicator'), + minIndicator = this.get('minIndicator'), + level = this.getMatchedLevel(); + if (maxIndicator !== null && maxIndicator !== undefined){ + maxIndicator.style.display = 'none'; + } + if (medIndicator !== null && medIndicator !== undefined){ + medIndicator.style.display = 'none'; + } + if (minIndicator !== null && minIndicator !== undefined){ + minIndicator.style.display = 'none'; + } + if (!this.get('isOn')){ + return; // we don't show password indicator if the password field is off + } + else if (this.get('optional') && this.isEmpty()){ + return; // don't display the indicator if this field is optional' + } + + if ((level == 3) && (maxIndicator !== null && maxIndicator !== undefined)){ + maxIndicator.style.display = ''; + } + else if ((level == 2) && (medIndicator !== null && medIndicator !== undefined)){ + medIndicator.style.display = ''; + } + else if ((level == 1) && (minIndicator !== null && minIndicator !== undefined)){ + minIndicator.style.display = ''; + } + }, + /** + * This will ensure the proper password indicator is shown, as well + * as the proper indicators showing if the field is valid or invalid. + * @method checkIndicators + * @return {boolean} true if this password field is considered valid. + */ + checkIndicators:function(){ + var rtVl = _PasswordField.superclass.checkIndicators.call(this); + this.showPasswordIndicator(); + return rtVl; + }, + /** + * Calls the super class' showCorrectIndicator, then ensures the proper + * password strength indicator is shown. + * @method showCorrectIndicator + */ + showCorrectIndicator:function(){ + _PasswordField.superclass.showCorrectIndicator.call(this); + this.showPasswordIndicator(); + }, + /** + * Calls the super class' showIncorrectIndicator, then ensures the proper + * password strength indicator is shown. + * @method showIncorrectIndicator + */ + showIncorrectIndicator:function(){ + _PasswordField.superclass.showIncorrectIndicator.call(this); + this.showPasswordIndicator(); + }, + /** + * Calls the super class' showNoIndicators, then ensures the proper + * password strength indicator is shown. + * @method showNoIndicators + */ + showNoIndicators:function(){ + _PasswordField.superclass.showNoIndicators.call(this); + this.showPasswordIndicator(); + } + }); + Y.PasswordField = _PasswordField; +/** + * @requires yahoo.base, yahoo.dom, yahoo.event + * @namespace Validator + * This field is for matching two inputs on the form. For instance, this would + * be useful for having users re-enter passwords, or re-enter e-mail addresses. + * @class MatchField + * @extends TextBaseField + */ + /** + * @constructor + * This will initialize the match field with the given JSON configuration + * @param {Object} config Configuration JSON object. + */ + function _MatchField(config){ + _MatchField.superclass.constructor.apply(this,arguments); + } + _MatchField.ATTRS = { + /** + * This is the dom that the match field will compare the input of its' own dom against. + * @property matchDOM + * @type HTMLElement + */ + matchDOM:{ + value:null, + setter:Y.BaseInputField.staticFunctions.standardElSetter + }, + /** + * If set to true, this will do a case sensitive match on the two input DOM's values + * in order to determine if this field is valid. True by default + * @property caseSensitive + * @type boolean + */ + caseSensitive:{ + value:true, + setter:Y.BaseInputField.staticFunctions.BOOLEANSETTER + + } + }; + _MatchField.NAME = 'MatchField'; + Y.extend(_MatchField,Y.TextBaseField,{ + /** + * This will return true if the match dom's value matches the input Dom's value. The comparison + * will be case sensitive depending on the case sensitive property. The input is also NOT trimmed + * so leading or tailing whitespace is included in the comparison. + * @method isValid + * @return {boolean} true if the match dom's value matches the input dom's value. + */ + isValid:function(){ + if (!this.get('isOn')){ + return true; + } + else if (this.get('optional') && this.isEmpty()){ + return true; + } + var matchDom = this.get('matchDOM'), + inputDom = this.get('inputDOM'); + if (this.isEmpty()){ + return false; + } + if (this.get('caseSensitive')){ + return matchDom.value == inputDom.value; + } + else{ + return matchDom.value.toLowerCase() == inputDom.value.toLowerCase(); + } + } + }); + Y.MatchField = _MatchField; +/** + * @requires yahoo.base, yahoo.dom, yahoo.event + * @namespace Validator + * This field is for SELET input. This will ensure that a non-empty value is + * selected in the field in order for it to be considered valid. + * @class SelectField + * @extends TextBaseField + */ + /** + * @constructor + * This will initialize the field with the given configuration json. + * @param {Object} config Configuration json object. + */ + function _SelectField(config){ + _SelectField.superclass.constructor.apply(this,arguments); + } + _SelectField.ATTRS = { + /** + * This will be the value of the option that specifies no selected + * value. '' by default. + * @property emptyValue + * @type String + */ + emptyValue:{ + value:'', + setter:function(val){ + if (val === null || val === undefined){ + return ''; + } + else{ + return val; + } + } + } + }; + _SelectField.NAME = 'SelectField'; + Y.extend(_SelectField,Y.TextBaseField,{ + /** + * Returns true if the value in the select input matches the specified empty value + * @return {boolean} true if the value in the select input matches the specified empty value + */ + isEmpty:function(){ + var value = this.get('inputDOM').value; + return value == this.get('emptyValue'); + }, + /** + * This will set the select field back to its' empty value. + * @method clear + * @param {boolean} silent If true, this function will not invoke the on change event listener. + */ + clear:function(silent){ + this.get('inputDOM').value = this.get('emptyValue'); + if (silent !== true){ + this._evtOnChange(); + } + }, + /** + * Returns true if the value selected in the dom doesn't match the value + * set for the emptyValue property. + * @return {boolean} false if the value in the select input matches the specified empty value + */ + isValid:function(){ + if (!this.get('isOn')){ + return true; // is always valid if off + } + else if (this.get('optional') && this.isEmpty()){ + return true; + } + var value = this.get('inputDOM').value; + return value != this.get('emptyValue'); + }, + /** + * This will attach the onchange event to the select DOM + * @method initializeEvents + * @param {Object} target Object who will be listening for the select field's change event. + */ + initializeEvents:function(target){ + var theTarget = target; + if (theTarget === null || theTarget === undefined){ + theTarget = this; + } + Y.Event.attach('change',theTarget._evtOnChange,this.get('inputDOM'),theTarget,true); + } + }); + Y.SelectField = _SelectField; + + +}, 'gallery-2009.11.19-20' ,{requires:['node', 'event', 'dom', 'base']}); diff --git a/build/gallery-formvalidator/gallery-formvalidator-min.js b/build/gallery-formvalidator/gallery-formvalidator-min.js new file mode 100644 index 0000000000..3ade43681f --- /dev/null +++ b/build/gallery-formvalidator/gallery-formvalidator-min.js @@ -0,0 +1,5 @@ +YUI.add("gallery-formvalidator",function(A){var K=A.Lang,C=function(){return(((1+Math.random())*65536)|0).toString(16).substring(1);},L=function(){return(C()+C()+"-"+C()+"-"+C()+"-"+C()+"-"+C()+C()+C());},D=function(W){var V=[],U,T;if((W.tagName!==null&&W.tagName!==undefined)&&(W.tagName.toLowerCase()=="input")&&(W.type=="submit")){return[W];}U=W.children;if(U===null||U===undefined){U=W.childNodes;}for(T=0;TA.BaseInputField.staticVariables.MAX_INTEGER){return A.BaseInputField.staticVariables.MAX_INTEGER;}return T;}},maxDecimalPlaces:{value:-1,setter:function(U){var T=U;if(!K.isNumber(T)){T=parseInt(T,10);}if(!K.isNumber(T)){throw"Invalid value given for decimal places: "+U;}else{return U;}}}};B.NAME="DoubleField";A.extend(B,A.TextBaseField,{isValid:function(){if(!B.superclass.isValid.call(this)){return false;}else{if(this.get("optional")&&this.isEmpty()){return true;}}var a=this.get("inputDOM").value,W=0,b,V,X,Z,U,T=this.get("maxDecimalPlaces");if((T!=-1)&&(a.indexOf(".")!=-1)){U=a.split(".")[1];if(U.length>T){return false;}}try{W=parseFloat(a,10);}catch(Y){return false;}if(W.toString()===null||W.toString()===undefined){return false;}if(W.toString().toLowerCase()=="nan"){return false;}b=this.get("minInclusive");V=this.get("maxInclusive");X=this.get("min");Z=this.get("max");if(b&&(X>W)){return false;}else{if(!b&&(X>=W)){return false;}else{if(V&&(ZT)){throw"Minimum must be less than or equal to maximum";}else{return U;}}},maxValid:{value:null,setter:function(U){if(U===null||U===undefined){return null;}if(U<1){throw"The maximum must be greater than 1";}var T=this.get("minValid");if((T!==null&&T!==undefined)&&(U=U)&&a;}}return a;},getId:function(){var U=this.get("id"),T;if(U!==null&&U!==undefined){return U;}T=this.get("groupDOM");if(T!==null&&T!==undefined){return T.id;}else{return null;}},isEmpty:function(){var T=this.get("members"),U;for(U=0;U=U)&&b;}}else{b=false;}if(!a){this.showNoIndicators();return true;}else{if(this.get("optional")&&this.isEmpty()){this.showNoIndicators();return true;}else{if(b){this.showCorrectIndicator();return true;}else{this.showIncorrectIndicator();return false;}}}},insertBeside:function(T){},initializeEvents:function(W){var V=W,T=this.get("members"),U;if(V===null||V===undefined){V=this;}for(U=0;U + *
          + *
        • Email
        • + *
        • Phone
        • + *
        • CreditCard
        • + *
        • Zipcode
        • + *
        • Postalcode
        • + *
        + * @property textType + * @type string + */ + textType:{ + lazy:false, + value:null, + setter:function(val){ + if (val === null || val === undefined){ + return null; + } + else if (val.toLowerCase() == 'email'){ + this.set('regex',Y.BaseInputField.staticVariables.EMAILREGEX); + } + else if (val.toLowerCase() == 'phone'){ + this.set('regex',/^([(]?[2-9]\d{2}[)]?)[ ]*-?[ ]*(\d{3})[ ]*-?[ ]*(\d{4})$/); + } + else if (val.toLowerCase() == 'creditcard'){ + this.set('regex',/[0-9]{4} {0,1}[0-9]{4} {0,1}[0-9]{4} {0,1}[0-9]{4}/); + } + else if (val.toLowerCase() == 'zipcode'){ + this.set('regex',/^(\d{5})([\s]*-[\s]*\d{4})?$/); + } + else if (val.toLowerCase() == 'postalcode'){ + this.set('regex',/^[a-zA-Z]{1}[0-9]{1}[a-zA-Z]{1}[\s]*[0-9]{1}[a-zA-Z]{1}[0-9]{1}$/); + } + return val; + } + }, + /** + * This is the main input DOM that the validator will check input on. + * @property inputDOM + * @type HTMLElement + */ + inputDOM:{ + value:null, + setter:Y.BaseInputField.staticFunctions.standardElSetter + } + }; + _TextBaseField.NAME = 'TextBaseField'; + Y.extend(_TextBaseField,Y.BaseInputField,{ + /** + * This will setup the indicators for the input + * @method initializeInput + */ + initializeInput:function(){ + this.setupIndicators(); + }, + /** + * This will reset the text field to '' + * @method clear + * @param {boolean} silent Set to true if you do not want the clear to invoke a form validator change event. + */ + clear:function(silent){ + this.get('inputDOM').value = ''; + if (silent !== true){ + this._evtOnChange(); + } + }, + /** + * This will return the dom that represents the input. + * @method getInputDOM + * @return {HTMLElement} input dom for this field. + */ + getInputDOM:function(){return this.get('inputDOM');}, + /** + * This will disable the input. + * @method disable + */ + disable:function(){ + _TextBaseField.superclass.disable.call(this); + this.get('inputDOM').disabled = true; + }, + /** + * This will enable the input. + * @method enable + */ + enable:function(){ + _TextBaseField.superclass.enable.call(this); + this.get('inputDOM').disabled = false; + }, + /** + * Returns true only if the input is not empty, and it is not longer than the maximum length setting + * @method isValid + * @return {boolean} true if the input is not empty, and it is not longer than the maximum length setting + */ + isValid:function(){ + if (!this.get('isOn')){ + return true; // is always valid if off + } + if (this.get('optional') && this.isEmpty()){ + return true; + } + var regex = this.get('regex'), + value = this.get('inputDOM').value; + if ((regex !== null && regex !== undefined) && (!regex.test(value))){ + return false; // return false if the value does not match the format of the set regular expression + } + return !this.isEmpty() && value.length <= this.get('maxLength'); + }, + /** + * Returns the id of the input dom + * @method getId + * @return {string} id of the input dom. + */ + getId:function(){ + return this.get('inputDOM').id; + }, + /** + * Returns true if the input dom has an empty string value. + * @method isEmpty + * @return {boolean} true if the input is not '' + */ + isEmpty:function(){ + return (this.get('inputDOM').value === ''); + }, + /** + * This will ensure the input is formatted as desired using the formatter, but only + * if the input is valid. If it does not match the regular expression, this will not call + * the format method/object's format method. + * @method checkFormat + */ + checkFormat:function(){ + if (!this.isValid()){ + return; // input has to be valid first + } + if (!this.inputIsOn()){ + return; // if its off, who cares + } + var formatter = this.get('formatter'),inputDOM; + if (formatter === null || formatter === undefined){ + return; + } + inputDOM = this.get('inputDOM'); + if (YL.isFunction(formatter)){ + inputDOM.value = formatter(inputDOM.value); + } + else{ + inputDOM.value = formatter.format(inputDOM.value); + } + }, + /** + * This will ensure the proper css and/or dom is showing to indicate + * that the input is valid or invalid. + * @method checkIndicators + * @return {boolean} True if input is valid + */ + checkIndicators:function(){ + if (!this.inputIsOn()){ + this.showNoIndicators(); + return true; + } + else if (this.get('optional') && this.isEmpty()){ + this.showNoIndicators(); + return true; + } + else if (this.isValid()){ + this.showCorrectIndicator(); + this.checkFormat(); + return true; + } + else{ + this.showIncorrectIndicator(); + return false; + } + }, + /** + * This will ensure that the incorrect indicator is hidden and the incorrect css is not used, and will + * ensure that the correct indicator is showing, and the correct css is applied. + * @method showCorrectIndicator + */ + showCorrectIndicator:function(){ + var inputDom = this.get('inputDOM'), + correctIndicator = this.get('correctIndicator'), + incorrectIndicator = this.get('incorrectIndicator'); + + this.checkFormat(); + Y.DOM.removeClass(inputDom,this.get('incorrectCss')); + Y.DOM.addClass(inputDom,this.get('correctCss')); + if (correctIndicator !== null && correctIndicator !== undefined){ + correctIndicator.style.display = ''; + } + if (incorrectIndicator !== null && incorrectIndicator !== undefined){ + incorrectIndicator.style.display = 'none'; + } + }, + /** + * This will ensure that the correct indicator is hidden and the correct css is not used, and will + * ensure that the incorrect indicator is showing, and the correct css is applied. + * @method showCorrectIndicator + */ + showIncorrectIndicator:function(){ + var inputDom = this.get('inputDOM'), + correctIndicator = this.get('correctIndicator'), + incorrectIndicator = this.get('incorrectIndicator'); + Y.DOM.addClass(inputDom,this.get('incorrectCss')); + Y.DOM.removeClass(inputDom,this.get('correctCss')); + if (correctIndicator !== null && correctIndicator !== undefined){ + correctIndicator.style.display = 'none'; + } + if (incorrectIndicator !== null && incorrectIndicator !== undefined){ + incorrectIndicator.style.display = ''; + } + }, + /** + * This will ensure NO indicators are showing. + * method @showNoIndicators + */ + showNoIndicators:function(){ + var inputDom = this.get('inputDOM'), + correctIndicator = this.get('correctIndicator'), + incorrectIndicator = this.get('incorrectIndicator'); + Y.DOM.removeClass(inputDom,this.get('incorrectCss')); + Y.DOM.removeClass(inputDom,this.get('correctCss')); + if (correctIndicator !== null && correctIndicator !== undefined){ + correctIndicator.style.display = 'none'; + } + if (incorrectIndicator !== null && incorrectIndicator !== undefined){ + incorrectIndicator.style.display = 'none'; + } + }, + /** + * This function will insert the given el beside the inputDOM. + * @method insertBeside + * @param {HTMLElement} el DOM object to be insert beside the main input. + */ + insertBeside:function(el) { + Y.DOM.insertAfter(el,this.get('inputDOM')); + }, + /** + * This will attach the keyup event to the input dom. + * @method initializeEvents + * @param {HTMLElement} target The Object that will be listening to the key up and blur events of the input DOM. + */ + initializeEvents:function(target){ + var theTarget = target; + if (theTarget === null || theTarget === undefined){ + theTarget = this; + } + Y.Event.attach('keyup',theTarget._evtOnChange,this.get('inputDOM'),theTarget,true); + Y.Event.attach('blur',theTarget._evtOnChange,this.get('inputDOM'),theTarget,true); + } + }); + Y.TextBaseField = _TextBaseField; +/** + * @requires yahoo.base, yahoo.dom, yahoo.event + * @namespace Validator + * This will represent a checkbox field in the form validator. Checkbox + * field can be put into a group based field or left on its' own. + * + * @class CheckboxField + * @extends BaseInputField + */ + /** + * @constructor + * This will initialize the element with the given configuration. Most of this will + * be passed to the BaseInputField. + * @param {Object} config Configuration for the checkbox field. Probably just the validWhenChecked property + */ + function _CheckboxField(config){ + _CheckboxField.superclass.constructor.apply(this,arguments); + } + _CheckboxField.ATTRS = { + /** + * The dom that represents the checkbox + * @property inputDOM + * @type HTMLElement + */ + inputDOM:{ + value:null, + setter:Y.BaseInputField.staticFunctions.standardElSetter + }, + /** + * If set to true, this checkbox will be considered valid if checked + * @property validWhenChecked + * @type boolean + */ + validWhenChecked:{ + value:true + } + }; + _CheckboxField.NAME = 'CheckboxField'; + Y.extend(_CheckboxField,Y.BaseInputField,{ + /** + * Sets up the indicators + * @method initializeInput + */ + initializeInput:function(){ + this.setupIndicators(); + //this.initializeEvents(); + }, + /** + * This will return the dom that represents the input. + * @method getInputDOM + * @return {HTMLElement} input dom for this field. + */ + getInputDOM:function(){return this.get('inputDOM');}, + /** + * This will set the checkbox back to unchecked if checked is valid, and checked, if unchecked is valid. + * @method clear + */ + clear:function(silent){ + this.get('inputDOM').checked = !this.get('validWhenChecked'); + if (silent !== true){ + this._evtOnChange(); + } + }, + /** + * This disables the checkbox + * @method disable + */ + disable:function(){ + _CheckboxField.superclass.disable.call(this); + this.get('inputDOM').disabled = true; + }, + /** + * This enables the checkbox + * @method enable + */ + enable:function(){ + _CheckboxField.superclass.enable.call(this); + this.get('inputDOM').disabled = false; + }, + /** + * Returns true only if the input is not empty, and it is not longer than the maximum length setting + * @method isValid + * @return {boolean} true if the input is not empty, and it is not longer than the maximum length setting + */ + isValid:function(){ + if (!this.get('isOn')){ + return true; // is always valid if off + } + var validWhenChecked = this.get('validWhenChecked'), + checked = this.get('inputDOM').checked; + return (validWhenChecked && checked) || (!validWhenChecked && !checked); + }, + /** + * Returns the id of the group based field. + * @method getId + * @return {String} id of the checkbox dom. + */ + getId:function(){ + return this.get('inputDOM').id; + }, + /** + * Returns the true if the checkbox input is invalid. + * @method isEmpty + * @return {boolean} true if the checkbox is not valid. + */ + isEmpty:function(){ + return !this.isValid(); + }, + /** + * This will ensure no indicators are showing, or css applied to the input that + * would signify correctness or incorrectness. + * @method showNoIndicators + */ + showNoIndicators:function(){ + var dom = this.get('inputDOM'), + correctIndicator = this.get('correctIndicator'), + incorrectIndicator = this.get('incorrectIndicator'); + + Y.DOM.removeClass(dom,this.get('incorrectCss')); + Y.DOM.removeClass(dom,this.get('correctCss')); + if (correctIndicator !== null && correctIndicator !== undefined){ + correctIndicator.style.display = 'none'; + } + if (incorrectIndicator !== null && incorrectIndicator !== undefined){ + incorrectIndicator.style.display = 'none'; + } + }, + /** + * This will ensure the proper css and/or dom is showing to indicate + * that the input is valid or invalid. + * @method checkIndicators + * @return {boolean} True if input is valid + */ + checkIndicators:function(){ + var dom = this.get('inputDOM'), + correctIndicator = this.get('correctIndicator'), + incorrectIndicator = this.get('incorrectIndicator'); + if (!this.get('isOn')){ + this.showNoIndicators(); + return this.isValid(); + } + else if (this.isValid()){ + if (dom !== null && dom !== undefined){ + Y.DOM.removeClass(dom,this.get('incorrectCss')); + Y.DOM.addClass(dom,this.get('correctCss')); + } + if (correctIndicator !== null && correctIndicator !== undefined){ + correctIndicator.style.display = ''; + } + if (incorrectIndicator !== null && incorrectIndicator !== undefined){ + incorrectIndicator.style.display = 'none'; + } + return true; + } + else{ + if (dom !== null && dom !== undefined){ + Y.DOM.addClass(dom,this.get('incorrectCss')); + Y.DOM.removeClass(dom,this.get('correctCss')); + } + if (correctIndicator !== null && correctIndicator !== undefined){ + correctIndicator.style.display = 'none'; + } + if (incorrectIndicator !== null && incorrectIndicator !== undefined){ + incorrectIndicator.style.display = ''; + } + return false; + } + }, + /** + * Inidicators are usually applicable to checkboxes, so creating them dynamically doesn't + * make much sense, this method does nothing. + * @method insertBeside + * @param {HTMLElement} el DOM object to be insert beside the main input. + */ + insertBeside:function(el) { + }, + /** + * This will initialize the checkbox so the form status is updated when the checkbox is clicked. + * @method initializeEvents + * @param {HTMLElement} target The object that will be listening to the events of the click event of the input DOM + */ + initializeEvents:function(target){ + var theTarget = target; + if (theTarget === null || theTarget === undefined){ + theTarget = this; + } + Y.Event.attach('click',theTarget._evtOnChange,this.get('inputDOM'),theTarget,true); + } + }); + Y.CheckboxField = _CheckboxField; +/** + * @requires yahoo.base, yahoo.dom, yahoo.event + * @namespace Validator + * This is a catch all class for types of input that do not fit the existing input types. + * @class CustomField + * @extends BaseInputField + */ + /** + * @constructor + * Takes the given configuration and initializes the input field's properties. + */ + function _CustomField(config){ + _CustomField.superclass.constructor.apply(this,arguments); + } + _CustomField.ATTRS = { + /** + * This will be an object that can optionally implement all the + * functions used by an input field. This can also be a function, which + * retrieves the object, or a string, which can be the name of an instance + * object or function call to retreive it. + * @property emptyValue + * @type {Object} + */ + validatorObject:{ + setter:function(val){ + if (val === null || val === undefined){ + throw 'You must provide a validator object to the custom input'; + } + var rtVl = null; + if (YL.isString(val)){ + rtVl = validatorGlobal[val];//eval(val); + } + else if (YL.isFunction(val)){ + rtVl = val(); + } + else if (YL.isObject(val)){ + rtVl = val; + } + + if (rtVl === null || rtVl === undefined){ + throw 'Your validator object must be a object'; + } + else{ + return rtVl; + } + } + }, + /** + * Property that can be optional set for the custom input for looking up the object + */ + id:{ + value:null + } + }; + _CustomField.NAME = 'CustomField'; + Y.extend(_CustomField,Y.BaseInputField,{ + /** + * This will ensure the proper dom is showing to indicate + * that the input is valid or invalid. + * @method checkIndicators + * @return {boolean} True if input is valid + */ + checkIndicators:function(){ + var correctIndicator = this.get('correctIndicator'), + incorrectIndicator = this.get('incorrectIndicator'); + if (this.isValid()){ + if (correctIndicator !== null && correctIndicator !== undefined){ + correctIndicator.style.display = ''; + } + if (incorrectIndicator !== null && incorrectIndicator !== undefined){ + incorrectIndicator.style.display = 'none'; + } + return true; + } + else{ + if (correctIndicator !== null && correctIndicator !== undefined){ + correctIndicator.style.display = 'none'; + } + if (incorrectIndicator !== null && incorrectIndicator !== undefined){ + incorrectIndicator.style.display = ''; + } + return false; + } + }, + /** + * If the id is set, that value will be returned. If not, then it will + * see if the validator object + * @method getId + * @return {String} id The id that was set to the custom validator on initialization. + */ + getId:function(){ + var rtVl = this.get('id'); + if (rtVl === null || rtVl === undefined){ + rtVl = this.executeFunction('getId',null); + } + return rtVl; + }, + /** + * This will execute the function with the specified name on the validator object + * @method executeFunction + * @private + * @return {Object} value returned from the function call + */ + executeFunction:function(name,defaultReturn){ + var obj = this.get('validatorObject'); + if (YL.isFunction(obj[name])){ + return obj[name](); + } + return defaultReturn; + }, + /** + * This will execute the function with the given name with the assumption it returns nothing + * @method executeVoidFunction + * @private + */ + executeVoidFunction:function(name){ + var obj = this.get('validatorObject'); + if (YL.isFunction(obj[name])){ + obj[name](); + } + }, + /** + * Calls the disable function on the custom validator object if it exists. + * @method disable + */ + disable:function(){ + _CustomField.superclass.disable.call(this); + this.executeVoidFunction('disable'); + }, + /** + * Calls the enable function on the custom validator object if it exists. + * @method enable + */ + enable:function(){ + _CustomField.superclass.enable.call(this); + this.executeVoidFunction('enable'); + }, + /** + * Calls the turnOff function on the custom validator object if it exists. + * @method turnOff + */ + turnOff:function(){ + _CustomField.superclass.turnOff.call(this); + this.executeVoidFunction('turnOff'); + }, + /** + * Calls the turnOn function on the custom validator object if it exists. + * @method turnOn + */ + turnOn:function(){ + _CustomField.superclass.turnOn.call(this); + this.executeVoidFunction('turnOn'); + }, + /** + * Calls the clear function on the custom validator object if it exists. + * @method clear + * @param {boolean} silent True if the clear action will not invoke an on change event. + */ + clear:function(silent){ + var obj = this.get('validatorObject'); + if (YL.isFunction(obj.clear)){ + obj.clear(silent); + } + if (silent !== true){ + this._evtOnChange(); + } + }, + /** + * Executs the custom validator object's isEmpty function + * @return {boolean} returns what the validator object's empty function returns. + */ + isEmpty:function(){ + return this.executeFunction('isEmpty',false); + }, + /** + * Returns true if the value selected in the dom doesn't match the value + * set for the emptyValue property. + * @return {boolean} false if the value in the select input matches the specified empty value + */ + isValid:function(){ + return this.executeFunction('isValid',false); + }, + /** + * This will call the insertBeside function on the validator Object if it exists. It will + * pass the dom object that is to be inserted. + * @method insertBeside + * @param {HTMLElement} el DOM object to be insert beside the main input. + */ + insertBeside:function(el) { + var obj = this.get('validatorObject'); + if (YL.isFunction(obj.insertBeside)){ + obj.insertBeside(el); + } + } + }); + Y.CustomField = _CustomField; +/** + * @requires yahoo.base, yahoo.dom, yahoo.event + * @namespace Validator + * This input field is for text input of doubles or floats. + * @class DoubleField + * @extends TextBaseField + */ + /** + * @constructor + * This will initialize the double field, and its' base fields properties. + * @param {Object} config Configuration JSON. + */ + function _DoubleField(config){ + _DoubleField.superclass.constructor.apply(this,arguments); + this.set('regex',Y.BaseInputField.staticVariables.DOUBLEREGEX); + } + _DoubleField.ATTRS = { + /** + * This is set to true if the minimum allowed values boundary is inclusive + * @property minInclusive + * @type boolean + */ + minInclusive:{ + value:true, + setter:Y.BaseInputField.staticFunctions.BOOLEANSETTER + }, + /** + * This is set to true if the maximum allowed values boundary is inclusive + * @property maxInclusive + * @type boolean + */ + maxInclusive:{ + value:true, + setter:Y.BaseInputField.staticFunctions.BOOLEANSETTER + }, + /** + * This is the minimum allowed value in the double field. Default value + * is the minimum value for an integer + * @property min + * @type number + */ + min:{ + value:0, + setter:function(val){ + var rtVl = val; + if (!YL.isNumber(rtVl)){ + rtVl = parseFloat(rtVl); + } + if (!YL.isNumber(rtVl)){ + throw 'Invalid value given for min: ' + val; + } + if (rtVl < (-1)*Y.BaseInputField.staticVariables.MAX_INTEGER){ + return (-1)*Y.BaseInputField.staticVariables.MAX_INTEGER; + } + return rtVl; + } + }, + /** + * This is the maximum allowed value in the double field. Default value + * is the maximum value for an integer + * @property max + * @type number + */ + max:{ + value:Y.BaseInputField.staticVariables.MAX_INTEGER, + setter:function(val){ + var rtVl = val; + if (!YL.isNumber(rtVl)){ + rtVl = parseFloat(rtVl); + } + if (!YL.isNumber(rtVl)){ + throw 'Invalid value given for max: ' + val; + } + if (rtVl > Y.BaseInputField.staticVariables.MAX_INTEGER){ + return Y.BaseInputField.staticVariables.MAX_INTEGER; + } + return rtVl; + } + }, + /** + * If set, this will restrict the number of decimal places allowed on the double. This could + * be done with regular expression, but this makes it a bit easier for everyone. + * @property maxDecimalPlaces + * @type number + */ + maxDecimalPlaces:{ + value:-1, + setter:function(val){ + var rtVl = val; + if (!YL.isNumber(rtVl)){ + rtVl = parseInt(rtVl,10); + } + if (!YL.isNumber(rtVl)){ + throw 'Invalid value given for decimal places: ' + val; + } + else{ + return val; + } + } + } + }; + _DoubleField.NAME = 'DoubleField'; + Y.extend(_DoubleField,Y.TextBaseField,{ + /** + * This will return true if the input matches the double regular expression + * and, if max decimals are set, the number of decimals places. + * @method isValid + * @return {boolean} true if the input is a valid double. + */ + isValid:function(){ + if (!_DoubleField.superclass.isValid.call(this)){ + return false; // return false if it doesn't match the double regex + } + else if (this.get('optional') && this.isEmpty()){ + return true; + } + var value = this.get('inputDOM').value,numVal = 0,minInclusive,maxInclusive,min,max,decimals, + maxDecimals = this.get('maxDecimalPlaces'); + if ((maxDecimals != -1) && (value.indexOf('.') != -1)){ + decimals = value.split('.')[1]; + if (decimals.length > maxDecimals) { + return false; + } + } + try{numVal = parseFloat(value,10);} + catch(e){return false;} + + if (numVal.toString() === null || numVal.toString() === undefined){ + return false; + } + if (numVal.toString().toLowerCase() == 'nan'){ + return false; + } + + minInclusive = this.get('minInclusive'); + maxInclusive = this.get('maxInclusive'); + min = this.get('min'); + max = this.get('max'); + + if (minInclusive && (min > numVal)){ + return false; + } + else if (!minInclusive && (min >= numVal)){ + return false; + } + else if (maxInclusive && (max < numVal)){ + return false; + } + else if (!maxInclusive && (max <= numVal)){ + return false; + } + else{ + return true; + } + } + }); + Y.DoubleField = _DoubleField; +/** + * @requires yahoo.base, yahoo.dom, yahoo.event + * @namespace Validator + * This input field is for text input of whole numbers. + * @class IntegerField + * @extends DoubleField + */ + /** + * @constructor + * This will initialize the integer field, and its' base fields properties. This + * will also set the integer regular expression in the Base class. This will + * require all inputs to have an integer format. + * @param {Object} config Configuration JSON. + */ + function _IntegerField(config){ + _IntegerField.superclass.constructor.apply(this,arguments); + this.set('regex',Y.BaseInputField.staticVariables.INTEGERREGEX); + } + _IntegerField.ATTRS = {}; + _IntegerField.NAME = 'IntegerField'; + Y.extend(_IntegerField,Y.DoubleField,{ + /** + * This method returns true if the input in the input DOM's value matches + * the format required for an integer. + * @method isValid + * @return {boolean} true if the field is a valid integer + */ + isValid:function(){ + if (!_IntegerField.superclass.isValid.call(this)){ + return false; // return false if it doesn't match the double regex + } + else if (this.get('optional') && this.isEmpty()){ + return true; + } + + var value = this.get('inputDOM').value,theVal = 0; + if ( value.indexOf( '.' ) != -1 ){ + return false; // don't allow numbers with decimals + } + try{theVal = parseInt(value,10);} + catch(e){return false;} + + if ( theVal.toString().toLowerCase() == 'nan' ){ + return false; + } + else{ + return true; + } + } + }); + Y.IntegerField = _IntegerField; +/** + * @requires yahoo.base, yahoo.dom, yahoo.event + * @namespace Validator + * This class is the text base input field and will be extended by all + * Inputs who's values will be text based in anyway. Examples include a number input, + * select box or a text input. + * @class GroupBaseField + * @extends BaseInputField + */ + /** + * @constructor + * This will initialize the group with the given configuration json object. + * @param {Object} config JSON configuration object containing the properties of the GroupBaseField + */ + function _GroupBaseField(config){ + _GroupBaseField.superclass.constructor.apply(this,arguments); + } + _GroupBaseField.ATTRS = { + /** + * This is the dom that contains the group's child elements. This is optional + * as the group's child inputs do not have to be contained in a dom element + * @property groupDOM + * @type HTMLElement + */ + groupDOM:{ + value:null, + setter:Y.BaseInputField.staticFunctions.standardElSetter + }, + /** + * This will contain the raw json of the input fields that will belong + * to the group input. + * @property membersJSON + * @type Object[] + */ + membersJSON:{ + value:[] + }, + /** + * When the member inputs are process and instantiated they are placed + * in this collection where they are persisted. + * @property members + * @type BaseInputField[] + */ + members:{ + value:[], + setter:function(val){ + if (YL.isArray(val)){ + return val; + } + else{ + throw 'The members property of a group must be an array'; + } + } + }, + /** + * Minimum number of inputs that need to be properly filled in for this to be valid. + * 0 to state that none have to be filled in. If not set, this is property is not used. + * @property minValid + * @type {number} + */ + minValid:{ + value:null, + setter:function(val){ + if (val === null || val === undefined){ + return null; + } + var theVal = val,max; + if (!YL.isNumber(theVal)){ + theVal = parseInt(theVal,10); + } + if (theVal < 1){ + throw 'The minimum must be greater than 1'; + } + + max = this.get('maxValid'); + if ((max !== null && max !== undefined) && (theVal > max)){ + throw 'Minimum must be less than or equal to maximum'; + } + else{ + return theVal; + } + } + }, + /** + * Maximum nuymber of fields that can be filled in. Anymore (valid or not) and the group is invalid. + * If not set, this is property is not used. + * @property maxValid + * @type number + */ + maxValid:{ + value:null, + setter:function(val){ + if (val === null || val === undefined){ + return null; + } + if (val < 1){ + throw 'The maximum must be greater than 1'; + } + var min = this.get('minValid'); + if ((min !== null && min !== undefined) && (val < min)){ + throw 'Maximum must be greater than or equal to minimum'; + } + else{ + return val; + } + } + }, + /** + * Property that can be optional set for the custom input for looking up the object + * @property id + * @type string + */ + id:{ + value:null + } + }; + _GroupBaseField.NAME = 'GroupBaseField'; + Y.extend(_GroupBaseField,Y.BaseInputField,{ + /** + * Indicator function to help the form validator deal with the regular inputs and group + * inputs differently on some occasions. + * @method isGroup + * @return boolean True all the time, as this is infact a group + */ + isGroup:function(){return true;}, + /** + * This will return the dom that represents the input. + * @method getInputDOM + * @return {HTMLElement} input dom for this field. + */ + getInputDOM:function(){return this.get('groupDOM');}, + /** + * This will return any member input with the given id. + * @method getInput + * @param {string} id Id of an input + * @return {BaseInputField} The field with the given id, null if no such field exists + */ + getInput:function(id){ + var members = this.get('members'),rtVl,i; + for (i = 0; i < members.length; ++i){ + if (members[i].getId() == id){ + return members[i]; + } + if (members[i].isGroup()){ + rtVl = members[i].getInput(id); + if (rtVl !== null && rtVl !== undefined){ + return rtVl; + } + } + } + return null; + }, + /** + * This will call the clear method (in silent mode) on all member inputs. + * If silent is true, it will then fire its own on change event. + * @method clear + * @param {boolean} silent True if this will call the on change event + */ + clear:function(silent){ + var members = this.get('members'),i; + for (i = 0; i < members.length; ++i){ + members[i].clear(true); + } + if (silent !== true){ + this._evtOnChange(); + } + }, + /** + * This will disable all the inputs in the group + * @method disable + */ + disable:function(){ + _GroupBaseField.superclass.disable.call(this); + var members = this.get('members'),i; + for (i = 0; i < members.length; ++i){ + members[i].disable(); + } + }, + /** + * This will enable all the inputs in the group + * @method enable + */ + enable:function(){ + _GroupBaseField.superclass.enable.call(this); + var members = this.get('members'),i; + for (i = 0; i < members.length; ++i){ + members[i].enable(); + } + }, + /** + * This will turn on all the members of the group, including the group as well + * @method turnOn + */ + turnOn:function(){ + _GroupBaseField.superclass.turnOn.call(this); + var members = this.get('members'),i; + for (i = 0; i < members.length; ++i){ + members[i].turnOn(); + } + this.checkIndicators(); + }, + /** + * This will turn off all the members of the group, including the group as well + * @method turnOff + */ + turnOff:function(){ + _GroupBaseField.superclass.turnOff.call(this); + var members = this.get('members'),i; + for (i = 0; i < members.length; ++i){ + members[i].turnOff(); + } + }, + /** + * This will take the members json and initialize the inputs. Validator + * is required for setting up the field based on settings given to the + * main form validator. + * @method initializeInput + * @param {Validator} validator The validator this input gets it's default values from. + */ + initializeInput:function(validator){ + var membersJSON = this.get('membersJSON'), + members = this.get('members'),newField,i; + for (i = 0 ; i < membersJSON.length; ++i){ + newField = new membersJSON[i].type(membersJSON[i].atts,false); + validator.setupInput(newField); + members[members.length] = newField; + } + this.setupIndicators(); + this.checkIndicators(); + }, + /** + * This will add the given field to the group as an input. + * @method addInput + * @param {BaseInputField} newField The new field to be added to the group. + */ + addInput:function(newField){ + var members = this.get('members'); + members[members.length] = newField; + }, + /** + * Returns true only if the input is not empty, and it is not longer than the maximum length setting + * @method isValid + * @return {boolean} true if the input is not empty, and it is not longer than the maximum length setting + */ + isValid:function(){ + if (this.get('optional') && this.isEmpty()){ + return true; + } + var members = this.get('members'), + numValid = 0, + groupOn = this.get('isOn'),rtVl = true,empty,valid,minValid,maxValid,i; + for (i = 0; i < members.length; ++i){ + empty = members[i].isEmpty(); + valid = members[i].isValid(); + + if (!empty && valid){ + numValid++; + } + else if (!empty){ + return !groupOn; // if not empty, and not valid then the whole group is invalid (only if the group is on + } + } + if (groupOn){ + minValid = this.get('minValid'); + maxValid = this.get('maxValid'); + if (minValid !== null && minValid !== undefined){ + rtVl = minValid <= numValid; + } + if (maxValid !== null && maxValid !== undefined){ + rtVl = (maxValid >= numValid) && rtVl; + } + } + return rtVl; + }, + /** + * Returns the id of the group based field. + * @method getId + * @return {string} id of the group base field if the id property was set, null otherwise. + */ + getId:function(){ + var id = this.get('id'),groupDOM; + if (id !== null && id !== undefined){ + return id; + } + groupDOM = this.get('groupDOM'); + if (groupDOM !== null && groupDOM !== undefined){ + return groupDOM.id; + } + else{ + return null; + } + }, + /** + * Returns true all the members of the group are empty + * @method isEmpty + * @return {boolean} true if all members of the group are empty + */ + isEmpty:function(){ + var members = this.get('members'),i; + for (i = 0; i < members.length; ++i){ + if (!members[i].isEmpty()){ + return false; + } + } + return true; + }, + /** + * This will show the correct indicator, and apply the correct css to the input + * if they are set for the group. It will also ensure the incorrect indicator + * is not showing, and incorrect css is not applied. + * @method showCorrectIndicator + */ + showCorrectIndicator:function(){ + var groupDom = this.get('groupDOM'), + correctIndicator = this.get('correctIndicator'), + incorrectIndicator = this.get('incorrectIndicator'); + if (groupDom !== null && groupDom !== undefined){ + Y.DOM.removeClass(groupDom,this.get('incorrectCss')); + Y.DOM.addClass(groupDom,this.get('correctCss')); + } + if (correctIndicator !== null && correctIndicator !== undefined){ + correctIndicator.style.display = ''; + } + if (incorrectIndicator !== null && incorrectIndicator !== undefined){ + incorrectIndicator.style.display = 'none'; + } + }, + /** + * This will show the incorrect indicator, and apply the incorrect css to the input + * if they are set for the group. It will also ensure the correct indicator + * is not showing, and correct css is not applied. + * @method showIncorrectIndicator + */ + showIncorrectIndicator:function(){ + var groupDom = this.get('groupDOM'), + correctIndicator = this.get('correctIndicator'), + incorrectIndicator = this.get('incorrectIndicator'); + if (groupDom !== null && groupDom !== undefined){ + Y.DOM.addClass(groupDom,this.get('incorrectCss')); + Y.DOM.removeClass(groupDom,this.get('correctCss')); + } + if (correctIndicator !== null && correctIndicator !== undefined){ + correctIndicator.style.display = 'none'; + } + if (incorrectIndicator !== null && incorrectIndicator !== undefined){ + incorrectIndicator.style.display = ''; + } + }, + /** + * This will ensure all indicators are not showing, and no indicator css + * is applied to the input + * @method showNoIndicators + */ + showNoIndicators:function(){ + var groupDom = this.get('groupDOM'), + correctIndicator = this.get('correctIndicator'), + incorrectIndicator = this.get('incorrectIndicator'); + if (groupDom !== null && groupDom !== undefined){ + Y.DOM.removeClass(groupDom,this.get('incorrectCss')); + Y.DOM.removeClass(groupDom,this.get('correctCss')); + } + if (correctIndicator !== null && correctIndicator !== undefined){ + correctIndicator.style.display = 'none'; + } + if (incorrectIndicator !== null && incorrectIndicator !== undefined){ + incorrectIndicator.style.display = 'none'; + } + }, + /** + * This will ensure the proper css and/or dom is showing to indicate + * that the input is valid or invalid. This will also ensure that the + * right indicators are showing on the children of the group. Any change + * in a member of the group will cause this method in the group to be executed. + * @method checkIndicators + * @return {boolean} True if input is valid + */ + checkIndicators:function(){ + var members = this.get('members'), + numValid = 0, + oneInvalid = false, + groupOn = this.get('isOn'), + empty,i,valid,rtVl = true,minValid,maxValid; + for (i = 0; i < members.length; ++i){ + empty = members[i].isEmpty(); + valid = members[i].isValid(); + if (!groupOn){ + members[i].showNoIndicators(); + } + else if (this.get('optional') && this.isEmpty()){ + members[i].showNoIndicators(); + } + else if (!empty && valid){ + numValid++; + members[i].showCorrectIndicator(); + } + else if (!empty){ + members[i].showIncorrectIndicator(); + oneInvalid = true; + } + else{ + members[i].showNoIndicators(); + } + } + // if one was not empty and invalid, then return false; + if (!oneInvalid){ + minValid = this.get('minValid'); + maxValid = this.get('maxValid'); + if (minValid !== null && minValid !== undefined){ + rtVl = minValid <= numValid; + } + if (maxValid !== null && maxValid !== undefined){ + rtVl = (maxValid >= numValid) && rtVl; + } + } + else{ + rtVl = false; + } + if (!groupOn){ + this.showNoIndicators(); + return true; + } + else if (this.get('optional') && this.isEmpty()){ + this.showNoIndicators(); + return true; + } + else if (rtVl){ + this.showCorrectIndicator(); + return true; + } + else{ + this.showIncorrectIndicator(); + return false; + } + }, + /** + * Indicators are not created dynamically for groups at the moment. + * @method insertBeside + * @param {HTMLElement} el DOM object to be insert beside the main input. + */ + insertBeside:function(el) { + }, + /** + * This will go through each member and initialize their event. + * @method initializeEvents + * @param {Object} target The target that will be listening to the events of the members. + */ + initializeEvents:function(target){ + var theTarget = target,members = this.get('members'),i; + if (theTarget === null || theTarget === undefined){ + theTarget = this; + } + for (i = 0; i < members.length; ++i){ + members[i].initializeEvents(theTarget); // the members will use the parent target as their event catcher + // this will cause the events to bubble up in the group. + } + //this.checkIndicators(); + } + }); + Y.GroupBaseField = _GroupBaseField; +/** + * @requires yahoo.base, yahoo.dom, yahoo.event + * @namespace Validator + * This field is for password input on a form. This would be used for having users + * select a password, and requiring a minimum strength to be required. + * @class PasswordField + * @extends TextBaseField + */ + /** + * @requires yahoo.base, yahoo.dom, yahoo.event + * @namespace Validator + * This field is for password input on a form. This would be used for having users + * select a password, and requiring a minimum strength to be required. + * @class PasswordField + * @extends TextBaseField + */ + /** + * @constructor + * This will initialize the password field using the given json configuration + * @param {Object} config Configuration object + */ + function _PasswordField(config){ + _PasswordField.superclass.constructor.apply(this,arguments); + } + _PasswordField.staticVariables = { + // default strong password + StrongPassword:/^(?=.{8,})(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*\W).*$/, + // default medium password + MediumPassword:/^(?=.{7,})(((?=.*[A-Z])(?=.*[a-z]))|((?=.*[A-Z])(?=.*[0-9]))|((?=.*[a-z])(?=.*[0-9]))).*$/, + // default minimum password + MinimumPassword:/(?=.{6,}).*/ + }; + _PasswordField.ATTRS = { + /** + * This is the required level of strength for the password field to be + * considered valid. In this field, 1=min, 2=med, 3=max. You + * can set this property by using min, med, or max, or 1,2 or 3. The default + * strength level is medium. + * @property requiredLevel + * @type {string|number} + */ + requiredLevel:{ + value:2, + setter:function(val){ + if (val === null || val === undefined){ + return 'med'; // medium by default + } + if (YL.isNumber(val)){ + return val; + } + else if (YL.isString(val)){ + if (val != 'min' && val != 'med' && val != 'max'){ + throw 'Invalid level requirement, please use min, med or max'; + } + if (val == 'min'){ + return 1; + } + else if (val == 'med'){ + return 2; + } + else if (val == 'max'){ + return 3; + } + else{ + return 2; + } + } + throw 'Invalid level requirement, please use min, med or max'; + } + }, + /** + * The dom object that appears when the first level of strength has been met (min) + * @property minIndicator + * @type HTMLElement + */ + minIndicator:{ + value:null, + setter:Y.BaseInputField.staticFunctions.standardElSetter + }, + /** + * The dom object that appears when the second level of strength has been met (med) + * @property medIndicator + * @type HTMLElement + */ + medIndicator:{ + value:null, + setter:Y.BaseInputField.staticFunctions.standardElSetter + }, + /** + * The dom object that appears when the third level of strength has been met (max) + * @property medIndicator + * @type HTMLElement + */ + maxIndicator:{ + value:null, + setter:Y.BaseInputField.staticFunctions.standardElSetter + }, + /** + * This is the regular expression that determines if the minimum (first) level of + * strength has been met. The default is 8 alpha numeric characters. + * @property min + * @type regex + */ + min:{ + value:_PasswordField.staticVariables.MinimumPassword, + setter:Y.BaseInputField.staticFunctions.standardRegexSetter + }, + /** + * This is the regular expression that determines if the medium (second) level of + * strength has been met. The default is 8 alpha numeric characters with letters and symbols. + * @property med + * @type regex + */ + med:{ + value:_PasswordField.staticVariables.MediumPassword, + setter:Y.BaseInputField.staticFunctions.standardRegexSetter + }, + /** + * This is the regular expression that determines if the maximum (third) level of + * strength has been met. + * @property max + * @type regex + */ + max:{ + value:_PasswordField.staticVariables.StrongPassword, + setter:Y.BaseInputField.staticFunctions.standardRegexSetter + } + }; + _PasswordField.NAME = 'PasswordField'; + Y.extend(_PasswordField,Y.TextBaseField,{ + /** + * This will return true if the required level of strength has been + * met by the current input. + * @method isValid + * @return {boolean} true if the required level of password strength has been met. + */ + isValid:function(){ + if (!this.get('isOn')){ + return true; + } + else if (this.get('optional') && this.isEmpty()){ + return true; + } + var requiredLevel = this.get('requiredLevel'), + matchedLevel = this.getMatchedLevel(); + return requiredLevel <= matchedLevel; + }, + /** + * This will return the level which the input matches + * @method getMatchedLevel + * @return {number} the level of password strength that has been reached, 0 if none have. + */ + getMatchedLevel:function(){ + var value = this.get('inputDOM').value; + if (this.get('max').test(value)){ + return 3; + } + else if (this.get('med').test(value)){ + return 2; + } + else if (this.get('min').test(value)){ + return 1; + } + else{ + return 0; + } + }, + /** + * This will show the proper indicator based on the strength of the password + * put in the password field. + * @method showPasswordIndicator + */ + showPasswordIndicator:function(){ + var maxIndicator = this.get('maxIndicator'), + medIndicator = this.get('medIndicator'), + minIndicator = this.get('minIndicator'), + level = this.getMatchedLevel(); + if (maxIndicator !== null && maxIndicator !== undefined){ + maxIndicator.style.display = 'none'; + } + if (medIndicator !== null && medIndicator !== undefined){ + medIndicator.style.display = 'none'; + } + if (minIndicator !== null && minIndicator !== undefined){ + minIndicator.style.display = 'none'; + } + if (!this.get('isOn')){ + return; // we don't show password indicator if the password field is off + } + else if (this.get('optional') && this.isEmpty()){ + return; // don't display the indicator if this field is optional' + } + + if ((level == 3) && (maxIndicator !== null && maxIndicator !== undefined)){ + maxIndicator.style.display = ''; + } + else if ((level == 2) && (medIndicator !== null && medIndicator !== undefined)){ + medIndicator.style.display = ''; + } + else if ((level == 1) && (minIndicator !== null && minIndicator !== undefined)){ + minIndicator.style.display = ''; + } + }, + /** + * This will ensure the proper password indicator is shown, as well + * as the proper indicators showing if the field is valid or invalid. + * @method checkIndicators + * @return {boolean} true if this password field is considered valid. + */ + checkIndicators:function(){ + var rtVl = _PasswordField.superclass.checkIndicators.call(this); + this.showPasswordIndicator(); + return rtVl; + }, + /** + * Calls the super class' showCorrectIndicator, then ensures the proper + * password strength indicator is shown. + * @method showCorrectIndicator + */ + showCorrectIndicator:function(){ + _PasswordField.superclass.showCorrectIndicator.call(this); + this.showPasswordIndicator(); + }, + /** + * Calls the super class' showIncorrectIndicator, then ensures the proper + * password strength indicator is shown. + * @method showIncorrectIndicator + */ + showIncorrectIndicator:function(){ + _PasswordField.superclass.showIncorrectIndicator.call(this); + this.showPasswordIndicator(); + }, + /** + * Calls the super class' showNoIndicators, then ensures the proper + * password strength indicator is shown. + * @method showNoIndicators + */ + showNoIndicators:function(){ + _PasswordField.superclass.showNoIndicators.call(this); + this.showPasswordIndicator(); + } + }); + Y.PasswordField = _PasswordField; +/** + * @requires yahoo.base, yahoo.dom, yahoo.event + * @namespace Validator + * This field is for matching two inputs on the form. For instance, this would + * be useful for having users re-enter passwords, or re-enter e-mail addresses. + * @class MatchField + * @extends TextBaseField + */ + /** + * @constructor + * This will initialize the match field with the given JSON configuration + * @param {Object} config Configuration JSON object. + */ + function _MatchField(config){ + _MatchField.superclass.constructor.apply(this,arguments); + } + _MatchField.ATTRS = { + /** + * This is the dom that the match field will compare the input of its' own dom against. + * @property matchDOM + * @type HTMLElement + */ + matchDOM:{ + value:null, + setter:Y.BaseInputField.staticFunctions.standardElSetter + }, + /** + * If set to true, this will do a case sensitive match on the two input DOM's values + * in order to determine if this field is valid. True by default + * @property caseSensitive + * @type boolean + */ + caseSensitive:{ + value:true, + setter:Y.BaseInputField.staticFunctions.BOOLEANSETTER + + } + }; + _MatchField.NAME = 'MatchField'; + Y.extend(_MatchField,Y.TextBaseField,{ + /** + * This will return true if the match dom's value matches the input Dom's value. The comparison + * will be case sensitive depending on the case sensitive property. The input is also NOT trimmed + * so leading or tailing whitespace is included in the comparison. + * @method isValid + * @return {boolean} true if the match dom's value matches the input dom's value. + */ + isValid:function(){ + if (!this.get('isOn')){ + return true; + } + else if (this.get('optional') && this.isEmpty()){ + return true; + } + var matchDom = this.get('matchDOM'), + inputDom = this.get('inputDOM'); + if (this.isEmpty()){ + return false; + } + if (this.get('caseSensitive')){ + return matchDom.value == inputDom.value; + } + else{ + return matchDom.value.toLowerCase() == inputDom.value.toLowerCase(); + } + } + }); + Y.MatchField = _MatchField; +/** + * @requires yahoo.base, yahoo.dom, yahoo.event + * @namespace Validator + * This field is for SELET input. This will ensure that a non-empty value is + * selected in the field in order for it to be considered valid. + * @class SelectField + * @extends TextBaseField + */ + /** + * @constructor + * This will initialize the field with the given configuration json. + * @param {Object} config Configuration json object. + */ + function _SelectField(config){ + _SelectField.superclass.constructor.apply(this,arguments); + } + _SelectField.ATTRS = { + /** + * This will be the value of the option that specifies no selected + * value. '' by default. + * @property emptyValue + * @type String + */ + emptyValue:{ + value:'', + setter:function(val){ + if (val === null || val === undefined){ + return ''; + } + else{ + return val; + } + } + } + }; + _SelectField.NAME = 'SelectField'; + Y.extend(_SelectField,Y.TextBaseField,{ + /** + * Returns true if the value in the select input matches the specified empty value + * @return {boolean} true if the value in the select input matches the specified empty value + */ + isEmpty:function(){ + var value = this.get('inputDOM').value; + return value == this.get('emptyValue'); + }, + /** + * This will set the select field back to its' empty value. + * @method clear + * @param {boolean} silent If true, this function will not invoke the on change event listener. + */ + clear:function(silent){ + this.get('inputDOM').value = this.get('emptyValue'); + if (silent !== true){ + this._evtOnChange(); + } + }, + /** + * Returns true if the value selected in the dom doesn't match the value + * set for the emptyValue property. + * @return {boolean} false if the value in the select input matches the specified empty value + */ + isValid:function(){ + if (!this.get('isOn')){ + return true; // is always valid if off + } + else if (this.get('optional') && this.isEmpty()){ + return true; + } + var value = this.get('inputDOM').value; + return value != this.get('emptyValue'); + }, + /** + * This will attach the onchange event to the select DOM + * @method initializeEvents + * @param {Object} target Object who will be listening for the select field's change event. + */ + initializeEvents:function(target){ + var theTarget = target; + if (theTarget === null || theTarget === undefined){ + theTarget = this; + } + Y.Event.attach('change',theTarget._evtOnChange,this.get('inputDOM'),theTarget,true); + } + }); + Y.SelectField = _SelectField; + + +}, 'gallery-2009.11.19-20' ,{requires:['node', 'event', 'dom', 'base']}); diff --git a/build/gallery-history-lite/gallery-history-lite-debug.js b/build/gallery-history-lite/gallery-history-lite-debug.js new file mode 100644 index 0000000000..cb2f0cf418 --- /dev/null +++ b/build/gallery-history-lite/gallery-history-lite-debug.js @@ -0,0 +1,267 @@ +YUI.add('gallery-history-lite', function(Y) { + +/** + * The History Lite utility is similar in purpose to the YUI Browser History + * utility, but with a more flexible API, no initialization or markup + * requirements, limited IE6/7 support, and a much smaller footprint. + * + * @module gallery-history-lite + */ + +/** + * @class HistoryLite + * @static + */ + +var w = Y.config.win, + docMode = Y.config.doc.documentMode, + encode = encodeURIComponent, + loc = w.location, + + // IE8 supports the hashchange event, but only in IE8 Standards + // Mode. However, IE8 in IE7 compatibility mode still defines the + // event (but never fires it), so we can't just sniff for the event. We + // also can't just sniff for IE8, since other browsers will eventually + // support this event as well. Thanks Microsoft! + supportsHashChange = w.onhashchange !== undefined && + (docMode === undefined || docMode > 7), + + lastHash, + pollInterval, + HistoryLite, + + /** + * Fired when the history state changes. + * + * @event history-lite:change + * @param {EventFacade} Event facade with the following additional + * properties: + *
        + *
        changed
        + *
        + * name:value pairs of history parameters that have been added or + * changed + *
        + *
        removed
        + *
        + * name:value pairs of history parameters that have been removed + * (values are the old values) + *
        + *
        + */ + EV_HISTORY_CHANGE = 'history-lite:change'; + +// -- Private Methods ------------------------------------------------------ + +/** + * Creates a hash string from the specified object of name/value parameter + * pairs. + * + * @method createHash + * @param {Object} params name/value parameter pairs + * @return {String} hash string + * @private + */ +function createHash(params) { + var hash = []; + + Y.each(params, function (value, name) { + if (Y.Lang.isValue(value)) { + hash.push(encode(name) + '=' + encode(value)); + } + }); + + return hash.join('&'); +} + +/** + * Wrapper around decodeURIComponent() that also converts + + * chars into spaces. + * + * @method decode + * @param {String} string string to decode + * @return {String} decoded string + * @private + */ +function decode(string) { + return decodeURIComponent(string.replace(/\+/g, ' ')); +} + +/** + * Gets the current URL hash. + * + * @method getHash + * @return {String} + * @private + */ +var getHash; + +if (Y.UA.gecko) { + // We branch at runtime for Gecko since window.location.hash in Gecko + // returns a decoded string, and we want all encoding untouched. + getHash = function () { + var matches = /#.*$/.exec(loc.href); + return matches && matches[0] ? matches[0] : ''; + }; +} else { + getHash = function () { + return loc.hash; + }; +} + +/** + * Sets the browser's location hash to the specified string. + * + * @method setHash + * @param {String} hash + * @private + */ +function setHash(hash) { + loc.hash = hash; +} + +// -- Private Event Handlers ----------------------------------------------- + +/** + * Handles changes to the location hash and fires the history-lite:change + * event if necessary. + * + * @method handleHashChange + * @param {String} newHash new hash value + * @private + */ +function handleHashChange(newHash) { + var lastParsed = HistoryLite.parseQuery(lastHash), + newParsed = HistoryLite.parseQuery(newHash), + changedParams = {}, + removedParams = {}, + isChanged; + + // Figure out what changed. + Y.each(newParsed, function (value, name) { + if (value !== lastParsed[name]) { + changedParams[name] = value; + isChanged = true; + } + }); + + // Figure out what was removed. + Y.each(lastParsed, function (value, name) { + if (!newParsed.hasOwnProperty(name)) { + removedParams[name] = value; + isChanged = true; + } + }); + + if (isChanged) { + HistoryLite.fire(EV_HISTORY_CHANGE, { + changed: changedParams, + newVal : newHash, + prevVal: lastHash, + removed: removedParams + }); + } +} + +/** + * Default handler for the history-lite:change event. Stores the new hash + * for later comparison and event triggering. + * + * @method defaultChangeHandler + * @param {EventFacade} e + * @private + */ +function defaultChangeHandler(e) { + lastHash = e.newVal; +} + +Y.HistoryLite = HistoryLite = { + // -- Public Methods --------------------------------------------------- + + /** + * Adds a history entry with changes to the specified parameters. Any + * parameters with a null or undefined value + * will be removed from the new history entry. + * + * @method add + * @param {String|Object} params query string, hash string, or object + * containing name/value parameter pairs + * @param {Boolean} silent if true, a history change event will + * not be fired for this change + */ + add: function (params, silent) { + var newHash = createHash(Y.merge(HistoryLite.parseQuery(getHash()), + Y.Lang.isString(params) ? HistoryLite.parseQuery(params) : params)); + + if (silent) { + defaultChangeHandler({newVal: newHash}); + } + + setHash(newHash); + }, + + /** + * Gets the current value of the specified history parameter, or an + * object of name/value pairs for all current values if no parameter + * name is specified. + * + * @method get + * @param {String} name (optional) parameter name + * @return {Object|mixed} + */ + get: function (name) { + var params = HistoryLite.parseQuery(getHash()); + return name ? params[name] : params; + }, + + /** + * Parses a query string or hash string into an object of name/value + * parameter pairs. + * + * @method parseQuery + * @param {String} query query string or hash string + * @return {Object} + */ + parseQuery: function (query) { + var matches = query.match(/([^\?#&]+)=([^&]+)/g) || [], + params = {}, + i, len, param; + + for (i = 0, len = matches.length; i < len; ++i) { + param = matches[i].split('='); + params[decode(param[0])] = decode(param[1]); + } + + return params; + } +}; + +// Make HistoryLite an event target and publish the change event. +Y.augment(HistoryLite, Y.EventTarget, true, null, {emitFacade: true}); + +HistoryLite.publish(EV_HISTORY_CHANGE, { + broadcast: 2, + defaultFn: defaultChangeHandler +}); + +// Start watching for hash changes. +lastHash = getHash(); + +if (supportsHashChange) { + Y.Node.DOM_EVENTS.hashchange = true; + + Y.on('hashchange', function () { + handleHashChange(getHash()); + }, w); +} else { + pollInterval = pollInterval || Y.later(50, HistoryLite, function () { + var hash = getHash(); + + if (hash !== lastHash) { + handleHashChange(hash); + } + }, null, true); +} + + +}, 'gallery-2009.11.09-19' ,{requires:['event-custom']}); diff --git a/build/gallery-history-lite/gallery-history-lite-min.js b/build/gallery-history-lite/gallery-history-lite-min.js new file mode 100644 index 0000000000..ba8f7ffdfa --- /dev/null +++ b/build/gallery-history-lite/gallery-history-lite-min.js @@ -0,0 +1 @@ +YUI.add("gallery-history-lite",function(E){var N=E.config.win,I=E.config.doc.documentMode,L=encodeURIComponent,K=N.location,F=N.onhashchange!==undefined&&(I===undefined||I>7),G,D,C,O="history-lite:change";function B(R){var Q=[];E.each(R,function(T,S){if(E.Lang.isValue(T)){Q.push(L(S)+"="+L(T));}});return Q.join("&");}function A(Q){return decodeURIComponent(Q.replace(/\+/g," "));}var H;if(E.UA.gecko){H=function(){var Q=/#.*$/.exec(K.href);return Q&&Q[0]?Q[0]:"";};}else{H=function(){return K.hash;};}function P(Q){K.hash=Q;}function M(U){var V=C.parseQuery(G),R=C.parseQuery(U),T={},S={},Q;E.each(R,function(X,W){if(X!==V[W]){T[W]=X;Q=true;}});E.each(V,function(X,W){if(!R.hasOwnProperty(W)){S[W]=X;Q=true;}});if(Q){C.fire(O,{changed:T,newVal:U,prevVal:G,removed:S});}}function J(Q){G=Q.newVal;}E.HistoryLite=C={add:function(S,Q){var R=B(E.merge(C.parseQuery(H()),E.Lang.isString(S)?C.parseQuery(S):S));if(Q){J({newVal:R});}P(R);},get:function(Q){var R=C.parseQuery(H());return Q?R[Q]:R;},parseQuery:function(T){var S=T.match(/([^\?#&]+)=([^&]+)/g)||[],V={},R,Q,U;for(R=0,Q=S.length;R 7), + + lastHash, + pollInterval, + HistoryLite, + + /** + * Fired when the history state changes. + * + * @event history-lite:change + * @param {EventFacade} Event facade with the following additional + * properties: + *
        + *
        changed
        + *
        + * name:value pairs of history parameters that have been added or + * changed + *
        + *
        removed
        + *
        + * name:value pairs of history parameters that have been removed + * (values are the old values) + *
        + *
        + */ + EV_HISTORY_CHANGE = 'history-lite:change'; + +// -- Private Methods ------------------------------------------------------ + +/** + * Creates a hash string from the specified object of name/value parameter + * pairs. + * + * @method createHash + * @param {Object} params name/value parameter pairs + * @return {String} hash string + * @private + */ +function createHash(params) { + var hash = []; + + Y.each(params, function (value, name) { + if (Y.Lang.isValue(value)) { + hash.push(encode(name) + '=' + encode(value)); + } + }); + + return hash.join('&'); +} + +/** + * Wrapper around decodeURIComponent() that also converts + + * chars into spaces. + * + * @method decode + * @param {String} string string to decode + * @return {String} decoded string + * @private + */ +function decode(string) { + return decodeURIComponent(string.replace(/\+/g, ' ')); +} + +/** + * Gets the current URL hash. + * + * @method getHash + * @return {String} + * @private + */ +var getHash; + +if (Y.UA.gecko) { + // We branch at runtime for Gecko since window.location.hash in Gecko + // returns a decoded string, and we want all encoding untouched. + getHash = function () { + var matches = /#.*$/.exec(loc.href); + return matches && matches[0] ? matches[0] : ''; + }; +} else { + getHash = function () { + return loc.hash; + }; +} + +/** + * Sets the browser's location hash to the specified string. + * + * @method setHash + * @param {String} hash + * @private + */ +function setHash(hash) { + loc.hash = hash; +} + +// -- Private Event Handlers ----------------------------------------------- + +/** + * Handles changes to the location hash and fires the history-lite:change + * event if necessary. + * + * @method handleHashChange + * @param {String} newHash new hash value + * @private + */ +function handleHashChange(newHash) { + var lastParsed = HistoryLite.parseQuery(lastHash), + newParsed = HistoryLite.parseQuery(newHash), + changedParams = {}, + removedParams = {}, + isChanged; + + // Figure out what changed. + Y.each(newParsed, function (value, name) { + if (value !== lastParsed[name]) { + changedParams[name] = value; + isChanged = true; + } + }); + + // Figure out what was removed. + Y.each(lastParsed, function (value, name) { + if (!newParsed.hasOwnProperty(name)) { + removedParams[name] = value; + isChanged = true; + } + }); + + if (isChanged) { + HistoryLite.fire(EV_HISTORY_CHANGE, { + changed: changedParams, + newVal : newHash, + prevVal: lastHash, + removed: removedParams + }); + } +} + +/** + * Default handler for the history-lite:change event. Stores the new hash + * for later comparison and event triggering. + * + * @method defaultChangeHandler + * @param {EventFacade} e + * @private + */ +function defaultChangeHandler(e) { + lastHash = e.newVal; +} + +Y.HistoryLite = HistoryLite = { + // -- Public Methods --------------------------------------------------- + + /** + * Adds a history entry with changes to the specified parameters. Any + * parameters with a null or undefined value + * will be removed from the new history entry. + * + * @method add + * @param {String|Object} params query string, hash string, or object + * containing name/value parameter pairs + * @param {Boolean} silent if true, a history change event will + * not be fired for this change + */ + add: function (params, silent) { + var newHash = createHash(Y.merge(HistoryLite.parseQuery(getHash()), + Y.Lang.isString(params) ? HistoryLite.parseQuery(params) : params)); + + if (silent) { + defaultChangeHandler({newVal: newHash}); + } + + setHash(newHash); + }, + + /** + * Gets the current value of the specified history parameter, or an + * object of name/value pairs for all current values if no parameter + * name is specified. + * + * @method get + * @param {String} name (optional) parameter name + * @return {Object|mixed} + */ + get: function (name) { + var params = HistoryLite.parseQuery(getHash()); + return name ? params[name] : params; + }, + + /** + * Parses a query string or hash string into an object of name/value + * parameter pairs. + * + * @method parseQuery + * @param {String} query query string or hash string + * @return {Object} + */ + parseQuery: function (query) { + var matches = query.match(/([^\?#&]+)=([^&]+)/g) || [], + params = {}, + i, len, param; + + for (i = 0, len = matches.length; i < len; ++i) { + param = matches[i].split('='); + params[decode(param[0])] = decode(param[1]); + } + + return params; + } +}; + +// Make HistoryLite an event target and publish the change event. +Y.augment(HistoryLite, Y.EventTarget, true, null, {emitFacade: true}); + +HistoryLite.publish(EV_HISTORY_CHANGE, { + broadcast: 2, + defaultFn: defaultChangeHandler +}); + +// Start watching for hash changes. +lastHash = getHash(); + +if (supportsHashChange) { + Y.Node.DOM_EVENTS.hashchange = true; + + Y.on('hashchange', function () { + handleHashChange(getHash()); + }, w); +} else { + pollInterval = pollInterval || Y.later(50, HistoryLite, function () { + var hash = getHash(); + + if (hash !== lastHash) { + handleHashChange(hash); + } + }, null, true); +} + + +}, 'gallery-2009.11.09-19' ,{requires:['event-custom']}); diff --git a/build/gallery-io-poller/gallery-io-poller-debug.js b/build/gallery-io-poller/gallery-io-poller-debug.js new file mode 100644 index 0000000000..1587014d9e --- /dev/null +++ b/build/gallery-io-poller/gallery-io-poller-debug.js @@ -0,0 +1,397 @@ +YUI.add('gallery-io-poller', function(Y) { + + /** + * IO Poller - A utility to smartly poll (via XHR requests) a resource on the server. + * http://925html.com/code/smart-polling/ + * + * Oddnut Software + * Copyright (c) 2009 Eric Ferraiuolo - http://eric.ferraiuolo.name + * YUI BSD License - http://developer.yahoo.com/yui/license.html + */ + + /** + * Extends Y.io to add support for xhr-polling. + * + * @module poller + * @requires io-base, base + */ + + var Poller, + POLLER = 'poller', + + INTERVAL = 'interval', + URI = 'uri', + IO_CONFIG = 'ioConfig', + POLLING = 'polling', + + IF_NONE_MATCH = 'If-None-Match', + If_MODIFIED_SINCE = 'If-Modified-Since', + + E_MODIFIED = 'io:modified', + + isString = Y.Lang.isString, + isNumber = Y.Lang.isNumber, + isObject = Y.Lang.isObject; + + /** + * Create a polling task to continually check the server at the specified interval for updates of a resource at a URI. + * The poller will use conditional GET requests and notify the client via Callbacks and Events when the resource has changed. + * + * @class Poller + * @extends Base + * @param {Object} config Configuration Object + * @constructor + */ + Poller = function (config) { + + Poller.superclass.constructor.apply( this, arguments ); + }; + + Y.mix( Poller, { + + /** + * The identity of the component. + * + * @property Poller.NAME + * @type String + * @static + */ + NAME : POLLER, + + /** + * Static property used to define the default attribute configuration of + * the component. + * + * @property Poller.ATTRS + * @type Object + * @static + */ + ATTRS : { + + /** + * The time in milliseconds for which the component should send a request to the server. + * + * @attribute interval + * @type Number + */ + interval : { + validator : isNumber + }, + + /** + * The URI of the resource which to xhr-poll. + * + * @attribute uri + * @type String + * @writeOnce + */ + uri : { + validator : isString, + writeOnce : true + }, + + /** + * The configuration Y.io config. + * + * @attribute ioConfig + * @type Object + * @writeOnce + */ + ioConfig : { + validator : isObject, + writeOnce : true + }, + + /** + * A read-only attribute the client can check to see if the component is actively polling the server. + * + * @attribute polling + * @type Boolean + * @default false + * @readOnly + * @final + */ + polling : { + value : false, + readOnly : true + } + + } + + }); + + Y.extend( Poller, Y.Base, { + + /** + * Timer object to schedule next request to server. + * + * @property _timer + * @type Y.later + * @protected + */ + _timer : null, + + /** + * Time of the last request was sent. + * + * @property _txTime + * @type Number + * @protected + */ + _txTime : null, + + /** + * active IO transaction Object + * + * @property _activeTx + * @type Object + * @protected + */ + _activeTx : null, + + /** + * Last-Modified date of the resource that the server returned, used to determine if the resource has changed. + * + * @property _modifiedDate + * @type String + * @protected + */ + _modifiedDate : null, + + /** + * Etag of the resource returned by the server, used to determine if the resource has changed. + * + * @property _etag + * @type String + * @protected + */ + _etag : null, + + /** + * Construction of the component. + * + * @method initializer + * @param {Object} config Configuration Object + * @protected + */ + initializer : function (config) { + + /** + * Signals that the resource the component is pulling the server for has been modified. + * This is the interesting event for the client to subscribe to. + * The subscriber could, for example, update the UI in response to this event, poller:modified. + * + * @event io:modified + * @param {Number} txId Y.io Transaction ID + * @param {Object} r Y.io Response Object + * @param {MIXED} args Arguments passed to response handler + */ + + this._timer = null; + this._txTime = null; + this._activeTx = null; + this._modifiedDate = null; + this._etag = null; + }, + + /** + * Destruction of the component. Stops polling and cleans up. + * + * @method destructor + * @protected + */ + destructor : function () { + + this.stop(); + this._modifiedDate = null; + this._etag = null; + }, + + /** + * Starts the polling task. + * A request will be sent to the server right at the time of calling this method; + * and continued by sending subsequent requests at the set interval. + * To stop or pause polling call the stop method. + * + * @method start + */ + start : function () { + + if ( ! this.get(POLLING)) { + this._sendRequest(); + this._set(POLLING, true); + } + }, + + /** + * Stops the polling task. + * Start can be called to resume polling. + * + * @method stop + */ + stop : function () { + + if (this._activeTx) { + this._activeTx.abort(); + this._activeTx = null; + } + + if (this._timer) { + this._timer.cancel(); + this._timer = null; + } + + this._txTime = null; + + this._set(POLLING, false); + }, + + /** + * Sends the XHR request to the server at the given URI (resource). + * Method used internally to make the XHR requests. + * + * @method sendRequest + * @protected + */ + _sendRequest : function () { + + var ioConfig = this.get(IO_CONFIG), + headers = {}, + config; + + if (this._etag) { + headers[IF_NONE_MATCH] = this._etag; + } + if (this._modifiedDate) { + headers[If_MODIFIED_SINCE] = this._modifiedDate; + } + + config = Y.merge(ioConfig, { + + headers : Y.merge(ioConfig.headers, headers), + context : this, + on : Y.merge(ioConfig.on, { + + start : this._defStartHandler, + complete : this._defCompleteHandler, + success : this._defSuccessHandler, + modified : this._defModifiedHandler + + }) + + }); + + this._activeTx = Y.io(this.get(URI), config); + }, + + /** + * Sets the txTime and calls the config's on.start handler. + * + * @method _defStartHandler + * @param {Number} txId Y.io Transaction ID + * @param {MIXED} args Arguments passed to handlers + * @protected + */ + _defStartHandler : function (txId, args) { + + var config = this.get(IO_CONFIG); + + this._txTime = new Date().getTime(); + + if (config && config.on && config.on.start) { + config.on.start.apply(config.context || Y, arguments); + } + }, + + /** + * Schedules the next transaction and calls the config's on.complete handler. + * + * @method _defCompleteHandler + * @param {Number} txId Y.io Transaction ID + * @param {Object} r Y.io Response Object + * @param {MIXED} args Arguments passed to handlers + * @protected + */ + _defCompleteHandler : function (txId, r, args) { + + var config = this.get(IO_CONFIG), + deltaT = this._txTime ? (new Date()).getTime() - this._txTime : 0; + + this._timer = Y.later(Math.max(this.get(INTERVAL) - deltaT, 0), this, this._sendRequest); + + if (config && config.on && config.on.complete) { + config.on.complete.apply(config.context || Y, arguments); + } + }, + + /** + * Chains to the modified callback and calls the config's on.success handler. + * + * @method _defSuccessHandler + * @param {Number} txId Y.io Transaction ID + * @param {Object} r Y.io Response Object + * @param {MIXED} args Arguments passed to handlers + * @protected + */ + _defSuccessHandler : function (txId, r, args) { + + var config = this.get(IO_CONFIG); + + if (config && config.on && config.on.success) { + config.on.success.apply(config.context || Y, arguments); + } + + this._defModifiedHandler.apply(this, arguments); + }, + + /** + * Caches Etag and Last-Modified sent back from the server and calls the config's on.modified handler. + * + * @method _defModifiedHandler + * @param {Number} txId Y.io Transaction ID + * @param {Object} r Y.io Response Object + * @param {MIXED} args Arguments passed to handlers + * @protected + */ + _defModifiedHandler : function (txId, r, args) { + + var config = this.get(IO_CONFIG); + + this._etag = r.getResponseHeader('Etag'); + this._modifiedDate = r.getResponseHeader('Last-Modified'); + + Y.fire(E_MODIFIED, txId, r); + + if (config && config.on && config.on.modified) { + config.on.modified.apply(config.context || Y, arguments); + } + } + + }); + + /** + * Method for scheduling a XHR-polling task. Returns an instance of Poller + * + * @method Y.io.poll + * @param {Number} interval The time in milliseconds for which the component should send a request to the server. + * @param {Object} uri qualified path to transaction resource. + * @param {Object} config configuration object for the transaction(s); just like Y.io's config object, but with an on:modified callback/event. + * @return {Poller} an instance of Poller which has start/stop methods and a configurable interval + * @public + * @static + */ + Y.mix(Y.io, { + + poll: function (interval, uri, config) { + return new Poller({ + interval : interval, + uri : uri, + ioConfig : config + }); + } + + }, true); + + + +}, 'gallery-2009.11.19-20' ,{requires:['io-base', 'base-base']}); diff --git a/build/gallery-io-poller/gallery-io-poller-min.js b/build/gallery-io-poller/gallery-io-poller-min.js new file mode 100644 index 0000000000..f1ee9bb6e9 --- /dev/null +++ b/build/gallery-io-poller/gallery-io-poller-min.js @@ -0,0 +1 @@ +YUI.add("gallery-io-poller",function(B){var C,I="poller",D="interval",H="uri",J="ioConfig",K="polling",E="If-None-Match",G="If-Modified-Since",F="io:modified",A=B.Lang.isString,M=B.Lang.isNumber,L=B.Lang.isObject;C=function(N){C.superclass.constructor.apply(this,arguments);};B.mix(C,{NAME:I,ATTRS:{interval:{validator:M},uri:{validator:A,writeOnce:true},ioConfig:{validator:L,writeOnce:true},polling:{value:false,readOnly:true}}});B.extend(C,B.Base,{_timer:null,_txTime:null,_activeTx:null,_modifiedDate:null,_etag:null,initializer:function(N){this._timer=null;this._txTime=null;this._activeTx=null;this._modifiedDate=null;this._etag=null;},destructor:function(){this.stop();this._modifiedDate=null;this._etag=null;},start:function(){if(!this.get(K)){this._sendRequest();this._set(K,true);}},stop:function(){if(this._activeTx){this._activeTx.abort();this._activeTx=null;}if(this._timer){this._timer.cancel();this._timer=null;}this._txTime=null;this._set(K,false);},_sendRequest:function(){var N=this.get(J),P={},O;if(this._etag){P[E]=this._etag;}if(this._modifiedDate){P[G]=this._modifiedDate;}O=B.merge(N,{headers:B.merge(N.headers,P),context:this,on:B.merge(N.on,{start:this._defStartHandler,complete:this._defCompleteHandler,success:this._defSuccessHandler,modified:this._defModifiedHandler})});this._activeTx=B.io(this.get(H),O);},_defStartHandler:function(P,O){var N=this.get(J);this._txTime=new Date().getTime();if(N&&N.on&&N.on.start){N.on.start.apply(N.context||B,arguments);}},_defCompleteHandler:function(R,Q,O){var N=this.get(J),P=this._txTime?(new Date()).getTime()-this._txTime:0;this._timer=B.later(Math.max(this.get(D)-P,0),this,this._sendRequest);if(N&&N.on&&N.on.complete){N.on.complete.apply(N.context||B,arguments);}},_defSuccessHandler:function(Q,P,O){var N=this.get(J);if(N&&N.on&&N.on.success){N.on.success.apply(N.context||B,arguments);}this._defModifiedHandler.apply(this,arguments);},_defModifiedHandler:function(Q,P,O){var N=this.get(J);this._etag=P.getResponseHeader("Etag");this._modifiedDate=P.getResponseHeader("Last-Modified");B.fire(F,Q,P);if(N&&N.on&&N.on.modified){N.on.modified.apply(N.context||B,arguments);}}});B.mix(B.io,{poll:function(N,P,O){return new C({interval:N,uri:P,ioConfig:O});}},true);},"gallery-2009.11.19-20",{requires:["io-base","base-base"]}); \ No newline at end of file diff --git a/build/gallery-io-poller/gallery-io-poller.js b/build/gallery-io-poller/gallery-io-poller.js new file mode 100644 index 0000000000..1587014d9e --- /dev/null +++ b/build/gallery-io-poller/gallery-io-poller.js @@ -0,0 +1,397 @@ +YUI.add('gallery-io-poller', function(Y) { + + /** + * IO Poller - A utility to smartly poll (via XHR requests) a resource on the server. + * http://925html.com/code/smart-polling/ + * + * Oddnut Software + * Copyright (c) 2009 Eric Ferraiuolo - http://eric.ferraiuolo.name + * YUI BSD License - http://developer.yahoo.com/yui/license.html + */ + + /** + * Extends Y.io to add support for xhr-polling. + * + * @module poller + * @requires io-base, base + */ + + var Poller, + POLLER = 'poller', + + INTERVAL = 'interval', + URI = 'uri', + IO_CONFIG = 'ioConfig', + POLLING = 'polling', + + IF_NONE_MATCH = 'If-None-Match', + If_MODIFIED_SINCE = 'If-Modified-Since', + + E_MODIFIED = 'io:modified', + + isString = Y.Lang.isString, + isNumber = Y.Lang.isNumber, + isObject = Y.Lang.isObject; + + /** + * Create a polling task to continually check the server at the specified interval for updates of a resource at a URI. + * The poller will use conditional GET requests and notify the client via Callbacks and Events when the resource has changed. + * + * @class Poller + * @extends Base + * @param {Object} config Configuration Object + * @constructor + */ + Poller = function (config) { + + Poller.superclass.constructor.apply( this, arguments ); + }; + + Y.mix( Poller, { + + /** + * The identity of the component. + * + * @property Poller.NAME + * @type String + * @static + */ + NAME : POLLER, + + /** + * Static property used to define the default attribute configuration of + * the component. + * + * @property Poller.ATTRS + * @type Object + * @static + */ + ATTRS : { + + /** + * The time in milliseconds for which the component should send a request to the server. + * + * @attribute interval + * @type Number + */ + interval : { + validator : isNumber + }, + + /** + * The URI of the resource which to xhr-poll. + * + * @attribute uri + * @type String + * @writeOnce + */ + uri : { + validator : isString, + writeOnce : true + }, + + /** + * The configuration Y.io config. + * + * @attribute ioConfig + * @type Object + * @writeOnce + */ + ioConfig : { + validator : isObject, + writeOnce : true + }, + + /** + * A read-only attribute the client can check to see if the component is actively polling the server. + * + * @attribute polling + * @type Boolean + * @default false + * @readOnly + * @final + */ + polling : { + value : false, + readOnly : true + } + + } + + }); + + Y.extend( Poller, Y.Base, { + + /** + * Timer object to schedule next request to server. + * + * @property _timer + * @type Y.later + * @protected + */ + _timer : null, + + /** + * Time of the last request was sent. + * + * @property _txTime + * @type Number + * @protected + */ + _txTime : null, + + /** + * active IO transaction Object + * + * @property _activeTx + * @type Object + * @protected + */ + _activeTx : null, + + /** + * Last-Modified date of the resource that the server returned, used to determine if the resource has changed. + * + * @property _modifiedDate + * @type String + * @protected + */ + _modifiedDate : null, + + /** + * Etag of the resource returned by the server, used to determine if the resource has changed. + * + * @property _etag + * @type String + * @protected + */ + _etag : null, + + /** + * Construction of the component. + * + * @method initializer + * @param {Object} config Configuration Object + * @protected + */ + initializer : function (config) { + + /** + * Signals that the resource the component is pulling the server for has been modified. + * This is the interesting event for the client to subscribe to. + * The subscriber could, for example, update the UI in response to this event, poller:modified. + * + * @event io:modified + * @param {Number} txId Y.io Transaction ID + * @param {Object} r Y.io Response Object + * @param {MIXED} args Arguments passed to response handler + */ + + this._timer = null; + this._txTime = null; + this._activeTx = null; + this._modifiedDate = null; + this._etag = null; + }, + + /** + * Destruction of the component. Stops polling and cleans up. + * + * @method destructor + * @protected + */ + destructor : function () { + + this.stop(); + this._modifiedDate = null; + this._etag = null; + }, + + /** + * Starts the polling task. + * A request will be sent to the server right at the time of calling this method; + * and continued by sending subsequent requests at the set interval. + * To stop or pause polling call the stop method. + * + * @method start + */ + start : function () { + + if ( ! this.get(POLLING)) { + this._sendRequest(); + this._set(POLLING, true); + } + }, + + /** + * Stops the polling task. + * Start can be called to resume polling. + * + * @method stop + */ + stop : function () { + + if (this._activeTx) { + this._activeTx.abort(); + this._activeTx = null; + } + + if (this._timer) { + this._timer.cancel(); + this._timer = null; + } + + this._txTime = null; + + this._set(POLLING, false); + }, + + /** + * Sends the XHR request to the server at the given URI (resource). + * Method used internally to make the XHR requests. + * + * @method sendRequest + * @protected + */ + _sendRequest : function () { + + var ioConfig = this.get(IO_CONFIG), + headers = {}, + config; + + if (this._etag) { + headers[IF_NONE_MATCH] = this._etag; + } + if (this._modifiedDate) { + headers[If_MODIFIED_SINCE] = this._modifiedDate; + } + + config = Y.merge(ioConfig, { + + headers : Y.merge(ioConfig.headers, headers), + context : this, + on : Y.merge(ioConfig.on, { + + start : this._defStartHandler, + complete : this._defCompleteHandler, + success : this._defSuccessHandler, + modified : this._defModifiedHandler + + }) + + }); + + this._activeTx = Y.io(this.get(URI), config); + }, + + /** + * Sets the txTime and calls the config's on.start handler. + * + * @method _defStartHandler + * @param {Number} txId Y.io Transaction ID + * @param {MIXED} args Arguments passed to handlers + * @protected + */ + _defStartHandler : function (txId, args) { + + var config = this.get(IO_CONFIG); + + this._txTime = new Date().getTime(); + + if (config && config.on && config.on.start) { + config.on.start.apply(config.context || Y, arguments); + } + }, + + /** + * Schedules the next transaction and calls the config's on.complete handler. + * + * @method _defCompleteHandler + * @param {Number} txId Y.io Transaction ID + * @param {Object} r Y.io Response Object + * @param {MIXED} args Arguments passed to handlers + * @protected + */ + _defCompleteHandler : function (txId, r, args) { + + var config = this.get(IO_CONFIG), + deltaT = this._txTime ? (new Date()).getTime() - this._txTime : 0; + + this._timer = Y.later(Math.max(this.get(INTERVAL) - deltaT, 0), this, this._sendRequest); + + if (config && config.on && config.on.complete) { + config.on.complete.apply(config.context || Y, arguments); + } + }, + + /** + * Chains to the modified callback and calls the config's on.success handler. + * + * @method _defSuccessHandler + * @param {Number} txId Y.io Transaction ID + * @param {Object} r Y.io Response Object + * @param {MIXED} args Arguments passed to handlers + * @protected + */ + _defSuccessHandler : function (txId, r, args) { + + var config = this.get(IO_CONFIG); + + if (config && config.on && config.on.success) { + config.on.success.apply(config.context || Y, arguments); + } + + this._defModifiedHandler.apply(this, arguments); + }, + + /** + * Caches Etag and Last-Modified sent back from the server and calls the config's on.modified handler. + * + * @method _defModifiedHandler + * @param {Number} txId Y.io Transaction ID + * @param {Object} r Y.io Response Object + * @param {MIXED} args Arguments passed to handlers + * @protected + */ + _defModifiedHandler : function (txId, r, args) { + + var config = this.get(IO_CONFIG); + + this._etag = r.getResponseHeader('Etag'); + this._modifiedDate = r.getResponseHeader('Last-Modified'); + + Y.fire(E_MODIFIED, txId, r); + + if (config && config.on && config.on.modified) { + config.on.modified.apply(config.context || Y, arguments); + } + } + + }); + + /** + * Method for scheduling a XHR-polling task. Returns an instance of Poller + * + * @method Y.io.poll + * @param {Number} interval The time in milliseconds for which the component should send a request to the server. + * @param {Object} uri qualified path to transaction resource. + * @param {Object} config configuration object for the transaction(s); just like Y.io's config object, but with an on:modified callback/event. + * @return {Poller} an instance of Poller which has start/stop methods and a configurable interval + * @public + * @static + */ + Y.mix(Y.io, { + + poll: function (interval, uri, config) { + return new Poller({ + interval : interval, + uri : uri, + ioConfig : config + }); + } + + }, true); + + + +}, 'gallery-2009.11.19-20' ,{requires:['io-base', 'base-base']}); diff --git a/build/gallery-node-accordion/assets/gallery-node-accordion-core.css b/build/gallery-node-accordion/assets/gallery-node-accordion-core.css new file mode 100644 index 0000000000..564af43e02 --- /dev/null +++ b/build/gallery-node-accordion/assets/gallery-node-accordion-core.css @@ -0,0 +1,32 @@ +.yui-accordion { + position: relative; + zoom: 1; +} +.yui-accordion .yui-accordion-item { + display: block; +} +.yui-accordion .yui-accordion-item .yui-accordion-item-hd { +} +.yui-accordion .yui-accordion-item .yui-accordion-item-bd { + height: 0; + _height: 1px; + *height: 1px; + overflow: hidden; + zoom: 1; +} +.yui-accordion .yui-accordion-item-active .yui-accordion-item-bd { + height: auto; +} +.yui-accordion-hidden { + /* absolute position off-screen and box collapsing used to + avoid display:none, which causes issues for some content (firefox + restarts flash movies) */ + border:0; + height:0; + width:0; + padding:0; + position:absolute; + left:-999999px; + overflow:hidden; + visibility:hidden; +} \ No newline at end of file diff --git a/build/gallery-node-accordion/assets/skins/sam/gallery-node-accordion-skin.css b/build/gallery-node-accordion/assets/skins/sam/gallery-node-accordion-skin.css new file mode 100644 index 0000000000..76c93dde69 --- /dev/null +++ b/build/gallery-node-accordion/assets/skins/sam/gallery-node-accordion-skin.css @@ -0,0 +1,43 @@ +.yui-skin-sam .yui-accordion { + font-size: 93%; /* 12px */ + line-height: 1.5; /* 18px */ + *line-height: 1.45; /* For IE */ + border-top: solid 1px #808080; + border-left: solid 1px #808080; + border-right: solid 1px #808080; + background: #fff; + padding: 0; + text-align: left; +} +.yui-skin-sam .yui-accordion .yui-accordion-item { + display: block; + border-bottom: solid 1px #808080; +} +.yui-skin-sam .yui-accordion .yui-accordion-item .yui-accordion-item-hd { + line-height: 2; /* ~24px */ + *line-height: 1.9; /* For IE */ + background: url(http://yui.yahooapis.com/3.0.0/build/assets/skins/sam/sprite.png) repeat-x 0 0; + padding: 0; + padding: 5px; +} +.yui-skin-sam .yui-accordion .yui-accordion-item .yui-accordion-item-bd { + font-size: 100%; + margin: 0; + padding: 0; + display: block; +} +.yui-skin-sam .yui-accordion .first-of-type { + +} +.yui-skin-sam .yui-accordion .yui-accordion-item-hd a.yui-accordion-item-trigger { + width: auto; + display: block; + color: #000; + text-decoration: none; + cursor: default; + padding: 0 5px 0 10px; + background: url(http://yui.yahooapis.com/3.0.0/build/assets/skins/sam/sprite.png) no-repeat 110% -345px; +} +.yui-skin-sam .yui-accordion .yui-accordion-item-active .yui-accordion-item-hd a.yui-accordion-item-trigger { + background: url(http://yui.yahooapis.com/3.0.0/build/assets/skins/sam/sprite.png) no-repeat 110% -395px; +} \ No newline at end of file diff --git a/build/gallery-node-accordion/assets/skins/sam/gallery-node-accordion.css b/build/gallery-node-accordion/assets/skins/sam/gallery-node-accordion.css new file mode 100644 index 0000000000..d16bf3f72b --- /dev/null +++ b/build/gallery-node-accordion/assets/skins/sam/gallery-node-accordion.css @@ -0,0 +1 @@ +.yui-accordion{position:relative;zoom:1;}.yui-accordion .yui-accordion-item{display:block;}.yui-accordion .yui-accordion-item .yui-accordion-item-bd{height:0;_height:1px;*height:1px;overflow:hidden;zoom:1;}.yui-accordion .yui-accordion-item-active .yui-accordion-item-bd{height:auto;}.yui-accordion-hidden{border:0;height:0;width:0;padding:0;position:absolute;left:-999999px;overflow:hidden;visibility:hidden;}.yui-skin-sam .yui-accordion{font-size:93%;line-height:1.5;*line-height:1.45;border-top:solid 1px #808080;border-left:solid 1px #808080;border-right:solid 1px #808080;background:#fff;padding:0;text-align:left;}.yui-skin-sam .yui-accordion .yui-accordion-item{display:block;border-bottom:solid 1px #808080;}.yui-skin-sam .yui-accordion .yui-accordion-item .yui-accordion-item-hd{line-height:2;*line-height:1.9;background:url(http://yui.yahooapis.com/3.0.0/build/assets/skins/sam/sprite.png) repeat-x 0 0;padding:0;padding:5px;}.yui-skin-sam .yui-accordion .yui-accordion-item .yui-accordion-item-bd{font-size:100%;margin:0;padding:0;display:block;}.yui-skin-sam .yui-accordion .yui-accordion-item-hd a.yui-accordion-item-trigger{width:auto;display:block;color:#000;text-decoration:none;cursor:default;padding:0 5px 0 10px;background:url(http://yui.yahooapis.com/3.0.0/build/assets/skins/sam/sprite.png) no-repeat 110% -345px;}.yui-skin-sam .yui-accordion .yui-accordion-item-active .yui-accordion-item-hd a.yui-accordion-item-trigger{background:url(http://yui.yahooapis.com/3.0.0/build/assets/skins/sam/sprite.png) no-repeat 110% -395px;} diff --git a/build/gallery-node-accordion/gallery-node-accordion-debug.js b/build/gallery-node-accordion/gallery-node-accordion-debug.js new file mode 100644 index 0000000000..28fedb7c36 --- /dev/null +++ b/build/gallery-node-accordion/gallery-node-accordion-debug.js @@ -0,0 +1,597 @@ +YUI.add('gallery-node-accordion', function(Y) { + +/** +*

        The Accordion Node Plugin makes it easy to transform existing +* markup into an accordion element with expandable and collapsable elements, +* elements are easy to customize, and only require a small set of dependencies.

        +* +* +*

        To use the Accordion Node Plugin, simply pass a reference to the plugin to a +* Node instance's plug method.

        +* +*

        +* +* <script type="text/javascript">
        +*
        +* // Call the "use" method, passing in "gallery-node-accordion". This will
        +* // load the script and CSS for the Accordion Node Plugin and all of
        +* // the required dependencies.
        +*
        +* YUI().use("gallery-node-accordion", function(Y) {
        +*
        +* // Use the "contentready" event to initialize the accordion when
        +* // the element that represente the accordion
        +* // (<div id="accordion-1">) is ready to be scripted.
        +*
        +* Y.on("contentready", function () {
        +*
        +* // The scope of the callback will be a Node instance
        +* // representing the accordion (<div id="accordion-1">).
        +* // Therefore, since "this" represents a Node instance, it
        +* // is possible to just call "this.plug" passing in a
        +* // reference to the Accordion Node Plugin.
        +*
        +* this.plug(Y.Plugin.NodeAccordion);
        +*
        +* }, "#accordion-1");
        +*
        +* });
        +*
        +* </script>
        +*
        +*

        +* +*

        The Accordion Node Plugin has several configuration properties that can be +* set via an object literal that is passed as a second argument to a Node +* instance's plug method. +*

        +* +*

        +* +* <script type="text/javascript">
        +*
        +* // Call the "use" method, passing in "gallery-node-accordion". This will
        +* // load the script and CSS for the Accordion Node Plugin and all of
        +* // the required dependencies.
        +*
        +* YUI().use("gallery-node-accordion", function(Y) {
        +*
        +* // Use the "contentready" event to initialize the accordion when
        +* // the element that represente the accordion
        +* // (<div id="accordion-1">) is ready to be scripted.
        +*
        +* Y.on("contentready", function () {
        +*
        +* // The scope of the callback will be a Node instance
        +* // representing the accordion (<div id="accordion-1">).
        +* // Therefore, since "this" represents a Node instance, it
        +* // is possible to just call "this.plug" passing in a
        +* // reference to the Accordion Node Plugin.
        +*
        +* this.plug(Y.Plugin.NodeAccordion, { anim: true, effect: Y.Easing.backIn }); +*

        +* }, "#accordion-1");
        +*
        +* });
        +*
        +* </script>
        +*
        +*

        +* +* @module gallery-node-accordion +*/ + + +// Util shortcuts + +var UA = Y.UA, + getClassName = Y.ClassNameManager.getClassName, + anims = {}, + wheels = {fast:0.1,slow:0.6,normal:0.4}, + + // Frequently used strings + ACCORDION = "accordion", + ACCORDIONITEM = "item", + SCROLL_HEIGHT = "scrollHeight", + SCROLL_WIDTH = "scrollWidth", + WIDTH = "width", + HEIGHT = "height", + PX = "px", + PERIOD = ".", + HOST = "host", + + // Attribute keys + ATTR_ORIENTATION = 'orientation', + ATTR_FADE = 'fade', + ATTR_MULTIPLE = 'multiple', + ATTR_PERSISTENT = 'persistent', + ATTR_SPEED = 'speed', + ATTR_ANIM = 'anim', + ATTR_ITEMS = 'items', + + // CSS class names + CLASS_ACCORDION = getClassName(ACCORDION), + CLASS_ACCORDION_HIDDEN = getClassName(ACCORDION, 'hidden'), + CLASS_ACCORDION_ITEM = getClassName(ACCORDION, ACCORDIONITEM), + CLASS_ACTIVE = getClassName(ACCORDION, ACCORDIONITEM, "active"), + CLASS_SLIDING = getClassName(ACCORDION, ACCORDIONITEM, "sliding"), + CLASS_ACCORDION_ITEM_HD = getClassName(ACCORDION, ACCORDIONITEM, "hd"), + CLASS_ACCORDION_ITEM_BD = getClassName(ACCORDION, ACCORDIONITEM, "bd"), + CLASS_ACCORDION_ITEM_FT = getClassName(ACCORDION, ACCORDIONITEM, "ft"), + CLASS_ACCORDION_ITEM_TRIGGER = getClassName(ACCORDION, ACCORDIONITEM, "trigger"), + + // CSS selectors + SELECTOR_ACCORDION_ITEM = PERIOD + CLASS_ACCORDION_ITEM, + SELECTOR_ACCORDION_ITEM_BD = PERIOD + CLASS_ACCORDION_ITEM_BD, + // few more just in case... + //SELECTOR_ACCORDION = PERIOD + CLASS_ACCORDION, + //SELECTOR_ACCORDION_ITEM_TRIGGER = PERIOD + CLASS_ACCORDION_ITEM_TRIGGER, + + FC = '>.', + ITEM_QUERY = FC + CLASS_ACCORDION_ITEM, + ITEM_TRIGGER_QUERY = FC + CLASS_ACCORDION_ITEM + PERIOD + CLASS_ACCORDION_ITEM_TRIGGER + ',' + + FC + CLASS_ACCORDION_ITEM + FC + CLASS_ACCORDION_ITEM_HD + FC + CLASS_ACCORDION_ITEM_TRIGGER + ',' + + FC + CLASS_ACCORDION_ITEM + FC + CLASS_ACCORDION_ITEM_FT + FC + CLASS_ACCORDION_ITEM_TRIGGER, + // few more just in case... + //ITEM_HD_QUERY = FC+CLASS_ACCORDION_ITEM+FC+CLASS_ACCORDION_ITEM_HD, + //ITEM_BD_QUERY = FC+CLASS_ACCORDION_ITEM+FC+CLASS_ACCORDION_ITEM_BD, + //ITEM_FT_QUERY = FC+CLASS_ACCORDION_ITEM+FC+CLASS_ACCORDION_ITEM_FT, + + // Utility functions + /** + * The NodeAccordion class is a plugin for a Node instance. The class is used via + * the plug method of Node and + * should not be instantiated directly. + * @namespace plugin + * @class NodeAccordion + */ + NodeAccordion = function () { + + NodeAccordion.superclass.constructor.apply(this, arguments); + + }; + +NodeAccordion.NAME = "NodeAccordion"; +NodeAccordion.NS = ACCORDION; + +NodeAccordion.ATTRS = { + /** + * Nodes representing the list of active items. + * + * @attribute activeItems + * @type Y.NodeList + */ + activeItems: { + readOnly: true, + getter: function (value) { + return this._root.all(FC+CLASS_ACTIVE); + } + }, + /** + * Nodes representing the list of items. + * + * @attribute items + * @type Y.NodeList + */ + items: { + readOnly: true, + getter: function (value) { + return this._root.all(ITEM_QUERY); + } + }, + + /** + * orientation defines if the accordion will use width or height to expand and collapse items. + * + * @attribute orientation + * @writeOnce + * @default height + * @type string + */ + orientation: { + value: HEIGHT, + writeOnce: true + }, + /** + * Boolean indicating that animation should include opacity to fade in/out the content of the item. + * + * @attribute fade + * @default false + * @type boolean + */ + fade: { + value: false + }, + /** + * Boolean indicating that Y.Anim should be used to expand and collapse items. + * It also supports a function with an specific effect. + *

        + * + * <script type="text/javascript">
        + *
        + * // Call the "use" method, passing in "anim" and "gallery-node-accordion".
        + *
        + * YUI().use("anim", "gallery-node-accordion", function(Y) {
        + *
        + * Y.one("#myaccordion").plug(Y.Plugin.NodeAccordion, {
        + * anim: Y.Easing.backIn
        + * });
        + *
        + * </script>
        + *
        + *

        + * + * @attribute anim + * @default false + * @type {boolean|function} + */ + anim: { + value: false, + validator : function (v) { + return !Y.Lang.isUndefined(Y.Anim); + } + }, + /** + * Boolean indicating that more than one item can be opened at the same time. + * + * @attribute multiple + * @default true + * @type boolean + */ + multiple: { + value: true + }, + /** + * Boolean indicating that one of the items should be open at any given time. + * + * @attribute persistent + * @default false + * @type boolean + */ + persistent: { + value: false + }, + /** + * Numeric value indicating the speed in mili-seconds for the animation process. + * Also support three predefined strings in lowercase: + *
          + *
        1. fast = 0.1
        2. + *
        3. normal = 0.4
        4. + *
        5. slow = 0.6
        6. + *
        + * + * @attribute speed + * @readOnly + * @default 0.4 + * @type numeric + */ + speed: { + value: 0.4, + validator : function (v) { + return (Y.Lang.isNumber(v) || (Y.Lang.isString(v) && wheels.hasOwnProperty(v))); + }, + setter : function (v) { + return (wheels.hasOwnProperty(v)?wheels[v]:v); + } + } + +}; + + +Y.extend(NodeAccordion, Y.Plugin.Base, { + + // Protected properties + + /** + * @property _root + * @description Node instance representing the root node in the accordion. + * @default null + * @protected + * @type Node + */ + _root: null, + + // Public methods + + initializer: function (config) { + var _root = this.get(HOST), + aHandlers = []; + if (_root) { + + this._root = _root; + + // close all items and open the actived ones + this.get(ATTR_ITEMS).each(function(item) { + if (item.hasClass(CLASS_ACTIVE)) { + this.expandItem(item); + } else { + this.collapseItem(item); + } + }, this); + + // Wire up all event handlers + aHandlers.push(_root.delegate('click', function(e) { + Y.log ('Accordion Trigger: ' + e); + this.toggleItem(e.currentTarget); // probably is better to pass the ancestor for the item + e.target.blur(); + e.halt(); + }, ITEM_TRIGGER_QUERY, this)); + aHandlers = this._eventHandlers; + + _root.removeClass(CLASS_ACCORDION_HIDDEN); + } + }, + + destructor: function () { + var aHandlers = this._eventHandlers; + if (aHandlers) { + Y.Array.each(aHandlers, function (handle) { + handle.detach(); + }); + this._eventHandlers = null; + } + }, + + // Protected methods + /** + * @method _getItem + * @description Searching for an item based on a node reference or an index order. + * @protected + * @param {Node|Number} node Node reference or Node index. + * @return {Node} The matching DOM node or null if none found. + */ + _getItem: function(node) { + if (Y.Lang.isNumber(node)) { + node = this.get(ATTR_ITEMS).item(node); + } + var fn = function(n) { + return n.hasClass(CLASS_ACCORDION_ITEM); + }; + if (node && !node.hasClass(CLASS_ACCORDION_ITEM)) { + return node.ancestor( fn ); + } + return node; + }, + + /** + * @method _animate + * @description Using Y.Anim to expand or collapse an item. + * @protected + * @param {String} id Global Unique ID for the animation. + * @param {Object} conf Configuration object for the animation. + * @param {Function} fn callback function that should be executed after the end of the anim. + * @return {Object} Animation handler. + */ + _animate: function(id, conf, fn) { + var anim = anims[id]; + Y.log ('Anim Conf: ' + conf); + // if the animation is underway: we need to stop it... + if ((anim) && (anim.get ('running'))) { + anim.stop(); + } + if (Y.Lang.isFunction(this.get(ATTR_ANIM))) { + conf.easing = this.get(ATTR_ANIM); + } + anim = new Y.Anim(conf); + anim.on('end', fn, this); + anim.run(); + anims[id] = anim; + return anim; + }, + + /** + * @method _openItem + * @description Open an item. + * @protected + * @param {Node} item Node instance representing an item. + */ + _openItem: function (item) { + var bd, + id, + fn, + fs, + i, + list = this.get(ATTR_ITEMS), + o = this.get (ATTR_ORIENTATION), + conf = { + duration: this.get(ATTR_SPEED), + to: { + scroll: [] + } + }, + mirror; + // if the item is not already opened + if (item && list.size() && !item.hasClass(CLASS_ACTIVE) && (bd = item.one(SELECTOR_ACCORDION_ITEM_BD)) && (id = Y.stamp(bd))) { + // closing all the selected items if neccesary + if (!this.get(ATTR_MULTIPLE)) { + // close all items and open the actived ones + mirror = this._root.one(FC+CLASS_ACTIVE); + } + // opening the selected element, based on the orientation, timer and anim attributes... + conf.to[o] = (o==WIDTH?bd.get(SCROLL_WIDTH):bd.get(SCROLL_HEIGHT)); + conf.node = bd; + item.addClass(CLASS_SLIDING); + fn = function() { + item.removeClass(CLASS_SLIDING); + item.addClass(CLASS_ACTIVE); + // broadcasting the corresponding event (close)... + // $B.fire ('accordionOpenItem', item); + }; + if (!this.get(ATTR_ANIM)) { + // animation manually + // getting the desired dimension from the current item + fs = bd.get(o); + // override the desired dimension from the mirror if exists + if (Y.Lang.isObject(mirror)) { + fs = mirror.get(o); + mirror.addClass(CLASS_SLIDING); + } + for (i=1;i<=fs;i++){ + if (Y.Lang.isObject(mirror)) { + mirror.setStyle (o, (fs-i)+PX); + } + bd.setStyle (o, i+PX); + } + if (Y.Lang.isObject(mirror)) { + mirror.removeClass(CLASS_SLIDING); + mirror.removeClass(CLASS_ACTIVE); + } + fn(); + } else { + // scrolling effect + conf.to.scroll = [0,0]; + // appliying fadeIn + if (this.get(ATTR_FADE)) { + conf.to.opacity = 1; + } + if (Y.Lang.isObject(mirror)) { + this._closeItem(mirror); + } + this._animate(id, conf, fn); + } + } + }, + + /** + * @method _closeItem + * @description Closes the specified item. + * @protected + * @param {Node} item Node instance representing an item. + */ + _closeItem: function (item) { + + var bd, + id, + fn, + fs, + i, + list = this.get(ATTR_ITEMS), + o = this.get (ATTR_ORIENTATION), + conf = { + duration: this.get(ATTR_SPEED), + to: { + scroll: [] + } + }; + if (item && list.size() && (bd = item.one(SELECTOR_ACCORDION_ITEM_BD)) && (id = Y.stamp(bd))) { + // closing the item, based on the orientation, timer and anim attributes... + conf.to[o] = (((o==HEIGHT) && UA.ie && (UA.ie<7))?1:0); // hack for vertical accordion issue on Safari and Opera + conf.node = bd; + item.addClass(CLASS_SLIDING); + fn = function() { + item.removeClass(CLASS_SLIDING); + item.removeClass(CLASS_ACTIVE); + // broadcasting the corresponding event (close)... + // $B.fire ('accordionCloseItem', item); + }; + if (!this.get(ATTR_ANIM)) { + // animation manually + fs = bd.get(o); + for (i=fs;i>=conf.to[o].to;i--){ + bd.setStyle (o, i+PX); + } + fn(); + } else { + // scrolling effect + conf.to.scroll = (o==WIDTH?[bd.get(SCROLL_WIDTH),0]:[0,bd.get(SCROLL_HEIGHT)]); + // appliying fadeIn + if (this.get(ATTR_FADE)) { + conf.to.opacity = 0; + } + this._animate(id, conf, fn); + } + } + + }, + + // Generic DOM Event handlers + /** + * @method expandAllItems + * @description Expanding all items. + * @public + * @return {object} Plugin reference for chaining + */ + expandAllItems: function () { + Y.log(("Expanding all items (only if attr multiple=true): " + this._root), "info", "nodeAccordion"); + if (this.get(ATTR_MULTIPLE)) { + this.get(ATTR_ITEMS).each(function (node) { + this.expandItem(node); + }, this); + } + return this; + }, + + /** + * @method collapseAllItems + * @description Collapsing all items. + * @public + * @return {object} Plugin reference for chaining + */ + collapseAllItems: function () { + Y.log(("Collapsing all items (only if attr multiple=true or attr persistent=false): " + this._root), "info", "nodeAccordion"); + if (this.get(ATTR_MULTIPLE) || !this.get(ATTR_PERSISTENT)) { + this.get(ATTR_ITEMS).each(function (node) { + this.collapseItem(node); + }, this); + } + return this; + }, + + /** + * @method expandItem + * @description Expand a certain item. + * @public + * @param {Node} node Node reference + * @return {object} Plugin reference for chaining + */ + expandItem: function ( node ) { + var item = this._getItem(node); + if (item) { + Y.log(("Expanding an item: " + item), "info", "nodeAccordion"); + this._openItem (item); + } + return this; + }, + + /** + * @method collapseItem + * @description Collapse a certain item. + * @public + * @param {Node} node Node reference + * @return {object} Plugin reference for chaining + */ + collapseItem: function ( node ) { + var item = this._getItem(node); + if (item && item.hasClass(CLASS_ACTIVE) && (this.get(ATTR_MULTIPLE) || !this.get(ATTR_PERSISTENT))) { + Y.log(("Collapse an item: " + item), "info", "nodeAccordion"); + this._closeItem(item); + } + return this; + }, + + /** + * @method toggleItem + * @description toggle a certain item. + * @public + * @param {object} node Node reference + * @return {object} Plugin reference for chaining + */ + toggleItem: function ( node ) { + var item = this._getItem(node); + Y.log ('Looking for accordion item: ' + SELECTOR_ACCORDION_ITEM); + if (item) { + // if the item is already opened, and is multiple and not persistent + Y.log(("Toggling an item: " + item), "info", "nodeAccordion"); + ((item.hasClass(CLASS_ACTIVE) && (this.get(ATTR_MULTIPLE) || !this.get(ATTR_PERSISTENT)))?this._closeItem (item):this._openItem (item)); + } + return this; + } + +}); + +Y.namespace('Plugin'); + +Y.Plugin.NodeAccordion = NodeAccordion; + + +}, 'gallery-2009.10.27-23' ,{requires:['node-base', 'node-style', 'plugin', 'node-event-delegate', 'classnamemanager'], optional:['anim']}); diff --git a/build/gallery-node-accordion/gallery-node-accordion-min.js b/build/gallery-node-accordion/gallery-node-accordion-min.js new file mode 100644 index 0000000000..b6c8b159ff --- /dev/null +++ b/build/gallery-node-accordion/gallery-node-accordion-min.js @@ -0,0 +1 @@ +YUI.add("gallery-node-accordion",function(B){var S=B.UA,f=B.ClassNameManager.getClassName,X={},I={fast:0.1,slow:0.6,normal:0.4},J="accordion",E="item",a="scrollHeight",e="scrollWidth",F="width",V="height",i="px",G=".",O="host",j="orientation",D="fade",W="multiple",L="persistent",k="speed",c="anim",T="items",g=f(J),Z=f(J,"hidden"),H=f(J,E),M=f(J,E,"active"),A=f(J,E,"sliding"),d=f(J,E,"hd"),b=f(J,E,"bd"),K=f(J,E,"ft"),P=f(J,E,"trigger"),h=G+H,C=G+b,Q=">.",U=Q+H,R=Q+H+G+P+","+Q+H+Q+d+Q+P+","+Q+H+Q+K+Q+P,N=function(){N.superclass.constructor.apply(this,arguments);};N.NAME="NodeAccordion";N.NS=J;N.ATTRS={activeItems:{readOnly:true,getter:function(Y){return this._root.all(Q+M);}},items:{readOnly:true,getter:function(Y){return this._root.all(U);}},orientation:{value:V,writeOnce:true},fade:{value:false},anim:{value:false,validator:function(Y){return !B.Lang.isUndefined(B.Anim);}},multiple:{value:true},persistent:{value:false},speed:{value:0.4,validator:function(Y){return(B.Lang.isNumber(Y)||(B.Lang.isString(Y)&&I.hasOwnProperty(Y)));},setter:function(Y){return(I.hasOwnProperty(Y)?I[Y]:Y);}}};B.extend(N,B.Plugin.Base,{_root:null,initializer:function(l){var m=this.get(O),Y=[];if(m){this._root=m;this.get(T).each(function(n){if(n.hasClass(M)){this.expandItem(n);}else{this.collapseItem(n);}},this);Y.push(m.delegate("click",function(n){this.toggleItem(n.currentTarget);n.target.blur();n.halt();},R,this));Y=this._eventHandlers;m.removeClass(Z);}},destructor:function(){var Y=this._eventHandlers;if(Y){B.Array.each(Y,function(l){l.detach();});this._eventHandlers=null;}},_getItem:function(l){if(B.Lang.isNumber(l)){l=this.get(T).item(l);}var Y=function(m){return m.hasClass(H);};if(l&&!l.hasClass(H)){return l.ancestor(Y);}return l;},_animate:function(n,Y,l){var m=X[n];if((m)&&(m.get("running"))){m.stop();}if(B.Lang.isFunction(this.get(c))){Y.easing=this.get(c);}m=new B.Anim(Y);m.on("end",l,this);m.run();X[n]=m;return m;},_openItem:function(u){var p,Y,s,n,m,r=this.get(T),l=this.get(j),q={duration:this.get(k),to:{scroll:[]}},t;if(u&&r.size()&&!u.hasClass(M)&&(p=u.one(C))&&(Y=B.stamp(p))){if(!this.get(W)){t=this._root.one(Q+M);}q.to[l]=(l==F?p.get(e):p.get(a));q.node=p;u.addClass(A);s=function(){u.removeClass(A);u.addClass(M);};if(!this.get(c)){n=p.get(l);if(B.Lang.isObject(t)){n=t.get(l);t.addClass(A);}for(m=1;m<=n;m++){if(B.Lang.isObject(t)){t.setStyle(l,(n-m)+i);}p.setStyle(l,m+i);}if(B.Lang.isObject(t)){t.removeClass(A);t.removeClass(M);}s();}else{q.to.scroll=[0,0];if(this.get(D)){q.to.opacity=1;}if(B.Lang.isObject(t)){this._closeItem(t);}this._animate(Y,q,s);}}},_closeItem:function(t){var p,Y,s,n,m,r=this.get(T),l=this.get(j),q={duration:this.get(k),to:{scroll:[]}};if(t&&r.size()&&(p=t.one(C))&&(Y=B.stamp(p))){q.to[l]=(((l==V)&&S.ie&&(S.ie<7))?1:0);q.node=p;t.addClass(A);s=function(){t.removeClass(A);t.removeClass(M);};if(!this.get(c)){n=p.get(l);for(m=n;m>=q.to[l].to;m--){p.setStyle(l,m+i);}s();}else{q.to.scroll=(l==F?[p.get(e),0]:[0,p.get(a)]);if(this.get(D)){q.to.opacity=0;}this._animate(Y,q,s);}}},expandAllItems:function(){if(this.get(W)){this.get(T).each(function(Y){this.expandItem(Y);},this);}return this;},collapseAllItems:function(){if(this.get(W)||!this.get(L)){this.get(T).each(function(Y){this.collapseItem(Y);},this);}return this;},expandItem:function(l){var Y=this._getItem(l);if(Y){this._openItem(Y);}return this;},collapseItem:function(l){var Y=this._getItem(l);if(Y&&Y.hasClass(M)&&(this.get(W)||!this.get(L))){this._closeItem(Y);}return this;},toggleItem:function(l){var Y=this._getItem(l);if(Y){((Y.hasClass(M)&&(this.get(W)||!this.get(L)))?this._closeItem(Y):this._openItem(Y));}return this;}});B.namespace("Plugin");B.Plugin.NodeAccordion=N;},"gallery-2009.11.02-19",{requires:["node-base","node-style","plugin","node-event-delegate","classnamemanager"],optional:["anim"]}); \ No newline at end of file diff --git a/build/gallery-node-accordion/gallery-node-accordion.js b/build/gallery-node-accordion/gallery-node-accordion.js new file mode 100644 index 0000000000..7fe011ee11 --- /dev/null +++ b/build/gallery-node-accordion/gallery-node-accordion.js @@ -0,0 +1,589 @@ +YUI.add('gallery-node-accordion', function(Y) { + +/** +*

        The Accordion Node Plugin makes it easy to transform existing +* markup into an accordion element with expandable and collapsable elements, +* elements are easy to customize, and only require a small set of dependencies.

        +* +* +*

        To use the Accordion Node Plugin, simply pass a reference to the plugin to a +* Node instance's plug method.

        +* +*

        +* +* <script type="text/javascript">
        +*
        +* // Call the "use" method, passing in "gallery-node-accordion". This will
        +* // load the script and CSS for the Accordion Node Plugin and all of
        +* // the required dependencies.
        +*
        +* YUI().use("gallery-node-accordion", function(Y) {
        +*
        +* // Use the "contentready" event to initialize the accordion when
        +* // the element that represente the accordion
        +* // (<div id="accordion-1">) is ready to be scripted.
        +*
        +* Y.on("contentready", function () {
        +*
        +* // The scope of the callback will be a Node instance
        +* // representing the accordion (<div id="accordion-1">).
        +* // Therefore, since "this" represents a Node instance, it
        +* // is possible to just call "this.plug" passing in a
        +* // reference to the Accordion Node Plugin.
        +*
        +* this.plug(Y.Plugin.NodeAccordion);
        +*
        +* }, "#accordion-1");
        +*
        +* });
        +*
        +* </script>
        +*
        +*

        +* +*

        The Accordion Node Plugin has several configuration properties that can be +* set via an object literal that is passed as a second argument to a Node +* instance's plug method. +*

        +* +*

        +* +* <script type="text/javascript">
        +*
        +* // Call the "use" method, passing in "gallery-node-accordion". This will
        +* // load the script and CSS for the Accordion Node Plugin and all of
        +* // the required dependencies.
        +*
        +* YUI().use("gallery-node-accordion", function(Y) {
        +*
        +* // Use the "contentready" event to initialize the accordion when
        +* // the element that represente the accordion
        +* // (<div id="accordion-1">) is ready to be scripted.
        +*
        +* Y.on("contentready", function () {
        +*
        +* // The scope of the callback will be a Node instance
        +* // representing the accordion (<div id="accordion-1">).
        +* // Therefore, since "this" represents a Node instance, it
        +* // is possible to just call "this.plug" passing in a
        +* // reference to the Accordion Node Plugin.
        +*
        +* this.plug(Y.Plugin.NodeAccordion, { anim: true, effect: Y.Easing.backIn }); +*

        +* }, "#accordion-1");
        +*
        +* });
        +*
        +* </script>
        +*
        +*

        +* +* @module gallery-node-accordion +*/ + + +// Util shortcuts + +var UA = Y.UA, + getClassName = Y.ClassNameManager.getClassName, + anims = {}, + wheels = {fast:0.1,slow:0.6,normal:0.4}, + + // Frequently used strings + ACCORDION = "accordion", + ACCORDIONITEM = "item", + SCROLL_HEIGHT = "scrollHeight", + SCROLL_WIDTH = "scrollWidth", + WIDTH = "width", + HEIGHT = "height", + PX = "px", + PERIOD = ".", + HOST = "host", + + // Attribute keys + ATTR_ORIENTATION = 'orientation', + ATTR_FADE = 'fade', + ATTR_MULTIPLE = 'multiple', + ATTR_PERSISTENT = 'persistent', + ATTR_SPEED = 'speed', + ATTR_ANIM = 'anim', + ATTR_ITEMS = 'items', + + // CSS class names + CLASS_ACCORDION = getClassName(ACCORDION), + CLASS_ACCORDION_HIDDEN = getClassName(ACCORDION, 'hidden'), + CLASS_ACCORDION_ITEM = getClassName(ACCORDION, ACCORDIONITEM), + CLASS_ACTIVE = getClassName(ACCORDION, ACCORDIONITEM, "active"), + CLASS_SLIDING = getClassName(ACCORDION, ACCORDIONITEM, "sliding"), + CLASS_ACCORDION_ITEM_HD = getClassName(ACCORDION, ACCORDIONITEM, "hd"), + CLASS_ACCORDION_ITEM_BD = getClassName(ACCORDION, ACCORDIONITEM, "bd"), + CLASS_ACCORDION_ITEM_FT = getClassName(ACCORDION, ACCORDIONITEM, "ft"), + CLASS_ACCORDION_ITEM_TRIGGER = getClassName(ACCORDION, ACCORDIONITEM, "trigger"), + + // CSS selectors + SELECTOR_ACCORDION_ITEM = PERIOD + CLASS_ACCORDION_ITEM, + SELECTOR_ACCORDION_ITEM_BD = PERIOD + CLASS_ACCORDION_ITEM_BD, + // few more just in case... + //SELECTOR_ACCORDION = PERIOD + CLASS_ACCORDION, + //SELECTOR_ACCORDION_ITEM_TRIGGER = PERIOD + CLASS_ACCORDION_ITEM_TRIGGER, + + FC = '>.', + ITEM_QUERY = FC + CLASS_ACCORDION_ITEM, + ITEM_TRIGGER_QUERY = FC + CLASS_ACCORDION_ITEM + PERIOD + CLASS_ACCORDION_ITEM_TRIGGER + ',' + + FC + CLASS_ACCORDION_ITEM + FC + CLASS_ACCORDION_ITEM_HD + FC + CLASS_ACCORDION_ITEM_TRIGGER + ',' + + FC + CLASS_ACCORDION_ITEM + FC + CLASS_ACCORDION_ITEM_FT + FC + CLASS_ACCORDION_ITEM_TRIGGER, + // few more just in case... + //ITEM_HD_QUERY = FC+CLASS_ACCORDION_ITEM+FC+CLASS_ACCORDION_ITEM_HD, + //ITEM_BD_QUERY = FC+CLASS_ACCORDION_ITEM+FC+CLASS_ACCORDION_ITEM_BD, + //ITEM_FT_QUERY = FC+CLASS_ACCORDION_ITEM+FC+CLASS_ACCORDION_ITEM_FT, + + // Utility functions + /** + * The NodeAccordion class is a plugin for a Node instance. The class is used via + * the plug method of Node and + * should not be instantiated directly. + * @namespace plugin + * @class NodeAccordion + */ + NodeAccordion = function () { + + NodeAccordion.superclass.constructor.apply(this, arguments); + + }; + +NodeAccordion.NAME = "NodeAccordion"; +NodeAccordion.NS = ACCORDION; + +NodeAccordion.ATTRS = { + /** + * Nodes representing the list of active items. + * + * @attribute activeItems + * @type Y.NodeList + */ + activeItems: { + readOnly: true, + getter: function (value) { + return this._root.all(FC+CLASS_ACTIVE); + } + }, + /** + * Nodes representing the list of items. + * + * @attribute items + * @type Y.NodeList + */ + items: { + readOnly: true, + getter: function (value) { + return this._root.all(ITEM_QUERY); + } + }, + + /** + * orientation defines if the accordion will use width or height to expand and collapse items. + * + * @attribute orientation + * @writeOnce + * @default height + * @type string + */ + orientation: { + value: HEIGHT, + writeOnce: true + }, + /** + * Boolean indicating that animation should include opacity to fade in/out the content of the item. + * + * @attribute fade + * @default false + * @type boolean + */ + fade: { + value: false + }, + /** + * Boolean indicating that Y.Anim should be used to expand and collapse items. + * It also supports a function with an specific effect. + *

        + * + * <script type="text/javascript">
        + *
        + * // Call the "use" method, passing in "anim" and "gallery-node-accordion".
        + *
        + * YUI().use("anim", "gallery-node-accordion", function(Y) {
        + *
        + * Y.one("#myaccordion").plug(Y.Plugin.NodeAccordion, {
        + * anim: Y.Easing.backIn
        + * });
        + *
        + * </script>
        + *
        + *

        + * + * @attribute anim + * @default false + * @type {boolean|function} + */ + anim: { + value: false, + validator : function (v) { + return !Y.Lang.isUndefined(Y.Anim); + } + }, + /** + * Boolean indicating that more than one item can be opened at the same time. + * + * @attribute multiple + * @default true + * @type boolean + */ + multiple: { + value: true + }, + /** + * Boolean indicating that one of the items should be open at any given time. + * + * @attribute persistent + * @default false + * @type boolean + */ + persistent: { + value: false + }, + /** + * Numeric value indicating the speed in mili-seconds for the animation process. + * Also support three predefined strings in lowercase: + *
          + *
        1. fast = 0.1
        2. + *
        3. normal = 0.4
        4. + *
        5. slow = 0.6
        6. + *
        + * + * @attribute speed + * @readOnly + * @default 0.4 + * @type numeric + */ + speed: { + value: 0.4, + validator : function (v) { + return (Y.Lang.isNumber(v) || (Y.Lang.isString(v) && wheels.hasOwnProperty(v))); + }, + setter : function (v) { + return (wheels.hasOwnProperty(v)?wheels[v]:v); + } + } + +}; + + +Y.extend(NodeAccordion, Y.Plugin.Base, { + + // Protected properties + + /** + * @property _root + * @description Node instance representing the root node in the accordion. + * @default null + * @protected + * @type Node + */ + _root: null, + + // Public methods + + initializer: function (config) { + var _root = this.get(HOST), + aHandlers = []; + if (_root) { + + this._root = _root; + + // close all items and open the actived ones + this.get(ATTR_ITEMS).each(function(item) { + if (item.hasClass(CLASS_ACTIVE)) { + this.expandItem(item); + } else { + this.collapseItem(item); + } + }, this); + + // Wire up all event handlers + aHandlers.push(_root.delegate('click', function(e) { + this.toggleItem(e.currentTarget); // probably is better to pass the ancestor for the item + e.target.blur(); + e.halt(); + }, ITEM_TRIGGER_QUERY, this)); + aHandlers = this._eventHandlers; + + _root.removeClass(CLASS_ACCORDION_HIDDEN); + } + }, + + destructor: function () { + var aHandlers = this._eventHandlers; + if (aHandlers) { + Y.Array.each(aHandlers, function (handle) { + handle.detach(); + }); + this._eventHandlers = null; + } + }, + + // Protected methods + /** + * @method _getItem + * @description Searching for an item based on a node reference or an index order. + * @protected + * @param {Node|Number} node Node reference or Node index. + * @return {Node} The matching DOM node or null if none found. + */ + _getItem: function(node) { + if (Y.Lang.isNumber(node)) { + node = this.get(ATTR_ITEMS).item(node); + } + var fn = function(n) { + return n.hasClass(CLASS_ACCORDION_ITEM); + }; + if (node && !node.hasClass(CLASS_ACCORDION_ITEM)) { + return node.ancestor( fn ); + } + return node; + }, + + /** + * @method _animate + * @description Using Y.Anim to expand or collapse an item. + * @protected + * @param {String} id Global Unique ID for the animation. + * @param {Object} conf Configuration object for the animation. + * @param {Function} fn callback function that should be executed after the end of the anim. + * @return {Object} Animation handler. + */ + _animate: function(id, conf, fn) { + var anim = anims[id]; + // if the animation is underway: we need to stop it... + if ((anim) && (anim.get ('running'))) { + anim.stop(); + } + if (Y.Lang.isFunction(this.get(ATTR_ANIM))) { + conf.easing = this.get(ATTR_ANIM); + } + anim = new Y.Anim(conf); + anim.on('end', fn, this); + anim.run(); + anims[id] = anim; + return anim; + }, + + /** + * @method _openItem + * @description Open an item. + * @protected + * @param {Node} item Node instance representing an item. + */ + _openItem: function (item) { + var bd, + id, + fn, + fs, + i, + list = this.get(ATTR_ITEMS), + o = this.get (ATTR_ORIENTATION), + conf = { + duration: this.get(ATTR_SPEED), + to: { + scroll: [] + } + }, + mirror; + // if the item is not already opened + if (item && list.size() && !item.hasClass(CLASS_ACTIVE) && (bd = item.one(SELECTOR_ACCORDION_ITEM_BD)) && (id = Y.stamp(bd))) { + // closing all the selected items if neccesary + if (!this.get(ATTR_MULTIPLE)) { + // close all items and open the actived ones + mirror = this._root.one(FC+CLASS_ACTIVE); + } + // opening the selected element, based on the orientation, timer and anim attributes... + conf.to[o] = (o==WIDTH?bd.get(SCROLL_WIDTH):bd.get(SCROLL_HEIGHT)); + conf.node = bd; + item.addClass(CLASS_SLIDING); + fn = function() { + item.removeClass(CLASS_SLIDING); + item.addClass(CLASS_ACTIVE); + // broadcasting the corresponding event (close)... + // $B.fire ('accordionOpenItem', item); + }; + if (!this.get(ATTR_ANIM)) { + // animation manually + // getting the desired dimension from the current item + fs = bd.get(o); + // override the desired dimension from the mirror if exists + if (Y.Lang.isObject(mirror)) { + fs = mirror.get(o); + mirror.addClass(CLASS_SLIDING); + } + for (i=1;i<=fs;i++){ + if (Y.Lang.isObject(mirror)) { + mirror.setStyle (o, (fs-i)+PX); + } + bd.setStyle (o, i+PX); + } + if (Y.Lang.isObject(mirror)) { + mirror.removeClass(CLASS_SLIDING); + mirror.removeClass(CLASS_ACTIVE); + } + fn(); + } else { + // scrolling effect + conf.to.scroll = [0,0]; + // appliying fadeIn + if (this.get(ATTR_FADE)) { + conf.to.opacity = 1; + } + if (Y.Lang.isObject(mirror)) { + this._closeItem(mirror); + } + this._animate(id, conf, fn); + } + } + }, + + /** + * @method _closeItem + * @description Closes the specified item. + * @protected + * @param {Node} item Node instance representing an item. + */ + _closeItem: function (item) { + + var bd, + id, + fn, + fs, + i, + list = this.get(ATTR_ITEMS), + o = this.get (ATTR_ORIENTATION), + conf = { + duration: this.get(ATTR_SPEED), + to: { + scroll: [] + } + }; + if (item && list.size() && (bd = item.one(SELECTOR_ACCORDION_ITEM_BD)) && (id = Y.stamp(bd))) { + // closing the item, based on the orientation, timer and anim attributes... + conf.to[o] = (((o==HEIGHT) && UA.ie && (UA.ie<7))?1:0); // hack for vertical accordion issue on Safari and Opera + conf.node = bd; + item.addClass(CLASS_SLIDING); + fn = function() { + item.removeClass(CLASS_SLIDING); + item.removeClass(CLASS_ACTIVE); + // broadcasting the corresponding event (close)... + // $B.fire ('accordionCloseItem', item); + }; + if (!this.get(ATTR_ANIM)) { + // animation manually + fs = bd.get(o); + for (i=fs;i>=conf.to[o].to;i--){ + bd.setStyle (o, i+PX); + } + fn(); + } else { + // scrolling effect + conf.to.scroll = (o==WIDTH?[bd.get(SCROLL_WIDTH),0]:[0,bd.get(SCROLL_HEIGHT)]); + // appliying fadeIn + if (this.get(ATTR_FADE)) { + conf.to.opacity = 0; + } + this._animate(id, conf, fn); + } + } + + }, + + // Generic DOM Event handlers + /** + * @method expandAllItems + * @description Expanding all items. + * @public + * @return {object} Plugin reference for chaining + */ + expandAllItems: function () { + if (this.get(ATTR_MULTIPLE)) { + this.get(ATTR_ITEMS).each(function (node) { + this.expandItem(node); + }, this); + } + return this; + }, + + /** + * @method collapseAllItems + * @description Collapsing all items. + * @public + * @return {object} Plugin reference for chaining + */ + collapseAllItems: function () { + if (this.get(ATTR_MULTIPLE) || !this.get(ATTR_PERSISTENT)) { + this.get(ATTR_ITEMS).each(function (node) { + this.collapseItem(node); + }, this); + } + return this; + }, + + /** + * @method expandItem + * @description Expand a certain item. + * @public + * @param {Node} node Node reference + * @return {object} Plugin reference for chaining + */ + expandItem: function ( node ) { + var item = this._getItem(node); + if (item) { + this._openItem (item); + } + return this; + }, + + /** + * @method collapseItem + * @description Collapse a certain item. + * @public + * @param {Node} node Node reference + * @return {object} Plugin reference for chaining + */ + collapseItem: function ( node ) { + var item = this._getItem(node); + if (item && item.hasClass(CLASS_ACTIVE) && (this.get(ATTR_MULTIPLE) || !this.get(ATTR_PERSISTENT))) { + this._closeItem(item); + } + return this; + }, + + /** + * @method toggleItem + * @description toggle a certain item. + * @public + * @param {object} node Node reference + * @return {object} Plugin reference for chaining + */ + toggleItem: function ( node ) { + var item = this._getItem(node); + if (item) { + // if the item is already opened, and is multiple and not persistent + ((item.hasClass(CLASS_ACTIVE) && (this.get(ATTR_MULTIPLE) || !this.get(ATTR_PERSISTENT)))?this._closeItem (item):this._openItem (item)); + } + return this; + } + +}); + +Y.namespace('Plugin'); + +Y.Plugin.NodeAccordion = NodeAccordion; + + +}, 'gallery-2009.10.27-23' ,{requires:['node-base', 'node-style', 'plugin', 'node-event-delegate', 'classnamemanager'], optional:['anim']}); diff --git a/build/gallery-overlay-modal/gallery-overlay-modal-debug.js b/build/gallery-overlay-modal/gallery-overlay-modal-debug.js new file mode 100644 index 0000000000..ea1791f4fc --- /dev/null +++ b/build/gallery-overlay-modal/gallery-overlay-modal-debug.js @@ -0,0 +1,150 @@ +YUI.add('gallery-overlay-modal', function(Y) { + + /** + * Overlay Modal Plugin + * + * Oddnut Software + * Copyright (c) 2009 Eric Ferraiuolo - http://eric.ferraiuolo.name + * YUI BSD License - http://developer.yahoo.com/yui/license.html + */ + + var OverlayModal, + OVERLAY_MODAL = 'overlayModal', + + OVERLAY = 'overlay', + MODAL = 'modal', + MASK = 'mask', + + HOST = 'host', + BOUNDING_BOX = 'boundingBox', + + CHANGE = 'Change', + + getCN = Y.ClassNameManager.getClassName, + isBoolean = Y.Lang.isBoolean; + + // *** Constructor *** // + + OverlayModal = function (config) { + + OverlayModal.superclass.constructor.apply(this, arguments); + }; + + // *** Static *** // + + Y.mix(OverlayModal, { + + NAME : OVERLAY_MODAL, + + NS : MODAL, + + ATTRS : { + + mask : { + value : true, + validator : isBoolean + } + + }, + + CLASSES : { + + modal : getCN(OVERLAY, MODAL), + mask : getCN(OVERLAY, MASK) + + } + + }); + + // *** Prototype *** // + + Y.extend(OverlayModal, Y.Plugin.Base, { + + // *** Instance Members *** // + + _maskNode : null, + + // *** Lifecycle Methods *** // + + initializer : function (config) { + + this.doAfter('renderUI', this.renderUI); + this.doAfter('bindUI', this.bindUI); + this.doAfter('syncUI', this.syncUI); + + if (this.get(HOST).get('rendered')) { + this.renderUI(); + this.bindUI(); + this.syncUI(); + } + }, + + destructor : function () { + + if (this._maskNode) { + this._maskNode.remove(true); + } + + this.get(HOST).get(BOUNDING_BOX).removeClass(OverlayModal.CLASSES.modal); + }, + + renderUI : function () { + + this._maskNode = Y.Node.create('
        '); + this._maskNode.addClass(OverlayModal.CLASSES.mask); + this._maskNode.setStyles({ + position : 'fixed', + width : '100%', + height : '100%', + top : '0', + left : '0', + zIndex : '-1' + }); + + this.get(HOST).get(BOUNDING_BOX).addClass(OverlayModal.CLASSES.modal); + }, + + bindUI : function () { + + this.after(MASK+CHANGE, this._afterMaskChange); + }, + + syncUI : function () { + + this._uiSetMask(this.get(MASK)); + }, + + // *** Public Methods *** // + + mask : function () { + + this.set(MASK, true); + }, + + unmask : function () { + + this.set(MASK, false); + }, + + // *** Private Methods *** // + + _uiSetMask : function (mask) { + + if (mask) { + this.get(HOST).get(BOUNDING_BOX).append(this._maskNode); + } else { + this._maskNode.remove(); + } + }, + + _afterMaskChange : function (e) { + + this._uiSetMask(e.newVal); + } + + }); + + Y.namespace('Plugin').OverlayModal = OverlayModal; + + +}, 'gallery-2009.11.09-19' ,{requires:['overlay','plugin']}); diff --git a/build/gallery-overlay-modal/gallery-overlay-modal-min.js b/build/gallery-overlay-modal/gallery-overlay-modal-min.js new file mode 100644 index 0000000000..795437ac14 --- /dev/null +++ b/build/gallery-overlay-modal/gallery-overlay-modal-min.js @@ -0,0 +1 @@ +YUI.add("gallery-overlay-modal",function(B){var K,A="overlayModal",H="overlay",F="modal",J="mask",I="host",G="boundingBox",E="Change",D=B.ClassNameManager.getClassName,C=B.Lang.isBoolean;K=function(L){K.superclass.constructor.apply(this,arguments);};B.mix(K,{NAME:A,NS:F,ATTRS:{mask:{value:true,validator:C}},CLASSES:{modal:D(H,F),mask:D(H,J)}});B.extend(K,B.Plugin.Base,{_maskNode:null,initializer:function(L){this.doAfter("renderUI",this.renderUI);this.doAfter("bindUI",this.bindUI);this.doAfter("syncUI",this.syncUI);if(this.get(I).get("rendered")){this.renderUI();this.bindUI();this.syncUI();}},destructor:function(){if(this._maskNode){this._maskNode.remove(true);}this.get(I).get(G).removeClass(K.CLASSES.modal);},renderUI:function(){this._maskNode=B.Node.create("
        ");this._maskNode.addClass(K.CLASSES.mask);this._maskNode.setStyles({position:"fixed",width:"100%",height:"100%",top:"0",left:"0",zIndex:"-1"});this.get(I).get(G).addClass(K.CLASSES.modal);},bindUI:function(){this.after(J+E,this._afterMaskChange);},syncUI:function(){this._uiSetMask(this.get(J));},mask:function(){this.set(J,true);},unmask:function(){this.set(J,false);},_uiSetMask:function(L){if(L){this.get(I).get(G).append(this._maskNode);}else{this._maskNode.remove();}},_afterMaskChange:function(L){this._uiSetMask(L.newVal);}});B.namespace("Plugin").OverlayModal=K;},"gallery-2009.11.09-19",{requires:["overlay","plugin"]}); \ No newline at end of file diff --git a/build/gallery-overlay-modal/gallery-overlay-modal.js b/build/gallery-overlay-modal/gallery-overlay-modal.js new file mode 100644 index 0000000000..ea1791f4fc --- /dev/null +++ b/build/gallery-overlay-modal/gallery-overlay-modal.js @@ -0,0 +1,150 @@ +YUI.add('gallery-overlay-modal', function(Y) { + + /** + * Overlay Modal Plugin + * + * Oddnut Software + * Copyright (c) 2009 Eric Ferraiuolo - http://eric.ferraiuolo.name + * YUI BSD License - http://developer.yahoo.com/yui/license.html + */ + + var OverlayModal, + OVERLAY_MODAL = 'overlayModal', + + OVERLAY = 'overlay', + MODAL = 'modal', + MASK = 'mask', + + HOST = 'host', + BOUNDING_BOX = 'boundingBox', + + CHANGE = 'Change', + + getCN = Y.ClassNameManager.getClassName, + isBoolean = Y.Lang.isBoolean; + + // *** Constructor *** // + + OverlayModal = function (config) { + + OverlayModal.superclass.constructor.apply(this, arguments); + }; + + // *** Static *** // + + Y.mix(OverlayModal, { + + NAME : OVERLAY_MODAL, + + NS : MODAL, + + ATTRS : { + + mask : { + value : true, + validator : isBoolean + } + + }, + + CLASSES : { + + modal : getCN(OVERLAY, MODAL), + mask : getCN(OVERLAY, MASK) + + } + + }); + + // *** Prototype *** // + + Y.extend(OverlayModal, Y.Plugin.Base, { + + // *** Instance Members *** // + + _maskNode : null, + + // *** Lifecycle Methods *** // + + initializer : function (config) { + + this.doAfter('renderUI', this.renderUI); + this.doAfter('bindUI', this.bindUI); + this.doAfter('syncUI', this.syncUI); + + if (this.get(HOST).get('rendered')) { + this.renderUI(); + this.bindUI(); + this.syncUI(); + } + }, + + destructor : function () { + + if (this._maskNode) { + this._maskNode.remove(true); + } + + this.get(HOST).get(BOUNDING_BOX).removeClass(OverlayModal.CLASSES.modal); + }, + + renderUI : function () { + + this._maskNode = Y.Node.create('
        '); + this._maskNode.addClass(OverlayModal.CLASSES.mask); + this._maskNode.setStyles({ + position : 'fixed', + width : '100%', + height : '100%', + top : '0', + left : '0', + zIndex : '-1' + }); + + this.get(HOST).get(BOUNDING_BOX).addClass(OverlayModal.CLASSES.modal); + }, + + bindUI : function () { + + this.after(MASK+CHANGE, this._afterMaskChange); + }, + + syncUI : function () { + + this._uiSetMask(this.get(MASK)); + }, + + // *** Public Methods *** // + + mask : function () { + + this.set(MASK, true); + }, + + unmask : function () { + + this.set(MASK, false); + }, + + // *** Private Methods *** // + + _uiSetMask : function (mask) { + + if (mask) { + this.get(HOST).get(BOUNDING_BOX).append(this._maskNode); + } else { + this._maskNode.remove(); + } + }, + + _afterMaskChange : function (e) { + + this._uiSetMask(e.newVal); + } + + }); + + Y.namespace('Plugin').OverlayModal = OverlayModal; + + +}, 'gallery-2009.11.09-19' ,{requires:['overlay','plugin']}); diff --git a/build/gallery-port/gallery-port-debug.js b/build/gallery-port/gallery-port-debug.js new file mode 100644 index 0000000000..8bb4623d2b --- /dev/null +++ b/build/gallery-port/gallery-port-debug.js @@ -0,0 +1,169 @@ +YUI.add('gallery-port', function(Y) { + + var YAHOO = { + lang: Y.Lang, + extend: Y.extend, + util: { + Dom: Y.DOM, + Event: { + on: Y.Event.nativeAdd, + getTarget: function(ev) { + return ev.target; + }, + stopEvent: function(ev) { + ev.halt(); + }, + stopPropagation: function(ev) { + ev.stopPropagation(); + }, + preventDefault: function(ev) { + ev.preventDefault(); + } + }, + Element: Y.Base + }, + getAttributes: function() { + } + }, + Port, PortBase; + // HACK + // So the build system doesn't remove this line.. + YAHOO['log'] = Y['log']; + + YAHOO.util.Dom.get = function(id) { + if (Y.Lang.isString(id)) { + id = '#' + id; + } + return Y.get(id); + }; + YAHOO.util.Dom.getChildren = function(el) { + return Y.Selector.query('> *', el); + }; + + Port = function(args) { + return YAHOO; + }; + + Y.Port = Port; + + PortBase = function(attrs) { + this._lazyAddAttrs = false; + this._portAttrs = attrs; + PortBase.superclass.constructor.apply(this, arguments); + }; + + PortBase.NAME = 'PortBase'; + + PortBase.ATTRS = { + node: { + setter: function(node) { + var n = Y.get(node); + return n.item(0); + } + }, + element: { + getter: function() { + return this.get('node'); + }, + setter: function(n) { + this.set('node', n); + return this.get('node'); + } + } + }; + + Y.extend(PortBase, Y.Base, { + DOM_EVENTS: { + 'click': true, + 'dblclick': true, + 'keydown': true, + 'keypress': true, + 'keyup': true, + 'mousedown': true, + 'mousemove': true, + 'mouseout': true, + 'mouseover': true, + 'mouseup': true, + 'focus': true, + 'blur': true, + 'submit': true, + 'change': true + }, + initializer: function() { + this.initAttributes(this._portAttrs); + }, + initAttributes: function() { + }, + setAttributeConfig: function(name, config) { + if (config.method) { + config.setter = config.method; + delete config.method; + } + this.addAttr(name, config); + }, + addListener: function(name, fn, ctxt, args) { + if (ctxt) { + if (args) { + fn = Y.rbind(fn, args, ctxt); + } else { + fn = Y.bind(fn, ctxt); + } + } + if (this.DOM_EVENTS[name]) { + this.get('node').on(name, fn); + } else { + this.on(name, fn); + } + //console.log('addListener: ', arguments); + }, + getElementsByClassName: function(className, tagName) { + var sel = tagName + ' .' + className; + //console.log('getElementsByClassName', this.constructor.NAME, arguments, sel); + return this.get('node').query(sel) || []; + }, + getElementsByTagName: function(tagName) { + var n = this.get('node'); + return ((n) ? n.query(tagName) : []); + }, + addClass: function(className) { + var n = this.get('node'); + if (!n) { return false; } + return n.addClass(className); + }, + removeClass: function(className) { + var n = this.get('node'); + if (!n) { return false; } + return n.removeClass(className); + }, + hasClass: function(className) { + var n = this.get('node'); + if (!n) { return false; } + return n.hasClass(className); + }, + appendTo: function(el) { + Y.get(el).appendChild(this.get('node')); + }, + fireEvent: function(type, args) { + if (args.target) { + delete args.target; + } + if (args.ev) { + args.ev = new Y.DOMEventFacade(args.ev); + } + var ret = false, e; + this.publish(type, { + defaultFn: function() { + ret = true; + } + }); + e = this.fire(type, args); + Y.log('fireEvent: ' + type); + return ret; + } + }); + + Y.PortBase = PortBase; + + + +}, 'gallery-2009.11.02-20' ,{requires:['base','node']}); diff --git a/build/gallery-port/gallery-port-min.js b/build/gallery-port/gallery-port-min.js new file mode 100644 index 0000000000..d0e903d36d --- /dev/null +++ b/build/gallery-port/gallery-port-min.js @@ -0,0 +1 @@ +YUI.add("gallery-port",function(C){var B={lang:C.Lang,extend:C.extend,util:{Dom:C.DOM,Event:{on:C.Event.nativeAdd,getTarget:function(E){return E.target;},stopEvent:function(E){E.halt();},stopPropagation:function(E){E.stopPropagation();},preventDefault:function(E){E.preventDefault();}},Element:C.Base},getAttributes:function(){}},D,A;B["log"]=C["log"];B.util.Dom.get=function(E){if(C.Lang.isString(E)){E="#"+E;}return C.get(E);};B.util.Dom.getChildren=function(E){return C.Selector.query("> *",E);};D=function(E){return B;};C.Port=D;A=function(E){this._lazyAddAttrs=false;this._portAttrs=E;A.superclass.constructor.apply(this,arguments);};A.NAME="PortBase";A.ATTRS={node:{setter:function(E){var F=C.get(E);return F.item(0);}},element:{getter:function(){return this.get("node");},setter:function(E){this.set("node",E);return this.get("node");}}};C.extend(A,C.Base,{DOM_EVENTS:{"click":true,"dblclick":true,"keydown":true,"keypress":true,"keyup":true,"mousedown":true,"mousemove":true,"mouseout":true,"mouseover":true,"mouseup":true,"focus":true,"blur":true,"submit":true,"change":true},initializer:function(){this.initAttributes(this._portAttrs);},initAttributes:function(){},setAttributeConfig:function(F,E){if(E.method){E.setter=E.method;delete E.method;}this.addAttr(F,E);},addListener:function(G,H,E,F){if(E){if(F){H=C.rbind(H,F,E);}else{H=C.bind(H,E);}}if(this.DOM_EVENTS[G]){this.get("node").on(G,H);}else{this.on(G,H);}},getElementsByClassName:function(F,E){var G=E+" ."+F;return this.get("node").query(G)||[];},getElementsByTagName:function(E){var F=this.get("node");return((F)?F.query(E):[]);},addClass:function(E){var F=this.get("node");if(!F){return false;}return F.addClass(E);},removeClass:function(E){var F=this.get("node");if(!F){return false;}return F.removeClass(E);},hasClass:function(E){var F=this.get("node");if(!F){return false;}return F.hasClass(E);},appendTo:function(E){C.get(E).appendChild(this.get("node"));},fireEvent:function(G,F){if(F.target){delete F.target;}if(F.ev){F.ev=new C.DOMEventFacade(F.ev);}var E=false,H;this.publish(G,{defaultFn:function(){E=true;}});H=this.fire(G,F);return E;}});C.PortBase=A;},"gallery-2009.11.02-20",{requires:["base","node"]}); \ No newline at end of file diff --git a/build/gallery-port/gallery-port.js b/build/gallery-port/gallery-port.js new file mode 100644 index 0000000000..a8b6095737 --- /dev/null +++ b/build/gallery-port/gallery-port.js @@ -0,0 +1,168 @@ +YUI.add('gallery-port', function(Y) { + + var YAHOO = { + lang: Y.Lang, + extend: Y.extend, + util: { + Dom: Y.DOM, + Event: { + on: Y.Event.nativeAdd, + getTarget: function(ev) { + return ev.target; + }, + stopEvent: function(ev) { + ev.halt(); + }, + stopPropagation: function(ev) { + ev.stopPropagation(); + }, + preventDefault: function(ev) { + ev.preventDefault(); + } + }, + Element: Y.Base + }, + getAttributes: function() { + } + }, + Port, PortBase; + // HACK + // So the build system doesn't remove this line.. + YAHOO['log'] = Y['log']; + + YAHOO.util.Dom.get = function(id) { + if (Y.Lang.isString(id)) { + id = '#' + id; + } + return Y.get(id); + }; + YAHOO.util.Dom.getChildren = function(el) { + return Y.Selector.query('> *', el); + }; + + Port = function(args) { + return YAHOO; + }; + + Y.Port = Port; + + PortBase = function(attrs) { + this._lazyAddAttrs = false; + this._portAttrs = attrs; + PortBase.superclass.constructor.apply(this, arguments); + }; + + PortBase.NAME = 'PortBase'; + + PortBase.ATTRS = { + node: { + setter: function(node) { + var n = Y.get(node); + return n.item(0); + } + }, + element: { + getter: function() { + return this.get('node'); + }, + setter: function(n) { + this.set('node', n); + return this.get('node'); + } + } + }; + + Y.extend(PortBase, Y.Base, { + DOM_EVENTS: { + 'click': true, + 'dblclick': true, + 'keydown': true, + 'keypress': true, + 'keyup': true, + 'mousedown': true, + 'mousemove': true, + 'mouseout': true, + 'mouseover': true, + 'mouseup': true, + 'focus': true, + 'blur': true, + 'submit': true, + 'change': true + }, + initializer: function() { + this.initAttributes(this._portAttrs); + }, + initAttributes: function() { + }, + setAttributeConfig: function(name, config) { + if (config.method) { + config.setter = config.method; + delete config.method; + } + this.addAttr(name, config); + }, + addListener: function(name, fn, ctxt, args) { + if (ctxt) { + if (args) { + fn = Y.rbind(fn, args, ctxt); + } else { + fn = Y.bind(fn, ctxt); + } + } + if (this.DOM_EVENTS[name]) { + this.get('node').on(name, fn); + } else { + this.on(name, fn); + } + //console.log('addListener: ', arguments); + }, + getElementsByClassName: function(className, tagName) { + var sel = tagName + ' .' + className; + //console.log('getElementsByClassName', this.constructor.NAME, arguments, sel); + return this.get('node').query(sel) || []; + }, + getElementsByTagName: function(tagName) { + var n = this.get('node'); + return ((n) ? n.query(tagName) : []); + }, + addClass: function(className) { + var n = this.get('node'); + if (!n) { return false; } + return n.addClass(className); + }, + removeClass: function(className) { + var n = this.get('node'); + if (!n) { return false; } + return n.removeClass(className); + }, + hasClass: function(className) { + var n = this.get('node'); + if (!n) { return false; } + return n.hasClass(className); + }, + appendTo: function(el) { + Y.get(el).appendChild(this.get('node')); + }, + fireEvent: function(type, args) { + if (args.target) { + delete args.target; + } + if (args.ev) { + args.ev = new Y.DOMEventFacade(args.ev); + } + var ret = false, e; + this.publish(type, { + defaultFn: function() { + ret = true; + } + }); + e = this.fire(type, args); + return ret; + } + }); + + Y.PortBase = PortBase; + + + +}, 'gallery-2009.11.02-20' ,{requires:['base','node']}); diff --git a/build/gallery-simple-editor/gallery-simple-editor-debug.js b/build/gallery-simple-editor/gallery-simple-editor-debug.js new file mode 100644 index 0000000000..315e22a460 --- /dev/null +++ b/build/gallery-simple-editor/gallery-simple-editor-debug.js @@ -0,0 +1,7045 @@ +YUI.add('gallery-simple-editor', function(Y) { + + + /** + * @description

        Creates a basic custom Toolbar Button. Primarily used with the Rich Text Editor's Toolbar

        Provides a toolbar button based on the button and menu widgets, <select> elements are used in place of menu's.

        + * @class ToolbarButton + * @namespace YAHOO.widget + * @requires yahoo, dom, element, event + * @extends YAHOO.util.Element + * + * + * @constructor + * @param {String/HTMLElement} el The element to turn into a button. + * @param {Object} attrs Object liternal containing configuration parameters. + */ + + var ToolbarButton = function() { + Y.log('ToolbarButton Initalizing', 'info', 'ToolbarButton'); + ToolbarButton.superclass.constructor.apply(this, arguments); + }; + + ToolbarButton.NAME = 'ToolbarButton'; + + ToolbarButton.ATTRS = { + node: { + setter: function(node) { + var n = Y.get(node); + if (!n) { + Y.error('Invalid Node Given: ' + node); + } else { + n = n.item(0); + } + return n; + } + }, + id: { + }, + /** + * @attribute value + * @description The value of the button + * @type String + */ + value: { + }, + /** + * @attribute menu + * @description The menu attribute, see YAHOO.widget.Button + * @type Object + */ + menu: { + value: false + }, + /** + * @attribute type + * @description The type of button to create: push, menu, color, select, spin + * @type String + */ + type: { + value: 'push', + writeOnce: true + }, + /** + * @attribute disabled + * @description Set the button into a disabled state + * @type String + */ + disabled: { + value: false, + setter: function(disabled) { + if (disabled) { + this.get('node').addClass('yui-button-disabled'); + this.get('node').addClass('yui-' + this.get('type') + '-button-disabled'); + } else { + this.get('node').removeClass('yui-button-disabled'); + this.get('node').removeClass('yui-' + this.get('type') + '-button-disabled'); + } + //console.log(this.get('node'), ': ', disabled); + if (this.get('type') == 'select') { + this.get('node').set('disabled', disabled); + } + } + }, + /** + * @attribute label + * @description The text label for the button + * @type String + */ + label: { + value: 'LABEL', + setter: function(label) { + if (this.get('type') == 'push') { + this.get('node').query('a').set('innerHTML', label); + } + } + }, + /** + * @attribute title + * @description The title of the button + * @type String + */ + title: { + value: 'TITLE', + setter: function(title) { + this.get('node').set('title', title); + } + }, + + /** + * @config container + * @description The container that the button is rendered to, handled by Toolbar + * @type String + */ + container: { + value: null, + writeOnce: true, + setter: function(node) { + var n = Y.get(node); + if (!n) { + Y.error('Invalid Node Given: ' + node); + } else { + n = n.item(0); + } + return n; + } + } + }; + + Y.extend(ToolbarButton, Y.Base, { + /** + * @property buttonType + * @private + * @description Tells if the Button is a Rich Button or a Simple Button + */ + buttonType: 'normal', + /** + * @method _handleMouseOver + * @private + * @description Adds classes to the button elements on mouseover (hover) + */ + _handleMouseOver: function() { + if (!this.get('disabled')) { + this.get('node').addClass('yui-button-hover'); + this.get('node').addClass('yui-' + this.get('type') + '-button-hover'); + } + }, + /** + * @method _handleMouseOut + * @private + * @description Removes classes from the button elements on mouseout (hover) + */ + _handleMouseOut: function() { + this.get('node').removeClass('yui-button-hover'); + this.get('node').removeClass('yui-' + this.get('type') + '-button-hover'); + }, + /** + * @method checkValue + * @param {String} value The value of the option that we want to mark as selected + * @description Select an option by value + */ + checkValue: function(value) { + if (this.get('type') == 'select') { + var opt = this.get('node').get('options'); + opt.each(function(n, k) { + if (n.get('value') == value) { + this.get('node').set('selectedIndex', k); + } + }, this); + } + }, + /** + * @method initializer + * @description The ToolbarButton class's initialization method + */ + initializer: function() { + var id, i, el, menu, opt, html; + switch (this.get('type')) { + case 'select': + case 'menu': + el = Y.Node.create(''); + menu = this.get('menu'); + + for (i = 0; i < menu.length; i++) { + opt = Y.Node.create(''); + el.appendChild(opt); + if (menu[i].checked) { + opt.set('selected', true); + } + } + el.on('change', Y.bind(this._handleSelect, this)); + id = Y.stamp(el); + el.set('id', id); + this.set('node', el); + break; + default: + html = ''; + this.set('node', Y.Node.create(html)); + this.get('node').on('mouseover', Y.bind(this._handleMouseOver, this)); + this.get('node').on('mouseout', Y.bind(this._handleMouseOut, this)); + break; + } + id = Y.stamp(this.get('node')); + this.set('id', id); + this.get('node').set('id', id); + this.set('title', this.get('title')); + this.set('label', this.get('label')); + this.set('disabled', this.get('disabled')); + this.get('container').appendChild(this.get('node')); + + this.get('node').on('click', Y.bind(function(e) { + e.halt(); + }, this)); + }, + /** + * @private + * @method _handleSelect + * @description The event fired when a change event gets fired on a select element + * @param {Event} ev The change event. + */ + _handleSelect: function(e) { + this.fire('change', {type: 'change', value: e.target.get('value'), target: this.get('node') }); + }, + /** + * @method getMenu + * @description A stub function to mimic YAHOO.widget.Button's getMenu method + */ + getMenu: function() { + return this.get('menu'); + }, + destructor: function() { + }, + /** + * @method toString + * @description Returns a string representing the toolbar. + * @return {String} + */ + toString: function() { + return 'Toolbar.Button (' + this.get('node').get('id') + ')'; + } + + }); + + Y.namespace('Toolbar'); + Y.Toolbar.Button = ToolbarButton; + + /* + var Lang = Y.Lang, + Dom = Y.DOM, + Event = { + on: Y.Event.nativeAdd, + getTarget: function(ev) { + return ev.target; + }, + stopEvent: function(ev) { + ev.halt(); + } + }, + YAHOO = { + log: console.log, + lang: Lang, + util: { + Dom: Dom, + Event: Event + } + }; + */ + + var getButton = function(id) { + var button = id; + if (Y.Lang.isString(id)) { + button = this.getButtonById(id); + } + if (Y.Lang.isNumber(id)) { + button = this.getButtonByIndex(id); + } + if (id instanceof Y.Toolbar.Button) { + button = id; + } else { + button = this.getButtonByValue(id); + } + return button; + }; + + /** + * Provides a rich toolbar widget based on the button and menu widgets + * @constructor + * @class Toolbar + * @extends YAHOO.util.Element + * @param {String/HTMLElement} el The element to turn into a toolbar. + * @param {Object} attrs Object liternal containing configuration parameters. + */ + var Toolbar = function() { + + this._configuredButtons = []; + + Toolbar.superclass.constructor.apply(this, arguments); + + }; + + Toolbar.NAME = 'Toolbar'; + + Toolbar.ATTRS = { + node: { + setter: function(node) { + var n = Y.get(node); + if (!n) { + Y.error('Invalid Node Given: ' + node); + } else { + n = n.item(0); + } + return n; + } + }, + /** + * @attribute buttonType + * @description The buttonType to use (advanced or basic) + * @type String + */ + buttonType: { + value: 'basic', + writeOnce: true, + validator: function(type) { + switch (type) { + case 'advanced': + case 'basic': + return type; + } + return 'basic'; + }, + setter: function(type) { + this.buttonType = Y.Toolbar.Button; + } + }, + /** + * @attribute buttons + * @description Object specifying the buttons to include in the toolbar + * Example: + *
        +        * {
        +        *   { id: 'b3', type: 'button', label: 'Underline', value: 'underline' },
        +        *   { type: 'separator' },
        +        *   { id: 'b4', type: 'menu', label: 'Align', value: 'align',
        +        *       menu: [
        +        *           { text: "Left", value: 'alignleft' },
        +        *           { text: "Center", value: 'aligncenter' },
        +        *           { text: "Right", value: 'alignright' }
        +        *       ]
        +        *   }
        +        * }
        +        * 
        + * @type Array + */ + + buttons: { + value: [] + }, + /** + * @config cont + * @description The container for the toolbar. + * @type HTMLElement + */ + cont: { + value: null + }, + id: { + value: null + }, + /** + * @attribute grouplabels + * @description Boolean indicating if the toolbar should show the group label's text string. + * @default true + * @type Boolean + */ + grouplabels: { + value: true, + setter: function(grouplabels) { + if (grouplabels) { + this.get('cont').removeClass(this.CLASS_PREFIX + '-nogrouplabels'); + } else { + this.get('cont').addClass(this.CLASS_PREFIX + '-nogrouplabels'); + } + } + }, + + /** + * @attribute titlebar + * @description Boolean indicating if the toolbar should have a titlebar. If + * passed a string, it will use that as the titlebar text + * @default false + * @type Boolean or String + */ + titlebar: { + value: false + }, + + /** + * @attribute collapse + * @description Boolean indicating if the the titlebar should have a collapse button. + * The collapse button will not remove the toolbar, it will minimize it to the titlebar + * @default false + * @type Boolean + */ + collapse: { + value: false + } + + }; + + Y.extend(Toolbar, Y.Base, { + fireEvent: function(type, args) { + if (args.target) { + delete args.target; + } + this.fire(type, args); + }, + _handleCollapse: function() { + var collapse = this.get('collapse'); + + if (this._titlebar) { + var collapseEl = null; + var el = this._titlebar.query('span.collapse'); + if (collapse) { + if (el) { + //There is already a collapse button + return true; + } + collapseEl = Y.Node.create('X'); + this._titlebar.appendChild(collapseEl); + collapseEl.on('click', Y.bind(function() { + if (this.get('cont').get('parentNode').hasClass('yui-toolbar-container-collapsed')) { + this.collapse(false); //Expand Toolbar + } else { + this.collapse(); //Collapse Toolbar + } + }, this)); + } else { + collapseEl = this._titlebar.query('span.collapse'); + if (collapseEl) { + if (this.get('cont').get('parentNode').hasClass('yui-toolbar-container-collapsed')) { + //We are closed, reopen the titlebar.. + this.collapse(false); //Expand Toolbar + } + collapseEl.get('parentNode').removeChild(collapseEl); + } + } + } + }, + _renderTitle: function() { + var titlebar = this.get('titlebar'); + if (titlebar) { + if (this._titlebar && this._titlebar.get('parentNode')) { + this._titlebar.get('parentNode').removeChild(this._titlebar); + } + this._titlebar = Y.Node.create('
        '); + this._titlebar.on('focus', Y.bind(this._handleFocus, this)); + + if (Y.Lang.isString(titlebar)) { + var h2 = Y.Node.create('

        ' + titlebar + '

        '); + this._titlebar.appendChild(h2); + h2.get('firstChild').on('click', function(ev) { + ev.halt(); + }); + /* + Event.on([h2, h2.firstChild], 'focus', function() { + this._handleFocus(); + }, this, true); + */ + } + if (this.get('node').get('firstChild')) { + this.get('node').insertBefore(this._titlebar, this.get('node').get('firstChild')); + } else { + this.get('node').appendChild(this._titlebar); + } + if (this.get('collapse')) { + this.set('collapse', true); + } + } else if (this._titlebar) { + if (this._titlebar && this._titlebar.get('parentNode')) { + this._titlebar.get('parentNode').removeChild(this._titlebar); + } + } + }, + /** + * @protected + * @property _configuredButtons + * @type Array + */ + _configuredButtons: null, + /** + * @method _addMenuClasses + * @private + * @description This method is called from Menu's renderEvent to add a few more classes to the menu items + * @param {String} ev The event that fired. + * @param {Array} na Array of event information. + * @param {Object} o Button config object. + */ + _addMenuClasses: function(ev, na, o) { + Dom.addClass(this.element, 'yui-toolbar-' + o.get('value') + '-menu'); + if (Dom.hasClass(o._button.parentNode.parentNode, 'yui-toolbar-select')) { + Dom.addClass(this.element, 'yui-toolbar-select-menu'); + } + var items = this.getItems(); + for (var i = 0; i < items.length; i++) { + Dom.addClass(items[i].element, 'yui-toolbar-' + o.get('value') + '-' + ((items[i].value) ? items[i].value.replace(/ /g, '-').toLowerCase() : items[i]._oText.nodeValue.replace(/ /g, '-').toLowerCase())); + Dom.addClass(items[i].element, 'yui-toolbar-' + o.get('value') + '-' + ((items[i].value) ? items[i].value.replace(/ /g, '-') : items[i]._oText.nodeValue.replace(/ /g, '-'))); + } + }, + /** + * @property buttonType + * @description The default button to use + * @type Object + */ + buttonType: Y.Toolbar.Button, + /** + * @property dd + * @description The DragDrop instance associated with the Toolbar + * @type Object + */ + dd: null, + /** + * @property _colorData + * @description Object reference containing colors hex and text values. + * @type Object + */ + _colorData: { +/* {{{ _colorData */ + '#111111': 'Obsidian', + '#2D2D2D': 'Dark Gray', + '#434343': 'Shale', + '#5B5B5B': 'Flint', + '#737373': 'Gray', + '#8B8B8B': 'Concrete', + '#A2A2A2': 'Gray', + '#B9B9B9': 'Titanium', + '#000000': 'Black', + '#D0D0D0': 'Light Gray', + '#E6E6E6': 'Silver', + '#FFFFFF': 'White', + '#BFBF00': 'Pumpkin', + '#FFFF00': 'Yellow', + '#FFFF40': 'Banana', + '#FFFF80': 'Pale Yellow', + '#FFFFBF': 'Butter', + '#525330': 'Raw Siena', + '#898A49': 'Mildew', + '#AEA945': 'Olive', + '#7F7F00': 'Paprika', + '#C3BE71': 'Earth', + '#E0DCAA': 'Khaki', + '#FCFAE1': 'Cream', + '#60BF00': 'Cactus', + '#80FF00': 'Chartreuse', + '#A0FF40': 'Green', + '#C0FF80': 'Pale Lime', + '#DFFFBF': 'Light Mint', + '#3B5738': 'Green', + '#668F5A': 'Lime Gray', + '#7F9757': 'Yellow', + '#407F00': 'Clover', + '#8A9B55': 'Pistachio', + '#B7C296': 'Light Jade', + '#E6EBD5': 'Breakwater', + '#00BF00': 'Spring Frost', + '#00FF80': 'Pastel Green', + '#40FFA0': 'Light Emerald', + '#80FFC0': 'Sea Foam', + '#BFFFDF': 'Sea Mist', + '#033D21': 'Dark Forrest', + '#438059': 'Moss', + '#7FA37C': 'Medium Green', + '#007F40': 'Pine', + '#8DAE94': 'Yellow Gray Green', + '#ACC6B5': 'Aqua Lung', + '#DDEBE2': 'Sea Vapor', + '#00BFBF': 'Fog', + '#00FFFF': 'Cyan', + '#40FFFF': 'Turquoise Blue', + '#80FFFF': 'Light Aqua', + '#BFFFFF': 'Pale Cyan', + '#033D3D': 'Dark Teal', + '#347D7E': 'Gray Turquoise', + '#609A9F': 'Green Blue', + '#007F7F': 'Seaweed', + '#96BDC4': 'Green Gray', + '#B5D1D7': 'Soapstone', + '#E2F1F4': 'Light Turquoise', + '#0060BF': 'Summer Sky', + '#0080FF': 'Sky Blue', + '#40A0FF': 'Electric Blue', + '#80C0FF': 'Light Azure', + '#BFDFFF': 'Ice Blue', + '#1B2C48': 'Navy', + '#385376': 'Biscay', + '#57708F': 'Dusty Blue', + '#00407F': 'Sea Blue', + '#7792AC': 'Sky Blue Gray', + '#A8BED1': 'Morning Sky', + '#DEEBF6': 'Vapor', + '#0000BF': 'Deep Blue', + '#0000FF': 'Blue', + '#4040FF': 'Cerulean Blue', + '#8080FF': 'Evening Blue', + '#BFBFFF': 'Light Blue', + '#212143': 'Deep Indigo', + '#373E68': 'Sea Blue', + '#444F75': 'Night Blue', + '#00007F': 'Indigo Blue', + '#585E82': 'Dockside', + '#8687A4': 'Blue Gray', + '#D2D1E1': 'Light Blue Gray', + '#6000BF': 'Neon Violet', + '#8000FF': 'Blue Violet', + '#A040FF': 'Violet Purple', + '#C080FF': 'Violet Dusk', + '#DFBFFF': 'Pale Lavender', + '#302449': 'Cool Shale', + '#54466F': 'Dark Indigo', + '#655A7F': 'Dark Violet', + '#40007F': 'Violet', + '#726284': 'Smoky Violet', + '#9E8FA9': 'Slate Gray', + '#DCD1DF': 'Violet White', + '#BF00BF': 'Royal Violet', + '#FF00FF': 'Fuchsia', + '#FF40FF': 'Magenta', + '#FF80FF': 'Orchid', + '#FFBFFF': 'Pale Magenta', + '#4A234A': 'Dark Purple', + '#794A72': 'Medium Purple', + '#936386': 'Cool Granite', + '#7F007F': 'Purple', + '#9D7292': 'Purple Moon', + '#C0A0B6': 'Pale Purple', + '#ECDAE5': 'Pink Cloud', + '#BF005F': 'Hot Pink', + '#FF007F': 'Deep Pink', + '#FF409F': 'Grape', + '#FF80BF': 'Electric Pink', + '#FFBFDF': 'Pink', + '#451528': 'Purple Red', + '#823857': 'Purple Dino', + '#A94A76': 'Purple Gray', + '#7F003F': 'Rose', + '#BC6F95': 'Antique Mauve', + '#D8A5BB': 'Cool Marble', + '#F7DDE9': 'Pink Granite', + '#C00000': 'Apple', + '#FF0000': 'Fire Truck', + '#FF4040': 'Pale Red', + '#FF8080': 'Salmon', + '#FFC0C0': 'Warm Pink', + '#441415': 'Sepia', + '#82393C': 'Rust', + '#AA4D4E': 'Brick', + '#800000': 'Brick Red', + '#BC6E6E': 'Mauve', + '#D8A3A4': 'Shrimp Pink', + '#F8DDDD': 'Shell Pink', + '#BF5F00': 'Dark Orange', + '#FF7F00': 'Orange', + '#FF9F40': 'Grapefruit', + '#FFBF80': 'Canteloupe', + '#FFDFBF': 'Wax', + '#482C1B': 'Dark Brick', + '#855A40': 'Dirt', + '#B27C51': 'Tan', + '#7F3F00': 'Nutmeg', + '#C49B71': 'Mustard', + '#E1C4A8': 'Pale Tan', + '#FDEEE0': 'Marble' +/* }}} */ + }, + /** + * @property _colorPicker + * @description The HTML Element containing the colorPicker + * @type HTMLElement + */ + _colorPicker: null, + /** + * @property STR_COLLAPSE + * @description String for Toolbar Collapse Button + * @type String + */ + STR_COLLAPSE: 'Collapse Toolbar', + /** + * @property STR_EXPAND + * @description String for Toolbar Collapse Button - Expand + * @type String + */ + STR_EXPAND: 'Expand Toolbar', + /** + * @property STR_SPIN_LABEL + * @description String for spinbutton dynamic label. Note the {VALUE} will be replaced with YAHOO.lang.substitute + * @type String + */ + STR_SPIN_LABEL: 'Spin Button with value {VALUE}. Use Control Shift Up Arrow and Control Shift Down arrow keys to increase or decrease the value.', + /** + * @property STR_SPIN_UP + * @description String for spinbutton up + * @type String + */ + STR_SPIN_UP: 'Click to increase the value of this input', + /** + * @property STR_SPIN_DOWN + * @description String for spinbutton down + * @type String + */ + STR_SPIN_DOWN: 'Click to decrease the value of this input', + /** + * @property _titlebar + * @description Object reference to the titlebar + * @type HTMLElement + */ + _titlebar: null, + /** + * @property browser + * @description Standard browser detection + * @type Object + */ + browser: Y.UA, + /** + * @protected + * @property _buttonList + * @description Internal property list of current buttons in the toolbar + * @type Array + */ + _buttonList: null, + /** + * @protected + * @property _buttonGroupList + * @description Internal property list of current button groups in the toolbar + * @type Array + */ + _buttonGroupList: null, + /** + * @protected + * @property _sep + * @description Internal reference to the separator HTML Element for cloning + * @type HTMLElement + */ + _sep: null, + /** + * @protected + * @property _sepCount + * @description Internal refernce for counting separators, so we can give them a useful class name for styling + * @type Number + */ + _sepCount: null, + /** + * @protected + * @property draghandle + * @type HTMLElement + */ + _dragHandle: null, + /** + * @protected + * @property _toolbarConfigs + * @type Object + */ + _toolbarConfigs: { + renderer: true + }, + /** + * @protected + * @property CLASS_CONTAINER + * @description Default CSS class to apply to the toolbar container element + * @type String + */ + CLASS_CONTAINER: 'yui-toolbar-container', + /** + * @protected + * @property CLASS_DRAGHANDLE + * @description Default CSS class to apply to the toolbar's drag handle element + * @type String + */ + CLASS_DRAGHANDLE: 'yui-toolbar-draghandle', + /** + * @protected + * @property CLASS_SEPARATOR + * @description Default CSS class to apply to all separators in the toolbar + * @type String + */ + CLASS_SEPARATOR: 'yui-toolbar-separator', + /** + * @protected + * @property CLASS_DISABLED + * @description Default CSS class to apply when the toolbar is disabled + * @type String + */ + CLASS_DISABLED: 'yui-toolbar-disabled', + /** + * @protected + * @property CLASS_PREFIX + * @description Default prefix for dynamically created class names + * @type String + */ + CLASS_PREFIX: 'yui-toolbar', + /** + * @method init + * @description The Toolbar class's initialization method + */ + initializer: function() { + var fs = Y.Node.create('
        Toolbar
        '); + this.set('id', this.get('node').get('id')); + this.get('node').appendChild(fs); + this.set('cont', fs.query('div.yui-toolbar-subcont')); + this.get('node').set('tabIndex', '-1'); + this.get('node').addClass(this.CLASS_CONTAINER); + + this._renderTitle(); + this._handleCollapse(); + this.on('titlebarChange', Y.bind(this._renderTitle, this)); + this.on('collapseChange', Y.bind(this._handleCollapse, this)); + + + this.processButtons(); + }, + processButtons: function() { + var button, buttons, len, b, data = this.get('buttons'); + Y.each(data, function(i, k) { + if (i.type == 'separator') { + this.addSeparator(); + } else if (i.group !== undefined) { + buttons = this.addButtonGroup(i); + if (buttons) { + len = buttons.length; + for(b = 0; b < len; b++) { + if (buttons[b]) { + this._configuredButtons[this._configuredButtons.length] = buttons[b].id; + } + } + } + this.set('buttons', data); + } else { + button = this.addButton(i); + if (button) { + this._configuredButtons[this._configuredButtons.length] = button.id; + } + } + }, this); + }, + /** + * @method initAttributes + * @description Initializes all of the configuration attributes used to create + * the toolbar. + * @param {Object} attr Object literal specifying a set of + * configuration attributes used to create the toolbar. + */ + initAttributes: function(attr) { + YAHOO.widget.Toolbar.superclass.initAttributes.call(this, attr); + + + /** + * @attribute disabled + * @description Boolean indicating if the toolbar should be disabled. It will also disable the draggable attribute if it is on. + * @default false + * @type Boolean + */ + this.setAttributeConfig('disabled', { + value: false, + method: function(disabled) { + if (this.get('disabled') === disabled) { + return false; + } + if (disabled) { + this.addClass(this.CLASS_DISABLED); + this.set('draggable', false); + this.disableAllButtons(); + } else { + this.removeClass(this.CLASS_DISABLED); + if (this._configs.draggable._initialConfig.value) { + //Draggable by default, set it back + this.set('draggable', true); + } + this.resetAllButtons(); + } + } + }); + + + /** + * @attribute draggable + * @description Boolean indicating if the toolbar should be draggable. + * @default false + * @type Boolean + */ + + this.setAttributeConfig('draggable', { + value: (attr.draggable || false), + method: function(draggable) { + if (draggable && !this.get('titlebar')) { + YAHOO.log('Dragging enabled', 'info', 'Toolbar'); + if (!this._dragHandle) { + this._dragHandle = document.createElement('SPAN'); + this._dragHandle.innerHTML = '|'; + this._dragHandle.setAttribute('title', 'Click to drag the toolbar'); + this._dragHandle.id = this.get('id') + '_draghandle'; + Dom.addClass(this._dragHandle, this.CLASS_DRAGHANDLE); + if (this.get('cont').hasChildNodes()) { + this.get('cont').insertBefore(this._dragHandle, this.get('cont').firstChild); + } else { + this.get('cont').appendChild(this._dragHandle); + } + this.dd = new YAHOO.util.DD(this.get('id')); + this.dd.setHandleElId(this._dragHandle.id); + + } + } else { + YAHOO.log('Dragging disabled', 'info', 'Toolbar'); + if (this._dragHandle) { + this._dragHandle.parentNode.removeChild(this._dragHandle); + this._dragHandle = null; + this.dd = null; + } + } + if (this._titlebar) { + if (draggable) { + this.dd = new YAHOO.util.DD(this.get('id')); + this.dd.setHandleElId(this._titlebar); + Dom.addClass(this._titlebar, 'draggable'); + } else { + Dom.removeClass(this._titlebar, 'draggable'); + if (this.dd) { + this.dd.unreg(); + this.dd = null; + } + } + } + }, + validator: function(value) { + var ret = true; + if (!YAHOO.util.DD) { + ret = false; + } + return ret; + } + }); + + }, + /** + * @method addButtonGroup + * @description Add a new button group to the toolbar. (uses addButton) + * @param {Object} oGroup Object literal reference to the Groups Config (contains an array of button configs as well as the group label) + */ + addButtonGroup: function(oGroup) { + + if (!this.get('node').hasClass(this.CLASS_PREFIX + '-grouped')) { + this.get('node').addClass(this.CLASS_PREFIX + '-grouped'); + } + var div = Y.Node.create('
        '); + if (oGroup.label) { + var label = Y.Node.create('

        ' + oGroup.label + '

        '); + div.appendChild(label); + } + if (!this.get('grouplabels')) { + this.get('cont').addClass(this.CLASS_PREFIX, '-nogrouplabels'); + } + + this.get('cont').appendChild(div); + + //For accessibility, let's put all of the group buttons in an Unordered List + var ul = Y.Node.create('
          '); + div.appendChild(ul); + + if (!this._buttonGroupList) { + this._buttonGroupList = {}; + } + + this._buttonGroupList[oGroup.group] = ul; + + + //An array of the button ids added to this group + //This is used for destruction later... + var addedButtons = [], + button; + + Y.each(oGroup.buttons, function(i) { + var li = Y.Node.create('
        • '); + ul.appendChild(li); + if ((i.type !== undefined) && i.type == 'separator') { + this.addSeparator(li); + } else { + i.container = li; + button = this.addButton(i); + if (button) { + addedButtons[addedButtons.length] = button.id; + } + } + }, this); + + return addedButtons; + }, + /** + * @method addButtonToGroup + * @description Add a new button to a toolbar group. Buttons supported: + * push, split, menu, select, color, spin + * @param {Object} oButton Object literal reference to the Button's Config + * @param {String} group The Group identifier passed into the initial config + * @param {HTMLElement} after Optional HTML element to insert this button after in the DOM. + */ + addButtonToGroup: function(oButton, group, after) { + var groupCont = this._buttonGroupList[group], + li = document.createElement('li'); + + li.className = this.CLASS_PREFIX + '-groupitem'; + oButton.container = li; + this.addButton(oButton, after); + groupCont.appendChild(li); + }, + /** + * @method addButton + * @description Add a new button to the toolbar. Buttons supported: + * push, split, menu, select, color, spin + * @param {Object} oButton Object literal reference to the Button's Config + * @param {HTMLElement} after Optional HTML element to insert this button after in the DOM. + */ + addButton: function(oButton, after) { + if (!this._buttonList) { + this._buttonList = []; + } + if (!oButton.container) { + oButton.container = this.get('cont'); + } + /* + if ((oButton.type == 'menu') || (oButton.type == 'split') || (oButton.type == 'select')) { + if (Y.Lang.isArray(oButton.menu)) { + for (var i in oButton.menu) { + if (Lang.hasOwnProperty(oButton.menu, i)) { + var funcObject = { + fn: function(ev, x, oMenu) { + if (!oButton.menucmd) { + oButton.menucmd = oButton.value; + } + oButton.value = ((oMenu.value) ? oMenu.value : oMenu._oText.nodeValue); + }, + scope: this + }; + oButton.menu[i].onclick = funcObject; + } + } + } + } + */ + var _oButton = {}, skip = false; + Y.each(oButton, function(v, k) { + if (!this._toolbarConfigs[k]) { + _oButton[k] = v; + } + }, this); + if (oButton.type == 'select') { + _oButton.type = 'menu'; + } + if (oButton.type == 'spin') { + _oButton.type = 'push'; + } + if (_oButton.type == 'color') { + if (Y.Overlay) { + _oButton = this._makeColorButton(_oButton); + } else { + skip = true; + } + } + if (_oButton.menu) { + if ((Y.Overlay) && (oButton.menu instanceof Y.Overlay)) { + oButton.menu.on('visibleChange', function() { + this._button = _oButton; + }); + } else { + /* + for (var m = 0; m < _oButton.menu.length; m++) { + if (!_oButton.menu[m].value) { + _oButton.menu[m].value = _oButton.menu[m].text; + } + } + if (this.browser.webkit) { + _oButton.focusmenu = false; + } + */ + } + } + if (skip) { + oButton = false; + } else { + //Add to .get('buttons') manually + //this._configs.buttons.value[this._configs.buttons.value.length] = oButton; + var tmp = new this.buttonType(_oButton); + tmp.get('node').set('tabIndex', '-1'); + tmp.get('node').set('role', 'button'); + if (!_oButton.id || !oButton.id) { + oButton.id = tmp.get('id'); + _oButton.id = tmp.get('id'); + } + //tmp._selected = true; + + if (this.get('disabled')) { + //Toolbar is disabled, disable the new button too! + tmp.set('disabled', true); + } + /* + if (after) { + var el = tmp.get('element'); + var nextSib = null; + if (after.get) { + nextSib = after.get('element').nextSibling; + } else if (after.nextSibling) { + nextSib = after.nextSibling; + } + if (nextSib) { + nextSib.parentNode.insertBefore(el, nextSib); + } + } + */ + tmp.get('node').addClass(this.CLASS_PREFIX + '-' + tmp.get('value')); + + var icon = Y.Node.create(''); + tmp.get('node').insertBefore(icon, tmp.get('node').get('firstChild')); + /* + if (tmp._button.tagName.toLowerCase() == 'button') { + tmp.get('element').setAttribute('unselectable', 'on'); + //Replace the Button HTML Element with an a href if it exists + var a = document.createElement('a'); + a.innerHTML = tmp._button.innerHTML; + a.href = '#'; + a.tabIndex = '-1'; + Event.on(a, 'click', function(ev) { + Event.stopEvent(ev); + }); + tmp._button.parentNode.replaceChild(a, tmp._button); + tmp._button = a; + } + */ + /* + if (oButton.type == 'select') { + if (tmp._button.tagName.toLowerCase() == 'select') { + icon.parentNode.removeChild(icon); + var iel = tmp._button, + parEl = tmp.get('element'); + parEl.parentNode.replaceChild(iel, parEl); + //The 'element' value is currently the orphaned element + //In order for "destroy" to execute we need to get('element') to reference the correct node. + //I'm not sure if there is a direct approach to setting this value. + tmp._configs.element.value = iel; + } else { + //Don't put a class on it if it's a real select element + tmp.addClass(this.CLASS_PREFIX + '-select'); + } + } + */ + if (oButton.type == 'spin') { + if (!Y.Lang.isArray(oButton.range)) { + oButton.range = [ 10, 100 ]; + } + this._makeSpinButton(tmp, oButton); + } + tmp.get('node').set('title', tmp.get('label')); + if (oButton.type != 'spin') { + if ((Y.Overlay) && (_oButton.menu instanceof Y.Overlay)) { + var showPicker = function(ev) { + var exec = true; + if (ev.keyCode && (ev.keyCode == 9)) { + exec = false; + } + if (exec) { + if (this._colorPicker) { + this._colorPicker._button = oButton.value; + } + tmp.getMenu().set('visible', !tmp.getMenu().get('visible')); + } + ev.halt(); + }; + tmp.get('node').on('mousedown', Y.bind(showPicker, this)); + tmp.get('node').on('keydown', Y.bind(showPicker, this)); + + } else if ((oButton.type != 'menu') && (oButton.type != 'select')) { + tmp.get('node').on('keypress', Y.bind(this._buttonClick, this)); + tmp.get('node').on('mousedown', Y.bind(function(ev) { + ev.halt(); + this._buttonClick(ev, oButton); + }, this)); + tmp.get('node').on('click', function(ev) { + ev.halt(); + }); + } else { + + //Stop the mousedown event so we can trap the selection in the editor! + tmp.on('mousedown', function(ev) { + ev.halt(); + }); + tmp.on('click', function(ev) { + ev.halt(); + }); + tmp.on('change', function(ev) { + if (!oButton.menucmd) { + oButton.menucmd = oButton.value; + } + oButton.value = ev.value; + this._buttonClick(ev, oButton); + }, this, true); + } + /* + var self = this; + //Hijack the mousedown event in the menu and make it fire a button click.. + + tmp.on('appendTo', function() { + var tmp = this; + if (tmp.getMenu() && tmp.getMenu().mouseDownEvent) { + tmp.getMenu().mouseDownEvent.subscribe(function(ev, args) { + YAHOO.log('mouseDownEvent', 'warn', 'Toolbar'); + var oMenu = args[1]; + YAHOO.util.Event.stopEvent(args[0]); + tmp._onMenuClick(args[0], tmp); + if (!oButton.menucmd) { + oButton.menucmd = oButton.value; + } + oButton.value = ((oMenu.value) ? oMenu.value : oMenu._oText.nodeValue); + self._buttonClick.call(self, args[1], oButton); + tmp._hideMenu(); + return false; + }); + tmp.getMenu().clickEvent.subscribe(function(ev, args) { + YAHOO.log('clickEvent', 'warn', 'Toolbar'); + YAHOO.util.Event.stopEvent(args[0]); + }); + tmp.getMenu().mouseUpEvent.subscribe(function(ev, args) { + YAHOO.log('mouseUpEvent', 'warn', 'Toolbar'); + YAHOO.util.Event.stopEvent(args[0]); + }); + } + }); + */ + } else { + //Stop the mousedown event so we can trap the selection in the editor! + tmp.on('mousedown', function(ev) { + ev.halt(); + }); + tmp.on('click', function(ev) { + ev.halt(); + }); + } + if (this.browser.ie) { + /* + //Add a couple of new events for IE + tmp.DOM_EVENTS.focusin = true; + tmp.DOM_EVENTS.focusout = true; + + //Stop them so we don't loose focus in the Editor + tmp.on('focusin', function(ev) { + YAHOO.util.Event.stopEvent(ev); + }, oButton, this); + + tmp.on('focusout', function(ev) { + YAHOO.util.Event.stopEvent(ev); + }, oButton, this); + tmp.on('click', function(ev) { + YAHOO.util.Event.stopEvent(ev); + }, oButton, this); + */ + } + if (this.browser.webkit) { + //This will keep the document from gaining focus and the editor from loosing it.. + //Forcefully remove the focus calls in button! + //tmp.hasFocus = function() { + // return true; + //}; + } + this._buttonList[this._buttonList.length] = tmp; + /* + if ((oButton.type == 'menu') || (oButton.type == 'split') || (oButton.type == 'select')) { + if (Lang.isArray(oButton.menu)) { + YAHOO.log('Button type is (' + oButton.type + '), doing extra renderer work.', 'info', 'Toolbar'); + var menu = tmp.getMenu(); + if (menu && menu.renderEvent) { + menu.renderEvent.subscribe(this._addMenuClasses, tmp); + if (oButton.renderer) { + menu.renderEvent.subscribe(oButton.renderer, tmp); + } + } + } + } + */ + } + return oButton; + }, + /** + * @method addSeparator + * @description Add a new button separator to the toolbar. + * @param {HTMLElement} cont Optional HTML element to insert this button into. + * @param {HTMLElement} after Optional HTML element to insert this button after in the DOM. + */ + addSeparator: function(cont, after) { + var sepCont = ((cont) ? cont : this.get('cont')); + if (this._sepCount === null) { + this._sepCount = 0; + } + if (!this._sep) { + Y.log('Separator does not yet exist, creating'); + this._sep = Y.Node.create('|'); + } + Y.log('Separator does exist, cloning'); + var _sep = this._sep.cloneNode(true); + this._sepCount++; + + _sep.addClass(this.CLASS_SEPARATOR + '-' + this._sepCount); + if (after) { + var nextSib = null; + if (after.get) { + nextSib = after.get('element').nextSibling; + } else if (after.nextSibling) { + nextSib = after.nextSibling; + } else { + nextSib = after; + } + if (nextSib) { + if (nextSib == after) { + nextSib.parentNode.appendChild(_sep); + } else { + nextSib.parentNode.insertBefore(_sep, nextSib); + } + } + } else { + sepCont.appendChild(_sep); + } + return _sep; + }, + /** + * @method _createColorPicker + * @private + * @description Creates the core DOM reference to the color picker menu item. + * @param {String} id the id of the toolbar to prefix this DOM container with. + */ + _createColorPicker: function(id) { + var c = Y.get('#' + id + '_colors'); + if (c) { + c.get('parentNode').removeChild(c); + } + var picker = Y.Node.create(''); + Y.get('body').appendChild(picker); + + this._colorPicker = picker; + + var html = ''; + Y.each(this._colorData, function(v, k) { + html += '' + k.replace('#', '') + ''; + }, this); + html += 'X'; + picker.set('innerHTML', html); + + picker.on('mouseover', Y.bind(function(ev) { + var picker = this._colorPicker; + var em = picker.query('em'); + var strong = picker.query('strong'); + var tar = ev.target; + if (tar.get('tagName').toLowerCase() == 'a') { + em.setStyle('backgroundColor', tar.getStyle('backgroundColor')); + strong.set('innerHTML', this._colorData['#' + tar.get('innerHTML')] + '
          ' + tar.get('innerHTML')); + } + }, this)); + picker.on('focus', function(ev) { + ev.halt(); + }); + picker.on('click', function(ev) { + ev.halt(); + }); + picker.on('mousedown', Y.bind(function(ev) { + ev.halt(); + var tar = ev.target, data; + if (tar.get('tagName').toLowerCase() == 'a') { + data = tar.get('innerHTML'); + var retVal = this.fire('colorPickerClicked', { type: 'colorPickerClicked', button: this._colorPicker._button, color: data, colorName: this._colorData['#' + data] } ); + var info = { + color: data, + colorName: this._colorData['#' + data], + value: this._colorPicker._button + }; + + this.fire('buttonClick', { type: 'buttonClick', button: info }); + this.getButtonByValue(this._colorPicker._button).getMenu().hide(); + this._colorPicker.setStyle('display', 'none'); + } + }, this)); + }, + /** + * @method _resetColorPicker + * @private + * @description Clears the currently selected color or mouseover color in the color picker. + */ + _resetColorPicker: function() { + var em = this._colorPicker.query('em'); + var strong = this._colorPicker.query('strong'); + em.setStyle('backgroundColor', 'transparent'); + strong.set('innerHTML', ''); + }, + /** + * @method _makeColorButton + * @private + * @description Called to turn a "color" button into a menu button with an Overlay for the menu. + * @param {Object} _oButton YAHOO.widget.ToolbarButton reference + */ + _makeColorButton: function(_oButton) { + if (!this._colorPicker) { + this._createColorPicker(this.get('id')); + } + _oButton.type = 'color'; + _oButton.menu = new Y.Overlay({ + contentBox: '#' + this.get('id') + '_' + _oButton.value + '_menu', + bodyContent: 'FOO', + visible: false, + zIndex: 999 + }); + _oButton.menu.render(this.get('cont')); + _oButton.menu.set('visible', false); + + _oButton.menu.get('contentBox').addClass('yui-button-menu'); + _oButton.menu.get('contentBox').addClass('yui-color-button-menu'); + + _oButton.menu.after('visibleChange', Y.bind(function() { + _oButton.menu.set('align', { + node: this.getButtonById(_oButton.id).get('node'), + points: ['tl', 'bl'] + }); + //Move the DOM reference of the color picker to the Overlay that we are about to show. + this._resetColorPicker(); + var _p = this._colorPicker; + if (_p.get('parentNode')) { + _p.get('parentNode').removeChild(_p); + } + _oButton.menu.get('contentBox').query('.yui-widget-bd').set('innerHTML', ''); + _oButton.menu.get('contentBox').query('.yui-widget-bd').appendChild(_p); + this._colorPicker.setStyle('display', 'block'); + }, this)); + + return _oButton; + }, + /** + * @private + * @method _makeSpinButton + * @description Create a button similar to an OS Spin button.. It has an up/down arrow combo to scroll through a range of int values. + * @param {Object} _button YAHOO.widget.ToolbarButton reference + * @param {Object} oButton Object literal containing the buttons initial config + */ + _makeSpinButton: function(_button, oButton) { + _button.get('node').addClass(this.CLASS_PREFIX + '-spinbutton'); + var self = this, + _par = _button.get('node'), //parentNode of Button Element for appending child + range = oButton.range, + _b1 = Y.Node.create('' + this.STR_SPIN_UP + ''), + _b2 = Y.Node.create('' + this.STR_SPIN_DOWN + ''); + + //Append them to the container + _par.appendChild(_b1); + _par.appendChild(_b2); + + + var label = Y.Lang.substitute(this.STR_SPIN_LABEL, { VALUE: _button.get('label') }); + _button.set('title', label); + + + var cleanVal = function(value) { + value = ((value < range[0]) ? range[0] : value); + value = ((value > range[1]) ? range[1] : value); + return value; + }; + + var br = this.browser; + var tbar = false; + var strLabel = this.STR_SPIN_LABEL; + if (this._titlebar && this._titlebar.get('firstChild')) { + tbar = this._titlebar.get('firstChild'); + } + + var _intUp = function(ev) { + ev.halt(); + if (!_button.get('disabled') && (ev.keyCode != 9)) { + var value = parseInt(_button.get('label'), 10); + value++; + value = cleanVal(value); + _button.set('label', ''+value); + var label = Y.Lang.substitute(strLabel, { VALUE: _button.get('label') }); + _button.set('title', label); + self._buttonClick(ev, oButton); + } + }; + + var _intDown = function(ev) { + ev.halt(); + if (!_button.get('disabled') && (ev.keyCode != 9)) { + var value = parseInt(_button.get('label'), 10); + value--; + value = cleanVal(value); + + _button.set('label', ''+value); + var label = Y.Lang.substitute(strLabel, { VALUE: _button.get('label') }); + _button.set('title', label); + self._buttonClick(ev, oButton); + } + }; + + var _intKeyUp = function(ev) { + if (ev.keyCode == 38) { + _intUp(ev); + } else if (ev.keyCode == 40) { + _intDown(ev); + } else if (ev.keyCode == 107 && ev.shiftKey) { //Plus Key + _intUp(ev); + } else if (ev.keyCode == 109 && ev.shiftKey) { //Minus Key + _intDown(ev); + } + }; + + //Handle arrow keys.. + _button.on('keydown', Y.bind(_intKeyUp, this)); + + //Listen for the click on the up button and act on it + //Listen for the click on the down button and act on it + _b1.on('mousedown',function(ev) { ev.halt(); }); + _b2.on('mousedown',function(ev) { ev.halt(); }); + _b1.on('click', Y.bind(_intUp, this)); + _b2.on('click', Y.bind(_intDown, this)); + }, + /** + * @protected + * @method _buttonClick + * @description Click handler for all buttons in the toolbar. + * @param {String} ev The event that was passed in. + * @param {Object} info Object literal of information about the button that was clicked. + */ + _buttonClick: function(ev, info) { + var doEvent = true; + + if (ev && ev.type == 'keypress') { + if (ev.keyCode == 9) { + doEvent = false; + } else if ((ev.keyCode === 13) || (ev.keyCode === 0) || (ev.keyCode === 32)) { + } else { + doEvent = false; + } + } + + if (doEvent) { + var fireNextEvent = true, + retValue = false; + + info.isSelected = this.isSelected(info.id); + + if (info.value) { + Y.log('fireEvent::' + info.value + 'Click'); + retValue = this.fire(info.value + 'Click', { type: info.value + 'Click', target: this.get('element'), button: info }); + if (retValue === false) { + fireNextEvent = false; + } + } + + if (info.menucmd && fireNextEvent) { + Y.log('fireEvent::' + info.menucmd + 'Click'); + retValue = this.fire(info.menucmd + 'Click', { type: info.menucmd + 'Click', target: this.get('element'), button: info }); + if (retValue === false) { + fireNextEvent = false; + } + } + if (fireNextEvent) { + Y.log('fireEvent::buttonClick'); + this.fire('buttonClick', { type: 'buttonClick', target: this.get('element'), button: info }); + } + + if (info.type == 'select') { + var button = this.getButtonById(info.id); + if (button.buttonType == 'rich') { + var txt = info.value; + for (var i = 0; i < info.menu.length; i++) { + if (info.menu[i].value == info.value) { + txt = info.menu[i].text; + break; + } + } + button.set('label', '' + txt + ''); + var _items = button.getMenu().getItems(); + for (var m = 0; m < _items.length; m++) { + if (_items[m].value.toLowerCase() == info.value.toLowerCase()) { + _items[m].cfg.setProperty('checked', true); + } else { + _items[m].cfg.setProperty('checked', false); + } + } + } + } + if (ev) { + ev.halt(); + } + } + }, + /** + * @private + * @property _keyNav + * @description Flag to determine if the arrow nav listeners have been attached + * @type Boolean + */ + _keyNav: null, + /** + * @private + * @property _navCounter + * @description Internal counter for walking the buttons in the toolbar with the arrow keys + * @type Number + */ + _navCounter: null, + /** + * @private + * @method _navigateButtons + * @description Handles the navigation/focus of toolbar buttons with the Arrow Keys + * @param {Event} ev The Key Event + */ + _navigateButtons: function(ev) { + switch (ev.keyCode) { + case 37: + case 39: + if (ev.keyCode == 37) { + this._navCounter--; + } else { + this._navCounter++; + } + if (this._navCounter > (this._buttonList.length - 1)) { + this._navCounter = 0; + } + if (this._navCounter < 0) { + this._navCounter = (this._buttonList.length - 1); + } + if (this._buttonList[this._navCounter]) { + var el = this._buttonList[this._navCounter].get('node'); + if (this.browser.ie) { + el = this._buttonList[this._navCounter].get('node').query('a'); + } + if (this._buttonList[this._navCounter].get('disabled')) { + this._navigateButtons(ev); + } else { + el.focus(); + } + } + break; + } + }, + /** + * @private + * @method _handleFocus + * @description Sets up the listeners for the arrow key navigation + */ + _handleFocus: function() { + if (!this._keyNav) { + var ev = 'keypress'; + if (this.browser.ie) { + ev = 'keydown'; + } + this.get('node').on(ev, Y.bind(this._navigateButtons, this)); + this._keyNav = true; + this._navCounter = -1; + } + }, + /** + * @method getButtonById + * @description Gets a button instance from the toolbar by is Dom id. + * @param {String} id The Dom id to query for. + * @return {YAHOO.widget.ToolbarButton} + */ + getButtonById: function(id) { + var len = this._buttonList.length; + for (var i = 0; i < len; i++) { + if (this._buttonList[i] && this._buttonList[i].get('id') == id) { + return this._buttonList[i]; + } + } + return false; + }, + /** + * @method getButtonByValue + * @description Gets a button instance or a menuitem instance from the toolbar by it's value. + * @param {String} value The button value to query for. + * @return {YAHOO.widget.ToolbarButton or YAHOO.widget.MenuItem} + */ + getButtonByValue: function(value) { + var _buttons = this.get('buttons'); + if (!_buttons) { + return false; + } + var len = _buttons.length; + for (var i = 0; i < len; i++) { + if (_buttons[i].group !== undefined) { + for (var m = 0; m < _buttons[i].buttons.length; m++) { + if ((_buttons[i].buttons[m].value == value) || (_buttons[i].buttons[m].menucmd == value)) { + return this.getButtonById(_buttons[i].buttons[m].id); + } + if (_buttons[i].buttons[m].menu) { //Menu Button, loop through the values + for (var s = 0; s < _buttons[i].buttons[m].menu.length; s++) { + if (_buttons[i].buttons[m].menu[s].value == value) { + return this.getButtonById(_buttons[i].buttons[m].id); + } + } + } + } + } else { + if ((_buttons[i].value == value) || (_buttons[i].menucmd == value)) { + return this.getButtonById(_buttons[i].id); + } + if (_buttons[i].menu) { //Menu Button, loop through the values + for (var j = 0; j < _buttons[i].menu.length; j++) { + if (_buttons[i].menu[j].value == value) { + return this.getButtonById(_buttons[i].id); + } + } + } + } + } + return false; + }, + /** + * @method getButtonByIndex + * @description Gets a button instance from the toolbar by is index in _buttonList. + * @param {Number} index The index of the button in _buttonList. + * @return {YAHOO.widget.ToolbarButton} + */ + getButtonByIndex: function(index) { + if (this._buttonList[index]) { + return this._buttonList[index]; + } else { + return false; + } + }, + /** + * @method getButtons + * @description Returns an array of buttons in the current toolbar + * @return {Array} + */ + getButtons: function() { + return this._buttonList; + }, + /** + * @method disableButton + * @description Disables a button in the toolbar. + * @param {String/Number} id Disable a button by it's id, index or value. + * @return {Boolean} + */ + disableButton: function(id) { + var button = getButton.call(this, id); + if (button) { + button.set('disabled', true); + } else { + return false; + } + }, + /** + * @method enableButton + * @description Enables a button in the toolbar. + * @param {String/Number} id Enable a button by it's id, index or value. + * @return {Boolean} + */ + enableButton: function(id) { + if (this.get('disabled')) { + return false; + } + var button = getButton.call(this, id); + if (button) { + if (button.get('disabled')) { + button.set('disabled', false); + } + } else { + return false; + } + }, + /** + * @method isSelected + * @description Tells if a button is selected or not. + * @param {String/Number} id A button by it's id, index or value. + * @return {Boolean} + */ + isSelected: function(id) { + var button = getButton.call(this, id); + if (button) { + return button._selected; + } + return false; + }, + /** + * @method selectButton + * @description Selects a button in the toolbar. + * @param {String/Number} id Select a button by it's id, index or value. + * @param {String} value If this is a Menu Button, check this item in the menu + * @return {Boolean} + */ + selectButton: function(id, value) { + var button = getButton.call(this, id); + if (button && (button.get('type') == 'push')) { + button.get('node').addClass('yui-button-selected'); + button.get('node').addClass('yui-button-' + button.get('value') + '-selected'); + button._selected = true; + if (value) { + if (button.buttonType == 'rich') { + var _items = button.getMenu().getItems(); + for (var m = 0; m < _items.length; m++) { + if (_items[m].value == value) { + _items[m].cfg.setProperty('checked', true); + button.set('label', '' + _items[m]._oText.nodeValue + ''); + } else { + _items[m].cfg.setProperty('checked', false); + } + } + } + } + } else { + return false; + } + }, + /** + * @method deselectButton + * @description Deselects a button in the toolbar. + * @param {String/Number} id Deselect a button by it's id, index or value. + * @return {Boolean} + */ + deselectButton: function(id) { + var button = getButton.call(this, id); + if (button) { + button.get('node').removeClass('yui-button-selected'); + button.get('node').removeClass('yui-button-' + button.get('value') + '-selected'); + button.get('node').removeClass('yui-button-hover'); + button._selected = false; + } else { + return false; + } + }, + /** + * @method deselectAllButtons + * @description Deselects all buttons in the toolbar. + * @return {Boolean} + */ + deselectAllButtons: function() { + var len = this._buttonList.length; + for (var i = 0; i < len; i++) { + this.deselectButton(this._buttonList[i]); + } + }, + /** + * @method disableAllButtons + * @description Disables all buttons in the toolbar. + * @return {Boolean} + */ + disableAllButtons: function() { + if (this.get('disabled')) { + return false; + } + var len = this._buttonList.length; + for (var i = 0; i < len; i++) { + this.disableButton(this._buttonList[i]); + } + }, + /** + * @method enableAllButtons + * @description Enables all buttons in the toolbar. + * @return {Boolean} + */ + enableAllButtons: function() { + if (this.get('disabled')) { + return false; + } + var len = this._buttonList.length; + for (var i = 0; i < len; i++) { + this.enableButton(this._buttonList[i]); + } + }, + /** + * @method resetAllButtons + * @description Resets all buttons to their initial state. + * @param {Object} _ex Except these buttons + * @return {Boolean} + */ + resetAllButtons: function(_ex) { + if (!Y.Lang.isObject(_ex)) { + _ex = {}; + } + if (this.get('disabled') || !this._buttonList) { + return false; + } + var len = this._buttonList.length; + for (var i = 0; i < len; i++) { + var _button = this._buttonList[i]; + if (_button) { + //var disabled = _button._configs.disabled._initialConfig.value; + var disabled = false; + if (_ex[_button.get('id')]) { + this.enableButton(_button); + this.selectButton(_button); + } else { + if (disabled) { + this.disableButton(_button); + } else { + this.enableButton(_button); + } + this.deselectButton(_button); + } + } + } + }, + /** + * @method destroyButton + * @description Destroy a button in the toolbar. + * @param {String/Number} id Destroy a button by it's id or index. + * @return {Boolean} + */ + destroyButton: function(id) { + var button = getButton.call(this, id); + if (button) { + var thisID = button.get('id'), + new_list = [], i = 0, + len = this._buttonList.length; + + button.destroy(); + + for (i = 0; i < len; i++) { + if (this._buttonList[i].get('id') != thisID) { + new_list[new_list.length]= this._buttonList[i]; + } + } + + this._buttonList = new_list; + } else { + return false; + } + }, + /** + * @method destroy + * @description Destroys the toolbar, all of it's elements and objects. + * @return {Boolean} + */ + destructor: function() { + var len = this._configuredButtons.length, j, i, b; + for(b = 0; b < len; b++) { + this.destroyButton(this._configuredButtons[b]); + } + + this._configuredButtons = null; + + this.get('element').innerHTML = ''; + this.get('element').className = ''; + //Brutal Object Destroy + for (i in this) { + if (Lang.hasOwnProperty(this, i)) { + this[i] = null; + } + } + return true; + }, + /** + * @method collapse + * @description Programatically collapse the toolbar. + * @param {Boolean} collapse True to collapse, false to expand. + */ + collapse: function(collapse) { + var el = this._titlebar.query('span.collapse'); + if (collapse === false) { + this.get('cont').get('parentNode').removeClass('yui-toolbar-container-collapsed'); + if (el) { + el.removeClass('collapsed'); + el.set('title', this.STR_COLLAPSE); + } + this.fire('toolbarExpanded', { type: 'toolbarExpanded' }); + } else { + if (el) { + el.addClass('collapsed'); + el.set('title', this.STR_EXPAND); + } + this.get('cont').get('parentNode').addClass('yui-toolbar-container-collapsed'); + this.fire('toolbarCollapsed', { type: 'toolbarCollapsed' }); + } + }, + /** + * @method toString + * @description Returns a string representing the toolbar. + * @return {String} + */ + toString: function() { + return 'Toolbar'; + } + }); + + Y.namespace('Y.Toolbar'); + Y.Toolbar.Bar = Toolbar; + + var YAHOO = Y.Port(), + Lang = YAHOO.lang, + Dom = YAHOO.util.Dom, + Event = YAHOO.util.Event; + + var SimpleEditor = function(o) { + Y.log('SimpleEditor Initalizing'); + + var node = Y.get(o.node), + id = node.get('id'); + if (!id) { + id = Y.stamp(node); + node.set('id', id); + } + + var element_cont = Y.Node.create('
          '); + //Hack + Y.get('body').appendChild(element_cont); + o.element_cont = element_cont; + o.editor_wrapper = element_cont.query('div.wrapper'); + o.toolbar_cont = element_cont.query('div.toolbar_cont'); + //Hack + Y.get('body').removeChild(element_cont); + + if (!o.html) { + o.html = '{TITLE}{CONTENT}'; + } + if (!o.css) { + o.css = this._defaultCSS; + } + //console.log(o); + + + SimpleEditor.superclass.constructor.apply(this, arguments); + }; + + SimpleEditor.NAME = 'simpleeditor'; + + SimpleEditor.ATTRS = { + node: { + setter: function(node) { + var n = Y.get(node); + if (!n) { + Y.error('Invalid Node Given: ' + node); + } else { + n = n.item(0); + } + return n; + } + }, + /** + * @config container + * @description Used when dynamically creating the Editor from Javascript with no default textarea. + * We will create one and place it in this container. If no container is passed we will append to document.body. + * @default false + * @type HTMLElement + */ + 'container': { + writeOnce: true, + value: false + }, + /** + * @private + * @config iframe + * @description Internal config for holding the iframe element. + * @default null + * @type HTMLElement + */ + 'iframe': { + value: null + }, + /** + * @private + * @depreciated - No longer used, should use this.get('element') + * @config textarea + * @description Internal config for holding the textarea element (replaced with element). + * @default null + * @type HTMLElement + */ + 'textarea': { + value: null, + writeOnce: true + }, + /** + * @config element_cont + * @description Internal config for the editors container + * @default false + * @type HTMLElement + */ + 'element_cont': { + value: null + }, + /** + * @private + * @config editor_wrapper + * @description The outter wrapper for the entire editor. + * @default null + * @type HTMLElement + */ + 'editor_wrapper': { + value: null + }, + /** + * @config toolbar_cont + * @description Internal config for the toolbars container + * @default false + * @type Boolean + */ + 'toolbar_cont': { + value: null, + writeOnce: true + }, + /** + * @attribute toolbar + * @description The default toolbar config. + * @type Object + */ + 'toolbar': { + value: null + }, + /** + * @attribute height + * @description The height of the editor iframe container, not including the toolbar.. + * @default Best guessed size of the textarea, for best results use CSS to style the height of the textarea or pass it in as an argument + * @type String + */ + 'height': { + value: null, + setter: function(height) { + if (this._rendered) { + this.get('iframe').get('parentNode').setStyle('height', height); + } + } + }, + /** + * @attribute width + * @description The width of the editor container. + * @default Best guessed size of the textarea, for best results use CSS to style the width of the textarea or pass it in as an argument + * @type String + */ + 'width': { + value: null, + setter: function(width) { + if (this._rendered) { + //We have been rendered, change the width + this.get('element_cont').setStyle('width', width); + } + } + }, + /** + * @attribute blankimage + * @description The URL for the image placeholder to put in when inserting an image. + * @default The yahooapis.com address for the current release + 'assets/blankimage.png' + * @type String + */ + 'blankimage': { + value: null + }, + /** + * @attribute css + * @description The Base CSS used to format the content of the editor + * @default
          html {
          +            height: 95%;
          +        }
          +        body {
          +            height: 100%;
          +            padding: 7px; background-color: #fff; font:13px/1.22 arial,helvetica,clean,sans-serif;*font-size:small;*font:x-small;
          +        }
          +        a {
          +            color: blue;
          +            text-decoration: underline;
          +            cursor: pointer;
          +        }
          +        .warning-localfile {
          +            border-bottom: 1px dashed red !important;
          +        }
          +        .yui-busy {
          +            cursor: wait !important;
          +        }
          +        img.selected { //Safari image selection
          +            border: 2px dotted #808080;
          +        }
          +        img {
          +            cursor: pointer !important;
          +            border: none;
          +        }
          +        
          + * @type String + */ + 'css': { + value: null, + writeOnce: true + }, + /** + * @attribute html + * @description The default HTML to be written to the iframe document before the contents are loaded (Note that the DOCTYPE attr will be added at render item) + * @default This HTML requires a few things if you are to override: +

          {TITLE}, {CSS}, {HIDDEN_CSS}, {EXTRA_CSS} and {CONTENT} need to be there, they are passed to YAHOO.lang.substitute to be replace with other strings.

          +

          onload="document.body._rteLoaded = true;" : the onload statement must be there or the editor will not finish loading.

          + +
          +            <html>
          +                <head>
          +                    <title>{TITLE}</title>
          +                    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
          +                    <style>
          +                    {CSS}
          +                    </style>
          +                    <style>
          +                    {HIDDEN_CSS}
          +                    </style>
          +                    <style>
          +                    {EXTRA_CSS}
          +                    </style>
          +                </head>
          +            <body onload="document.body._rteLoaded = true;">
          +            {CONTENT}
          +            </body>
          +            </html>
          +            
          +
          + * @type String + */ + 'html': { + value: null, + writeOnce: true + }, + /** + * @attribute extracss + * @description Extra user defined css to load after the default SimpleEditor CSS + * @default '' + * @type String + */ + 'extracss': { + value: '', + writeOnce: true + }, + /** + * @config setDesignMode + * @description Should the Editor set designMode on the document. Default: true. + * @default true + * @type Boolean + */ + setDesignMode: { + value: true + }, + /** + * @config nodeChangeDelay + * @description Do we wrap the nodeChange method in a timeout for performance. Default: true. + * @default true + * @type Boolean + */ + nodeChangeDelay: { + value: true + }, + /** + * @attribute dompath + * @description Toggle the display of the current Dom path below the editor + * @default false + * @type Boolean + */ + dompath: { + value: false + }, + /** + * @attribute markup + * @description Should we try to adjust the markup for the following types: semantic, css, default or xhtml + * @default "semantic" + * @type String + */ + markup: { + value: 'semantic', + validator: function(markup) { + switch (markup.toLowerCase()) { + case 'semantic': + case 'css': + case 'default': + case 'xhtml': + return true; + } + return false; + } + }, + /** + * @config maxUndo + * @description The max number of undo levels to store. + * @default 30 + * @type Number + */ + maxUndo: { + writeOnce: true, + value: 30 + }, + + /** + * @config ptags + * @description If true, the editor uses <P> tags instead of <br> tags. (Use Shift + Enter to get a <br>) + * @default false + * @type Boolean + */ + ptags: { + writeOnce: true, + value: false + }, + /** + * @config plainText + * @description Process the inital textarea data as if it was plain text. Accounting for spaces, tabs and line feeds. + * @default false + * @type Boolean + */ + plainText: { + writeOnce: true, + value: false + }, + /** + * @private + * @config disabled_iframe + * @description Internal config for holding the iframe element used when disabling the Editor. + * @default null + * @type HTMLElement + */ + disabled_iframe: { + value: null + }, + /** + * @config nodeChangeThreshold + * @description The number of seconds that need to be in between nodeChange processing + * @default 3 + * @type Number + */ + nodeChangeThreshold: { + value: 3, + validator: Y.Lang.isNumber + }, + /** + * @config allowNoEdit + * @description Should the editor check for non-edit fields. It should be noted that this technique is not perfect. If the user does the right things, they will still be able to make changes. + * Such as highlighting an element below and above the content and hitting a toolbar button or a shortcut key. + * @default false + * @type Boolean + */ + allowNoEdit: { + value: false, + validator: Y.Lang.isBoolean + }, + /** + * @config limitCommands + * @description Should the Editor limit the allowed execCommands to the ones available in the toolbar. If true, then execCommand and keyboard shortcuts will fail if they are not defined in the toolbar. + * @default false + * @type Boolean + */ + limitCommands: { + value: false, + validator: Y.Lang.isBoolean + }, + /** + * @attribute removeLineBreaks + * @description Should we remove linebreaks and extra spaces on cleanup + * @default false + * @type Boolean + */ + removeLineBreaks: { + value: false, + validator: Y.Lang.isBoolean + }, + /** + * @config filterWord + * @description Attempt to filter out MS Word HTML from the Editor's output. + * @type Boolean + */ + filterWord: { + value: false, + validator: Y.Lang.isBoolean + }, + /** + * @config insert + * @description If true, selection is not required for: fontname, fontsize, forecolor, backcolor. + * @default false + * @type Boolean + */ + insert: { + value: false + }, + /** + * @config saveEl + * @description When save HTML is called, this element will be updated as well as the source of data. + * @default element + * @type HTMLElement + */ + saveEl: { + value: null, + setter: function(node) { + var n = Y.get(node); + if (!n) { + return false; + } else { + n = n.item(0); + } + return n; + } + }, + /** + * @attribute disabled + * @description This will toggle the editor's disabled state. When the editor is disabled, designMode is turned off and a mask is placed over the iframe so no interaction can take place. + All Toolbar buttons are also disabled so they cannot be used. + * @default false + * @type Boolean + */ + disabled: { + value: false, + setter: function(disabled) { + if (this._rendered) { + this._disableEditor(disabled); + } + } + }, + /** + * @attribute focusAtStart + * @description Should we focus the window when the content is ready? + * @default false + * @type Boolean + */ + focusAtStart: { + value: false, + writeOnce: true + }, + /** + * @attribute handleSubmit + * @description Config handles if the editor will attach itself to the textareas parent form's submit handler. + If it is set to true, the editor will attempt to attach a submit listener to the textareas parent form. + Then it will trigger the editors save handler and place the new content back into the text area before the form is submitted. + * @default false + * @type Boolean + */ + handleSubmit: { + value: false + } + }; + + Y.extend(SimpleEditor, Y.PortBase, { + _handleSubmit: function() { + var exec = this.get('handleSubmit'); + if (this.get('node').get('form')) { + if (!this._formButtons) { + this._formButtons = []; + } + if (exec) { + this.get('node').get('form').on('submit', Y.bind(this._handleFormSubmit, this)); + + var i = this.get('node').get('form').query('input'); + if (i) { + i.each(function(v, k) { + var type = v.get('type'); + if (type && (type.toLowerCase() == 'submit')) { + v.on('click', Y.bind(this._handleFormButtonClick, this)); + this._formButtons.push(v); + } + }, this); + } + } else { + //Event.removeListener(this.get('element').form, 'submit', this._handleFormSubmit); + //if (this._formButtons) { + // Event.removeListener(this._formButtons, 'click', this._handleFormButtonClick); + //} + } + } + }, + _handleInsert: function() { + if (this.get('insert')) { + var buttons = { + fontname: true, + fontsize: true, + forecolor: true, + backcolor: true + }; + var tmp = this._defaultToolbar.buttons; + for (var i = 0; i < tmp.length; i++) { + if (tmp[i].buttons) { + for (var a = 0; a < tmp[i].buttons.length; a++) { + if (tmp[i].buttons[a].value) { + if (buttons[tmp[i].buttons[a].value]) { + delete tmp[i].buttons[a].disabled; + } + } + } + } + } + } + }, + _handleDOMPath: function() { + var dompath = this.get('dompath'); + if (dompath && !this.dompath) { + this.dompath = Y.Node.create('
          '); + this.get('element_cont').get('firstChild').appendChild(this.dompath); + if (this.get('iframe')) { + this._writeDomPath(); + } + } else if (!dompath && this.dompath) { + this.dompath.get('parentNode').removeChild(this.dompath); + this.dompath = null; + } + }, + /** + * @private + * @property _lastCommand + * @description A cache of the last execCommand (used for Undo/Redo so they don't mark an undo level) + * @type String + */ + _lastCommand: null, + _undoNodeChange: function() {}, + _storeUndo: function() {}, + /** + * @private + * @method _checkKey + * @description Checks a keyMap entry against a key event + * @param {Object} k The _keyMap object + * @param {Event} e The Mouse Event + * @return {Boolean} + */ + _checkKey: function(k, e) { + var ret = false; + if ((e.keyCode === k.key)) { + if (k.mods && (k.mods.length > 0)) { + var val = 0; + for (var i = 0; i < k.mods.length; i++) { + if (this.browser.mac) { + if (k.mods[i] == 'ctrl') { + k.mods[i] = 'meta'; + } + } + if (e[k.mods[i] + 'Key'] === true) { + val++; + } + } + if (val === k.mods.length) { + ret = true; + } + } else { + ret = true; + } + } + //YAHOO.log('Shortcut Key Check: (' + k.key + ') return: ' + ret, 'info', 'SimpleEditor'); + return ret; + }, + /** + * @private + * @property _keyMap + * @description Named key maps for various actions in the Editor. Example: CLOSE_WINDOW: { key: 87, mods: ['shift', 'ctrl'] }. + * This entry shows that when key 87 (W) is found with the modifiers of shift and control, the window will close. You can customize this object to tweak keyboard shortcuts. + * @type {Object/Mixed} + */ + _keyMap: { + SELECT_ALL: { + key: 65, //A key + mods: ['ctrl'] + }, + CLOSE_WINDOW: { + key: 87, //W key + mods: ['shift', 'ctrl'] + }, + FOCUS_TOOLBAR: { + key: 27, + mods: ['shift'] + }, + FOCUS_AFTER: { + key: 27 + }, + FONT_SIZE_UP: { + key: 38, + mods: ['shift', 'ctrl'] + }, + FONT_SIZE_DOWN: { + key: 40, + mods: ['shift', 'ctrl'] + }, + CREATE_LINK: { + key: 76, + mods: ['shift', 'ctrl'] + }, + BOLD: { + key: 66, + mods: ['shift', 'ctrl'] + }, + ITALIC: { + key: 73, + mods: ['shift', 'ctrl'] + }, + UNDERLINE: { + key: 85, + mods: ['shift', 'ctrl'] + }, + UNDO: { + key: 90, + mods: ['ctrl'] + }, + REDO: { + key: 90, + mods: ['shift', 'ctrl'] + }, + JUSTIFY_LEFT: { + key: 219, + mods: ['shift', 'ctrl'] + }, + JUSTIFY_CENTER: { + key: 220, + mods: ['shift', 'ctrl'] + }, + JUSTIFY_RIGHT: { + key: 221, + mods: ['shift', 'ctrl'] + } + }, + /** + * @private + * @method _cleanClassName + * @description Makes a useable classname from dynamic data, by dropping it to lowercase and replacing spaces with -'s. + * @param {String} str The classname to clean up + * @return {String} + */ + _cleanClassName: function(str) { + return str.replace(/ /g, '-').toLowerCase(); + }, + /** + * @property _textarea + * @description Flag to determine if we are using a textarea or an HTML Node. + * @type Boolean + */ + _textarea: null, + /** + * @property _docType + * @description The DOCTYPE to use in the editable container. + * @type String + */ + _docType: '', + /** + * @property editorDirty + * @description This flag will be set when certain things in the Editor happen. It is to be used by the developer to check to see if content has changed. + * @type Boolean + */ + editorDirty: null, + /** + * @property _defaultCSS + * @description The default CSS used in the config for 'css'. This way you can add to the config like this: { css: YAHOO.widget.SimpleEditor.prototype._defaultCSS + 'ADD MYY CSS HERE' } + * @type String + */ + _defaultCSS: '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; } .warning-localfile { border-bottom: 1px dashed red !important; } .yui-busy { cursor: wait !important; } img.selected { border: 2px dotted #808080; } img { cursor: pointer !important; border: none; } body.ptags.webkit div.yui-wk-p { margin: 11px 0; } body.ptags.webkit div.yui-wk-div { margin: 0; }', + /** + * @property _defaultToolbar + * @private + * @description Default toolbar config. + * @type Object + */ + _defaultToolbar: null, + /** + * @property _lastButton + * @private + * @description The last button pressed, so we don't disable it. + * @type Object + */ + _lastButton: null, + /** + * @property _baseHREF + * @private + * @description The base location of the editable page (this page) so that relative paths for image work. + * @type String + */ + _baseHREF: function() { + var href = document.location.href; + if (href.indexOf('?') !== -1) { //Remove the query string + href = href.substring(0, href.indexOf('?')); + } + href = href.substring(0, href.lastIndexOf('/')) + '/'; + return href; + }(), + /** + * @property _lastImage + * @private + * @description Safari reference for the last image selected (for styling as selected). + * @type HTMLElement + */ + _lastImage: null, + /** + * @property _blankImageLoaded + * @private + * @description Don't load the blank image more than once.. + * @type Boolean + */ + _blankImageLoaded: null, + /** + * @property _fixNodesTimer + * @private + * @description Holder for the fixNodes timer + * @type Date + */ + _fixNodesTimer: null, + /** + * @property _nodeChangeTimer + * @private + * @description Holds a reference to the nodeChange setTimeout call + * @type Number + */ + _nodeChangeTimer: null, + /** + * @property _nodeChangeDelayTimer + * @private + * @description Holds a reference to the nodeChangeDelay setTimeout call + * @type Number + */ + _nodeChangeDelayTimer: null, + /** + * @property _lastNodeChangeEvent + * @private + * @description Flag to determine the last event that fired a node change + * @type Event + */ + _lastNodeChangeEvent: null, + /** + * @property _lastNodeChange + * @private + * @description Flag to determine when the last node change was fired + * @type Date + */ + _lastNodeChange: 0, + /** + * @property _rendered + * @private + * @description Flag to determine if editor has been rendered or not + * @type Boolean + */ + _rendered: null, + /** + * @property DOMReady + * @private + * @description Flag to determine if DOM is ready or not + * @type Boolean + */ + DOMReady: null, + /** + * @property _selection + * @private + * @description Holder for caching iframe selections + * @type Object + */ + _selection: null, + /** + * @property _mask + * @private + * @description DOM Element holder for the editor Mask when disabled + * @type Object + */ + _mask: null, + /** + * @property _showingHiddenElements + * @private + * @description Status of the hidden elements button + * @type Boolean + */ + _showingHiddenElements: null, + /** + * @property currentWindow + * @description A reference to the currently open EditorWindow + * @type Object + */ + currentWindow: null, + /** + * @property currentEvent + * @description A reference to the current editor event + * @type Event + */ + currentEvent: null, + /** + * @property operaEvent + * @private + * @description setTimeout holder for Opera and Image DoubleClick event.. + * @type Object + */ + operaEvent: null, + /** + * @property currentFont + * @description A reference to the last font selected from the Toolbar + * @type HTMLElement + */ + currentFont: null, + /** + * @property currentElement + * @description A reference to the current working element in the editor + * @type Array + */ + currentElement: null, + /** + * @property dompath + * @description A reference to the dompath container for writing the current working dom path to. + * @type HTMLElement + */ + dompath: null, + /** + * @property beforeElement + * @description A reference to the H2 placed before the editor for Accessibilty. + * @type HTMLElement + */ + beforeElement: null, + /** + * @property afterElement + * @description A reference to the H2 placed after the editor for Accessibilty. + * @type HTMLElement + */ + afterElement: null, + /** + * @property invalidHTML + * @description Contains a list of HTML elements that are invalid inside the editor. They will be removed when they are found. If you set the value of a key to "{ keepContents: true }", then the element will be replaced with a yui-non span to be filtered out when cleanHTML is called. The only tag that is ignored here is the span tag as it will force the Editor into a loop and freeze the browser. However.. all of these tags will be removed in the cleanHTML routine. + * @type Object + */ + invalidHTML: { + form: true, + input: true, + button: true, + select: true, + link: true, + html: true, + body: true, + iframe: true, + script: true, + style: true, + textarea: true + }, + /** + * @property toolbar + * @description Local property containing the YAHOO.widget.Toolbar instance + * @type YAHOO.widget.Toolbar + */ + toolbar: null, + /** + * @private + * @property _contentTimer + * @description setTimeout holder for documentReady check + */ + _contentTimer: null, + /** + * @private + * @property _contentTimerMax + * @description The number of times the loaded content should be checked before giving up. Default: 500 + */ + _contentTimerMax: 500, + /** + * @private + * @property _contentTimerCounter + * @description Counter to check the number of times the body is polled for before giving up + * @type Number + */ + _contentTimerCounter: 0, + /** + * @private + * @property _disabled + * @description The Toolbar items that should be disabled if there is no selection present in the editor. + * @type Array + */ + _disabled: [ 'createlink', 'fontname', 'fontsize', 'forecolor', 'backcolor' ], + /** + * @private + * @property _alwaysDisabled + * @description The Toolbar items that should ALWAYS be disabled event if there is a selection present in the editor. + * @type Object + */ + _alwaysDisabled: { undo: true, redo: true }, + /** + * @private + * @property _alwaysEnabled + * @description The Toolbar items that should ALWAYS be enabled event if there isn't a selection present in the editor. + * @type Object + */ + _alwaysEnabled: { }, + /** + * @private + * @property _semantic + * @description The Toolbar commands that we should attempt to make tags out of instead of using styles. + * @type Object + */ + _semantic: { 'bold': true, 'italic' : true, 'underline' : true }, + /** + * @private + * @property _tag2cmd + * @description A tag map of HTML tags to convert to the different types of commands so we can select the proper toolbar button. + * @type Object + */ + _tag2cmd: { + 'b': 'bold', + 'strong': 'bold', + 'i': 'italic', + 'em': 'italic', + 'u': 'underline', + 'sup': 'superscript', + 'sub': 'subscript', + 'img': 'insertimage', + 'a' : 'createlink', + 'ul' : 'insertunorderedlist', + 'ol' : 'insertorderedlist' + }, + + /** + * @private _createIframe + * @description Creates the DOM and YUI Element for the iFrame editor area. + * @param {String} id The string ID to prefix the iframe with + * @return {Object} iFrame object + */ + _createIframe: function() { + var ifrmDom = document.createElement('iframe'); + ifrmDom.id = this.get('node').get('id') + '_editor'; + var config = { + border: '0', + frameBorder: '0', + marginWidth: '0', + marginHeight: '0', + leftMargin: '0', + topMargin: '0', + allowTransparency: 'true', + width: '100%' + }; + if (this.get('autoHeight')) { + config.scrolling = 'no'; + } + Y.each(config, function(v, k) { + ifrmDom.setAttribute(k, v); + }); + var isrc = 'javascript:;'; + if (this.browser.ie) { + //isrc = 'about:blank'; + //TODO - Check this, I have changed it before.. + isrc = 'javascript:false;'; + } + ifrmDom.setAttribute('src', isrc); + //var ifrm = new YAHOO.util.Element(ifrmDom); + var ifrm = Y.get(ifrmDom); + ifrm.setStyle('visibility', 'hidden'); + return ifrm; + }, + /** + * @private _isElement + * @description Checks to see if an Element reference is a valid one and has a certain tag type + * @param {HTMLElement} el The element to check + * @param {String} tag The tag that the element needs to be + * @return {Boolean} + */ + _isElement: function(el, tag) { + if (el && el.tagName && (el.tagName.toLowerCase() == tag)) { + return true; + } + if (el && el.getAttribute && (el.getAttribute('tag') == tag)) { + return true; + } + return false; + }, + /** + * @private _hasParent + * @description Checks to see if an Element reference or one of it's parents is a valid one and has a certain tag type + * @param {HTMLElement} el The element to check + * @param {String} tag The tag that the element needs to be + * @return HTMLElement + */ + _hasParent: function(el, tag) { + if (!el || !el.parentNode) { + return false; + } + + while (el.parentNode) { + if (this._isElement(el, tag)) { + return el; + } + if (el.parentNode) { + el = el.parentNode; + } else { + return false; + } + } + return false; + }, + /** + * @private + * @method _getDoc + * @description Get the Document of the IFRAME + * @return {Object} + */ + _getDoc: function() { + var value = false; + try { + var f = document.getElementById(this.get('iframe').get('id')); + if (f && f.contentWindow.document) { + value = f.contentWindow.document; + return value; + } + } catch (e) { + return false; + } + }, + /** + * @private + * @method _getWindow + * @description Get the Window of the IFRAME + * @return {Object} + */ + _getWindow: function() { + var f = document.getElementById(this.get('iframe').get('id')); + return f.contentWindow; + }, + /** + * @method focus + * @description Attempt to set the focus of the iframes window. + */ + focus: function() { + this._getWindow().focus(); + }, + /** + * @private + * @depreciated - This should not be used, moved to this.focus(); + * @method _focusWindow + * @description Attempt to set the focus of the iframes window. + */ + _focusWindow: function() { + YAHOO.log('_focusWindow: depreciated in favor of this.focus()', 'warn', 'Editor'); + this.focus(); + }, + /** + * @private + * @method _hasSelection + * @description Determines if there is a selection in the editor document. + * @return {Boolean} + */ + _hasSelection: function() { + var sel = this._getSelection(); + var range = this._getRange(); + var hasSel = false; + + if (!sel || !range) { + return hasSel; + } + + //Internet Explorer + if (this.browser.ie || this.browser.opera) { + if (range.text) { + hasSel = true; + } + if (range.html) { + hasSel = true; + } + } else { + if (this.browser.webkit) { + if (sel+'' !== '') { + hasSel = true; + } + } else { + if (sel && (sel.toString() !== '') && (sel !== undefined)) { + hasSel = true; + } + } + } + return hasSel; + }, + /** + * @private + * @method _getSelection + * @description Handles the different selection objects across the A-Grade list. + * @return {Object} Selection Object + */ + _getSelection: function() { + var _sel = null; + if (this._getDoc() && this._getWindow()) { + if (this._getDoc().selection) { + _sel = this._getDoc().selection; + } else { + _sel = this._getWindow().getSelection(); + } + //Handle Safari's lack of Selection Object + if (this.browser.webkit) { + if (_sel.baseNode) { + this._selection = {}; + this._selection.baseNode = _sel.baseNode; + this._selection.baseOffset = _sel.baseOffset; + this._selection.extentNode = _sel.extentNode; + this._selection.extentOffset = _sel.extentOffset; + } else if (this._selection !== null) { + _sel = this._getWindow().getSelection(); + _sel.setBaseAndExtent( + this._selection.baseNode, + this._selection.baseOffset, + this._selection.extentNode, + this._selection.extentOffset); + this._selection = null; + } + } + } + return _sel; + }, + /** + * @private + * @method _selectNode + * @description Places the highlight around a given node + * @param {HTMLElement} node The node to select + */ + _selectNode: function(node, collapse) { + if (!node) { + return false; + } + var sel = this._getSelection(), + range = null; + + if (this.browser.ie) { + try { //IE freaks out here sometimes.. + range = this._getDoc().body.createTextRange(); + range.moveToElementText(node); + range.select(); + } catch (e) { + YAHOO.log('IE failed to select element: ' + node.tagName, 'warn', 'SimpleEditor'); + } + } else if (this.browser.webkit) { + if (collapse) { + sel.setBaseAndExtent(node, 1, node, node.innerText.length); + } else { + sel.setBaseAndExtent(node, 0, node, node.innerText.length); + } + } else if (this.browser.opera) { + sel = this._getWindow().getSelection(); + range = this._getDoc().createRange(); + range.selectNode(node); + sel.removeAllRanges(); + sel.addRange(range); + } else { + range = this._getDoc().createRange(); + range.selectNodeContents(node); + sel.removeAllRanges(); + sel.addRange(range); + } + //TODO - Check Performance + this.nodeChange(); + }, + /** + * @private + * @method _getRange + * @description Handles the different range objects across the A-Grade list. + * @return {Object} Range Object + */ + _getRange: function() { + var sel = this._getSelection(); + + if (sel === null) { + return null; + } + + if (this.browser.webkit && !sel.getRangeAt) { + var _range = this._getDoc().createRange(); + try { + _range.setStart(sel.anchorNode, sel.anchorOffset); + _range.setEnd(sel.focusNode, sel.focusOffset); + } catch (e) { + _range = this._getWindow().getSelection()+''; + } + return _range; + } + + if (this.browser.ie || this.browser.opera) { + try { + return sel.createRange(); + } catch (e2) { + return null; + } + } + + if (sel.rangeCount > 0) { + return sel.getRangeAt(0); + } + return null; + }, + /** + * @private + * @method _setDesignMode + * @description Sets the designMode property of the iFrame document's body. + * @param {String} state This should be either on or off + */ + _setDesignMode: function(state) { + if (this.get('setDesignMode')) { + try { + this._getDoc().designMode = ((state.toLowerCase() == 'off') ? 'off' : 'on'); + } catch(e) { } + } + }, + /** + * @private + * @method _toggleDesignMode + * @description Toggles the designMode property of the iFrame document on and off. + * @return {String} The state that it was set to. + */ + _toggleDesignMode: function() { + YAHOO.log('It is not recommended to use this method and it will be depreciated.', 'warn', 'SimpleEditor'); + var _dMode = this._getDoc().designMode, + _state = ((_dMode.toLowerCase() == 'on') ? 'off' : 'on'); + this._setDesignMode(_state); + return _state; + }, + /** + * @private + * @property _focused + * @description Holder for trapping focus/blur state and prevent double events + * @type Boolean + */ + _focused: null, + /** + * @private + * @method _handleFocus + * @description Handles the focus of the iframe. Note, this is window focus event, not an Editor focus event. + * @param {Event} e The DOM Event + */ + _handleFocus: function(e) { + if (!this._focused) { + YAHOO.log('Editor Window Focused', 'info', 'SimpleEditor'); + this._focused = true; + this.fireEvent('editorWindowFocus', { type: 'editorWindowFocus', target: this }); + } + }, + /** + * @private + * @method _handleBlur + * @description Handles the blur of the iframe. Note, this is window blur event, not an Editor blur event. + * @param {Event} e The DOM Event + */ + _handleBlur: function(e) { + if (this._focused) { + YAHOO.log('Editor Window Blurred', 'info', 'SimpleEditor'); + this._focused = false; + this.fireEvent('editorWindowBlur', { type: 'editorWindowBlur', target: this }); + } + }, + /** + * @private + * @method _initEditorEvents + * @description This method sets up the listeners on the Editors document. + */ + _initEditorEvents: function() { + //Setup Listeners on iFrame + var doc = this._getDoc(), + win = this._getWindow(); + + Y.Event.nativeAdd(doc, 'mouseup', Y.bind(this._handleMouseUp, this)); + Y.Event.nativeAdd(doc, 'mousedown', Y.bind(this._handleMouseDown, this)); + Y.Event.nativeAdd(doc, 'click', Y.bind(this._handleClick, this)); + Y.Event.nativeAdd(doc, 'dblclick', Y.bind(this._handleDoubleClick, this)); + Y.Event.nativeAdd(doc, 'keypress', Y.bind(this._handleKeyPress, this)); + Y.Event.nativeAdd(doc, 'keyup', Y.bind(this._handleKeyUp, this)); + Y.Event.nativeAdd(doc, 'keydown', Y.bind(this._handleKeyDown, this)); + + //Focus and blur.. + Y.Event.nativeAdd(win, 'focus', Y.bind(this._handleFocus, this)); + Y.Event.nativeAdd(win, 'blur', Y.bind(this._handleBlur, this)); + }, + /** + * @private + * @method _removeEditorEvents + * @description This method removes the listeners on the Editors document (for disabling). + */ + _removeEditorEvents: function() { + //Remove Listeners on iFrame + var doc = this._getDoc(), + win = this._getWindow(); + + Y.Event.nativeRremove(doc, 'mouseup', Y.bind(this._handleMouseUp, this)); + Y.Event.nativeRremove(doc, 'mousedown', Y.bind(this._handleMouseDown, this)); + Y.Event.nativeRremove(doc, 'click', Y.bind(this._handleClick, this)); + Y.Event.nativeRremove(doc, 'dblclick', Y.bind(this._handleDoubleClick, this)); + Y.Event.nativeRremove(doc, 'keypress', Y.bind(this._handleKeyPress, this)); + Y.Event.nativeRremove(doc, 'keyup', Y.bind(this._handleKeyUp, this)); + Y.Event.nativeRremove(doc, 'keydown', Y.bind(this._handleKeyDown, this)); + + //Focus and blur.. + Y.Event.nativeRremove(win, 'focus', Y.bind(this._handleFocus, this)); + Y.Event.nativeRremove(win, 'blur', Y.bind(this._handleBlur, this)); + }, + _fixWebkitDivs: function() { + if (this.browser.webkit) { + var divs = this._getDoc().body.getElementsByTagName('div'); + Dom.addClass(divs, 'yui-wk-div'); + } + }, + /** + * @private + * @method _initEditor + * @param {Boolean} raw Don't add events. + * @description This method is fired from _checkLoaded when the document is ready. It turns on designMode and set's up the listeners. + */ + _initEditor: function(raw) { + if (this._editorInit) { + return; + } + this._editorInit = true; + if (this.browser.ie) { + this._getDoc().body.style.margin = '0'; + } + if (!this.get('disabled')) { + this._setDesignMode('on'); + this._contentTimerCounter = 0; + } + if (!this._getDoc().body) { + YAHOO.log('Body is null, check again', 'error', 'SimpleEditor'); + this._contentTimerCounter = 0; + this._editorInit = false; + this._checkLoaded(); + return false; + } + + YAHOO.log('editorLoaded', 'info', 'SimpleEditor'); + if (!raw) { + this.toolbar.on('buttonClick', Y.bind(this._handleToolbarClick, this)); + } + if (!this.get('disabled')) { + this._initEditorEvents(); + this.toolbar.set('disabled', false); + } + + if (raw) { + this.fire('editorContentReloaded', { type: 'editorreloaded'}); + } else { + this.fire('editorContentLoaded', { type: 'editorLoaded' }); + } + this._fixWebkitDivs(); + if (this.get('dompath')) { + YAHOO.log('Delayed DomPath write', 'info', 'SimpleEditor'); + Y.later(150, this, this._writeDomPath); + } + var br = []; + Y.each(this.browser, function(v, k) { + if (v) { + br.push(k); + } + }, this); + if (this.get('ptags')) { + br.push('ptags'); + } + Y.DOM.addClass(this._getDoc().body, br.join(' ')); + this.nodeChange(true); + }, + /** + * @private + * @method _checkLoaded + * @param {Boolean} raw Don't add events. + * @description Called from a setTimeout loop to check if the iframes body.onload event has fired, then it will init the editor. + */ + _checkLoaded: function(raw) { + this._editorInit = false; + this._contentTimerCounter++; + if (this._contentTimer) { + clearTimeout(this._contentTimer); + } + if (this._contentTimerCounter > this._contentTimerMax) { + YAHOO.log('ERROR: Body Did Not load', 'error', 'SimpleEditor'); + return false; + } + var init = false; + try { + if (this._getDoc() && this._getDoc().body) { + if (this.browser.ie) { + if (this._getDoc().body.readyState == 'complete') { + init = true; + } + } else { + if (this._getDoc().body._rteLoaded === true) { + init = true; + } + } + } + } catch (e) { + init = false; + YAHOO.log('checking body (e)' + e, 'error', 'SimpleEditor'); + } + + if (init === true) { + //The onload event has fired, clean up after ourselves and fire the _initEditor method + YAHOO.log('Firing _initEditor', 'info', 'SimpleEditor'); + this._initEditor(raw); + } else { + var self = this; + this._contentTimer = setTimeout(function() { + self._checkLoaded.call(self, raw); + }, 20); + } + }, + /** + * @private + * @method _setInitialContent + * @param {Boolean} raw Don't add events. + * @description This method will open the iframes content document and write the textareas value into it, then start the body.onload checking. + */ + _setInitialContent: function(raw) { + YAHOO.log('Populating editor body with contents of the text area', 'info', 'SimpleEditor'); + + var value = ((this._textarea) ? this.get('node').get('value') : this.get('node').get('innerHTML')), + doc = null; + + if (value === '') { + value = '
          '; + } + + var html = Y.Lang.substitute(this.get('html'), { + TITLE: this.STR_TITLE, + CONTENT: this._cleanIncomingHTML(value), + CSS: this.get('css'), + HIDDEN_CSS: ((this.get('hiddencss')) ? this.get('hiddencss') : '/* No Hidden CSS */'), + EXTRA_CSS: ((this.get('extracss')) ? this.get('extracss') : '/* No Extra CSS */') + }), + check = true; + + html = html.replace(/RIGHT_BRACKET/gi, '{'); + html = html.replace(/LEFT_BRACKET/gi, '}'); + + if (document.compatMode != 'BackCompat') { + YAHOO.log('Adding Doctype to editable area', 'info', 'SimpleEditor'); + html = this._docType + "\n" + html; + } else { + YAHOO.log('DocType skipped because we are in BackCompat Mode.', 'warn', 'SimpleEditor'); + } + + if (this.browser.ie || this.browser.webkit || this.browser.opera || (navigator.userAgent.indexOf('Firefox/1.5') != -1)) { + //Firefox 1.5 doesn't like setting designMode on an document created with a data url + try { + //Adobe AIR Code + if (this.browser.air) { + doc = this._getDoc().implementation.createHTMLDocument(); + var origDoc = this._getDoc(); + origDoc.open(); + origDoc.close(); + doc.open(); + doc.write(html); + doc.close(); + var node = origDoc.importNode(doc.getElementsByTagName("html")[0], true); + origDoc.replaceChild(node, origDoc.getElementsByTagName("html")[0]); + origDoc.body._rteLoaded = true; + } else { + doc = this._getDoc(); + doc.open(); + doc.write(html); + doc.close(); + } + } catch (e) { + YAHOO.log('Setting doc failed.. (_setInitialContent)', 'error', 'SimpleEditor'); + //Safari will only be here if we are hidden + check = false; + } + } else { + //This keeps Firefox 2 from writing the iframe to history preserving the back buttons functionality + this.get('iframe').set('src', 'data:text/html;charset=utf-8,' + encodeURIComponent(html)); + } + this.get('iframe').setStyle('visibility', ''); + if (check) { + this._checkLoaded(raw); + } + }, + /** + * @private + * @method _setMarkupType + * @param {String} action The action to take. Possible values are: css, default or semantic + * @description This method will turn on/off the useCSS execCommand. + */ + _setMarkupType: function(action) { + switch (this.get('markup')) { + case 'css': + this._setEditorStyle(true); + break; + case 'default': + this._setEditorStyle(false); + break; + case 'semantic': + case 'xhtml': + if (this._semantic[action]) { + this._setEditorStyle(false); + } else { + this._setEditorStyle(true); + } + break; + } + }, + /** + * Set the editor to use CSS instead of HTML + * @param {Booleen} stat True/False + */ + _setEditorStyle: function(stat) { + try { + this._getDoc().execCommand('useCSS', false, !stat); + } catch (ex) { + } + }, + /** + * @private + * @method _getSelectedElement + * @description This method will attempt to locate the element that was last interacted with, either via selection, location or event. + * @return {HTMLElement} The currently selected element. + */ + _getSelectedElement: function() { + var doc = this._getDoc(), + range = null, + sel = null, + elm = null, + check = true; + + if (this.browser.ie) { + this.currentEvent = this._getWindow().event; //Event utility assumes window.event, so we need to reset it to this._getWindow().event; + range = this._getRange(); + if (range) { + elm = range.item ? range.item(0) : range.parentElement(); + if (this._hasSelection()) { + //TODO + //WTF.. Why can't I get an element reference here?!??! + } + if (elm === doc.body) { + elm = null; + } + } + if ((this.currentEvent !== null) && (this.currentEvent.keyCode === 0)) { + elm = Event.getTarget(this.currentEvent); + } + } else { + sel = this._getSelection(); + range = this._getRange(); + + if (!sel || !range) { + return null; + } + //TODO + if (!this._hasSelection() && this.browser.webkit3) { + //check = false; + } + if (this.browser.gecko) { + //Added in 2.6.0 + if (range.startContainer) { + if (range.startContainer.nodeType === 3) { + elm = range.startContainer.parentNode; + } else if (range.startContainer.nodeType === 1) { + elm = range.startContainer; + } + //Added in 2.7.0 + if (this.currentEvent) { + var tar = Event.getTarget(this.currentEvent); + if (!this._isElement(tar, 'html')) { + if (elm !== tar) { + elm = tar; + } + } + } + } + } + + if (check) { + if (sel.anchorNode && (sel.anchorNode.nodeType == 3)) { + if (sel.anchorNode.parentNode) { //next check parentNode + elm = sel.anchorNode.parentNode; + } + if (sel.anchorNode.nextSibling != sel.focusNode.nextSibling) { + elm = sel.anchorNode.nextSibling; + } + } + if (this._isElement(elm, 'br')) { + elm = null; + } + if (!elm) { + elm = range.commonAncestorContainer; + if (!range.collapsed) { + if (range.startContainer == range.endContainer) { + if (range.startOffset - range.endOffset < 2) { + if (range.startContainer.hasChildNodes()) { + elm = range.startContainer.childNodes[range.startOffset]; + } + } + } + } + } + } + } + + if (this.currentEvent !== null) { + try { + switch (this.currentEvent.type) { + case 'click': + case 'mousedown': + case 'mouseup': + if (this.browser.webkit) { + elm = Event.getTarget(this.currentEvent); + } + break; + default: + //Do nothing + break; + } + } catch (e) { + YAHOO.log('Firefox 1.5 errors here: ' + e, 'error', 'SimpleEditor'); + } + } else if ((this.currentElement && this.currentElement[0]) && (!this.browser.ie)) { + //TODO is this still needed? + //elm = this.currentElement[0]; + } + + + if (this.browser.opera || this.browser.webkit) { + if (this.currentEvent && !elm) { + elm = Event.getTarget(this.currentEvent); + } + } + if (!elm || !elm.tagName) { + elm = doc.body; + } + if (this._isElement(elm, 'html')) { + //Safari sometimes gives us the HTML node back.. + elm = doc.body; + } + if (this._isElement(elm, 'body')) { + //make sure that body means this body not the parent.. + elm = doc.body; + } + if (elm && !elm.parentNode) { //Not in document + elm = doc.body; + } + if (elm === undefined) { + elm = null; + } + return elm; + }, + /** + * @private + * @method _getSelectedNode + * @description Calls _getSelectedElement and passes it through Y.get to get a Node reference. + * @return {Y.Node} The currently selected element. + */ + _getSelectedNode: function() { + return Y.get(this._getSelectedElement()); + }, + /** + * @private + * @method _getDomPath + * @description This method will attempt to build the DOM path from the currently selected element. + * @param HTMLElement el The element to start with, if not provided _getSelectedElement is used + * @return {Array} An array of node references that will create the DOM Path. + */ + _getDomPath: function(el) { + if (!el) { + el = this._getSelectedElement(); + } + var domPath = []; + while (el !== null) { + if (el.ownerDocument != this._getDoc()) { + el = null; + break; + } + //Check to see if we get el.nodeName and nodeType + if (el.nodeName && el.nodeType && (el.nodeType == 1)) { + domPath[domPath.length] = el; + } + + if (this._isElement(el, 'body')) { + break; + } + + el = el.parentNode; + } + if (domPath.length === 0) { + if (this._getDoc() && this._getDoc().body) { + domPath[0] = this._getDoc().body; + } + } + return domPath.reverse(); + }, + /** + * @private + * @method _writeDomPath + * @description Write the current DOM path out to the dompath container below the editor. + */ + _writeDomPath: function() { + var path = this._getDomPath(), + pathArr = [], + classPath = '', + pathStr = ''; + + for (var i = 0; i < path.length; i++) { + var tag = path[i].tagName.toLowerCase(); + if ((tag == 'ol') && (path[i].type)) { + tag += ':' + path[i].type; + } + if (Dom.hasClass(path[i], 'yui-tag')) { + tag = path[i].getAttribute('tag'); + } + if ((this.get('markup') == 'semantic') || (this.get('markup') == 'xhtml')) { + switch (tag) { + case 'b': tag = 'strong'; break; + case 'i': tag = 'em'; break; + } + } + if (!Dom.hasClass(path[i], 'yui-non')) { + if (Dom.hasClass(path[i], 'yui-tag')) { + pathStr = tag; + } else { + classPath = ((path[i].className !== '') ? '.' + path[i].className.replace(/ /g, '.') : ''); + if ((classPath.indexOf('yui') != -1) || (classPath.toLowerCase().indexOf('apple-style-span') != -1)) { + classPath = ''; + } + pathStr = tag + ((path[i].id) ? '#' + path[i].id : '') + classPath; + } + switch (tag) { + case 'body': + pathStr = 'body'; + break; + case 'a': + if (path[i].getAttribute('href', 2)) { + pathStr += ':' + path[i].getAttribute('href', 2).replace('mailto:', '').replace('http:/'+'/', '').replace('https:/'+'/', ''); //May need to add others here ftp + } + break; + case 'img': + var h = path[i].height; + var w = path[i].width; + if (path[i].style.height) { + h = parseInt(path[i].style.height, 10); + } + if (path[i].style.width) { + w = parseInt(path[i].style.width, 10); + } + pathStr += '(' + w + 'x' + h + ')'; + break; + } + + if (pathStr.length > 10) { + pathStr = '' + pathStr.substring(0, 10) + '...' + ''; + } else { + pathStr = '' + pathStr + ''; + } + pathArr[pathArr.length] = pathStr; + } + } + var str = pathArr.join(' ' + this.SEP_DOMPATH + ' '); + //Prevent flickering + if (this.dompath.get('innerHTML') != str) { + this.dompath.set('innerHTML', str); + } + }, + /** + * @private + * @method _fixNodes + * @description Fix href and imgs as well as remove invalid HTML. + */ + _fixNodes: function() { + try { + var doc = this._getDoc(), + els = []; + + Y.each(this.invalidHTML, function(v, k) { + if (k.toLowerCase() != 'span') { + var tags = doc.body.getElementsByTagName(k); + if (tags.length) { + for (var i = 0; i < tags.length; i++) { + els.push(tags[i]); + } + } + } + }); + for (var h = 0; h < els.length; h++) { + if (els[h].parentNode) { + if (Lang.isObject(this.invalidHTML[els[h].tagName.toLowerCase()]) && this.invalidHTML[els[h].tagName.toLowerCase()].keepContents) { + this._swapEl(els[h], 'span', function(el) { + el.className = 'yui-non'; + }); + } else { + els[h].parentNode.removeChild(els[h]); + } + } + } + var imgs = this._getDoc().getElementsByTagName('img'); + Dom.addClass(imgs, 'yui-img'); + } catch(e) {} + }, + /** + * @private + * @method _isNonEditable + * @param Event ev The Dom event being checked + * @description Method is called at the beginning of all event handlers to check if this element or a parent element has the class yui-noedit (this.CLASS_NOEDIT) applied. + * If it does, then this method will stop the event and return true. The event handlers will then return false and stop the nodeChange from occuring. This method will also + * disable and enable the Editor's toolbar based on the noedit state. + * @return Boolean + */ + _isNonEditable: function(ev) { + if (this.get('allowNoEdit')) { + var el = Event.getTarget(ev); + if (this._isElement(el, 'html')) { + el = null; + } + var path = this._getDomPath(el); + for (var i = (path.length - 1); i > -1; i--) { + if (Dom.hasClass(path[i], this.CLASS_NOEDIT)) { + //if (this.toolbar.get('disabled') === false) { + // this.toolbar.set('disabled', true); + //} + try { + this._getDoc().execCommand('enableObjectResizing', false, 'false'); + } catch (e) {} + this.nodeChange(); + Event.stopEvent(ev); + YAHOO.log('CLASS_NOEDIT found in DOM Path, stopping event', 'info', 'SimpleEditor'); + return true; + } + } + //if (this.toolbar.get('disabled') === true) { + //Should only happen once.. + //this.toolbar.set('disabled', false); + try { + this._getDoc().execCommand('enableObjectResizing', false, 'true'); + } catch (e2) {} + //} + } + return false; + }, + /** + * @private + * @method _setCurrentEvent + * @param {Event} ev The event to cache + * @description Sets the current event property + */ + _setCurrentEvent: function(ev) { + this.currentEvent = ev; + }, + /** + * @private + * @method _handleClick + * @param {Event} ev The event we are working on. + * @description Handles all click events inside the iFrame document. + */ + _handleClick: function(ev) { + var ret = this.fireEvent('beforeEditorClick', { type: 'beforeEditorClick', target: this, ev: ev }); + if (ret === false) { + return false; + } + if (this._isNonEditable(ev)) { + return false; + } + this._setCurrentEvent(ev); + if (this.currentWindow) { + this.closeWindow(); + } + if (this.currentWindow) { + this.closeWindow(); + } + if (this.browser.webkit) { + var tar =Event.getTarget(ev); + if (this._isElement(tar, 'a') || this._isElement(tar.parentNode, 'a')) { + Event.stopEvent(ev); + this.nodeChange(); + } + } else { + this.nodeChange(); + } + this.fireEvent('editorClick', { type: 'editorClick', target: this, ev: ev }); + }, + /** + * @private + * @method _handleMouseUp + * @param {Event} ev The event we are working on. + * @description Handles all mouseup events inside the iFrame document. + */ + _handleMouseUp: function(ev) { + var ret = this.fireEvent('beforeEditorMouseUp', { type: 'beforeEditorMouseUp', target: this, ev: ev }); + if (ret === false) { + return false; + } + if (this._isNonEditable(ev)) { + return false; + } + //Don't set current event for mouseup. + //It get's fired after a menu is closed and gives up a bogus event to work with + //this._setCurrentEvent(ev); + var self = this; + if (this.browser.opera) { + /* + * @knownissue Opera appears to stop the MouseDown, Click and DoubleClick events on an image inside of a document with designMode on.. + * @browser Opera + * @description This work around traps the MouseUp event and sets a timer to check if another MouseUp event fires in so many seconds. If another event is fired, they we internally fire the DoubleClick event. + */ + var sel = Event.getTarget(ev); + if (this._isElement(sel, 'img')) { + this.nodeChange(); + if (this.operaEvent) { + clearTimeout(this.operaEvent); + this.operaEvent = null; + this._handleDoubleClick(ev); + } else { + this.operaEvent = window.setTimeout(function() { + self.operaEvent = false; + }, 700); + } + } + } + //This will stop Safari from selecting the entire document if you select all the text in the editor + if (this.browser.webkit || this.browser.opera) { + if (this.browser.webkit) { + Event.stopEvent(ev); + } + } + this.nodeChange(); + this.fireEvent('editorMouseUp', { type: 'editorMouseUp', target: this, ev: ev }); + }, + /** + * @private + * @method _handleMouseDown + * @param {Event} ev The event we are working on. + * @description Handles all mousedown events inside the iFrame document. + */ + _handleMouseDown: function(ev) { + var ret = this.fireEvent('beforeEditorMouseDown', { type: 'beforeEditorMouseDown', target: this, ev: ev }); + if (ret === false) { + return false; + } + if (this._isNonEditable(ev)) { + return false; + } + this._setCurrentEvent(ev); + var sel = Event.getTarget(ev); + if (this.browser.webkit && this._hasSelection()) { + var _sel = this._getSelection(); + if (!this.browser.webkit3) { + _sel.collapse(true); + } else { + _sel.collapseToStart(); + } + } + if (this.browser.webkit && this._lastImage) { + Dom.removeClass(this._lastImage, 'selected'); + this._lastImage = null; + } + if (this._isElement(sel, 'img') || this._isElement(sel, 'a')) { + if (this.browser.webkit) { + Event.stopEvent(ev); + if (this._isElement(sel, 'img')) { + Dom.addClass(sel, 'selected'); + this._lastImage = sel; + } + } + if (this.currentWindow) { + this.closeWindow(); + } + this.nodeChange(); + } + this.fireEvent('editorMouseDown', { type: 'editorMouseDown', target: this, ev: ev }); + }, + /** + * @private + * @method _handleDoubleClick + * @param {Event} ev The event we are working on. + * @description Handles all doubleclick events inside the iFrame document. + */ + _handleDoubleClick: function(ev) { + var ret = this.fireEvent('beforeEditorDoubleClick', { type: 'beforeEditorDoubleClick', target: this, ev: ev }); + if (ret === false) { + return false; + } + if (this._isNonEditable(ev)) { + return false; + } + this._setCurrentEvent(ev); + var sel = Event.getTarget(ev); + if (this._isElement(sel, 'img')) { + this.currentElement[0] = sel; + this.toolbar.fireEvent('insertimageClick', { type: 'insertimageClick', target: this.toolbar }); + this.fireEvent('afterExecCommand', { type: 'afterExecCommand', target: this }); + } else if (this._hasParent(sel, 'a')) { //Handle elements inside an a + this.currentElement[0] = this._hasParent(sel, 'a'); + this.toolbar.fireEvent('createlinkClick', { type: 'createlinkClick', target: this.toolbar }); + this.fireEvent('afterExecCommand', { type: 'afterExecCommand', target: this }); + } + this.nodeChange(); + this.fireEvent('editorDoubleClick', { type: 'editorDoubleClick', target: this, ev: ev }); + }, + /** + * @private + * @method _handleKeyUp + * @param {Event} ev The event we are working on. + * @description Handles all keyup events inside the iFrame document. + */ + _handleKeyUp: function(ev) { + var ret = this.fireEvent('beforeEditorKeyUp', { type: 'beforeEditorKeyUp', target: this, ev: ev }); + if (ret === false) { + return false; + } + if (this._isNonEditable(ev)) { + return false; + } + this._setCurrentEvent(ev); + switch (ev.keyCode) { + case this._keyMap.SELECT_ALL.key: + if (this._checkKey(this._keyMap.SELECT_ALL, ev)) { + this.nodeChange(); + } + break; + case 32: //Space Bar + case 35: //End + case 36: //Home + case 37: //Left Arrow + case 38: //Up Arrow + case 39: //Right Arrow + case 40: //Down Arrow + case 46: //Forward Delete + case 8: //Delete + case this._keyMap.CLOSE_WINDOW.key: //W key if window is open + if ((ev.keyCode == this._keyMap.CLOSE_WINDOW.key) && this.currentWindow) { + if (this._checkKey(this._keyMap.CLOSE_WINDOW, ev)) { + this.closeWindow(); + } + } else { + if (!this.browser.ie) { + if (this._nodeChangeTimer) { + clearTimeout(this._nodeChangeTimer); + } + var self = this; + this._nodeChangeTimer = setTimeout(function() { + self._nodeChangeTimer = null; + self.nodeChange.call(self); + }, 100); + } else { + this.nodeChange(); + } + this.editorDirty = true; + } + break; + } + this.fireEvent('editorKeyUp', { type: 'editorKeyUp', target: this, ev: ev }); + this._storeUndo(); + }, + /** + * @private + * @method _handleKeyPress + * @param {Event} ev The event we are working on. + * @description Handles all keypress events inside the iFrame document. + */ + _handleKeyPress: function(ev) { + var ret = this.fireEvent('beforeEditorKeyPress', { type: 'beforeEditorKeyPress', target: this, ev: ev }); + if (ret === false) { + return false; + } + + if (this.get('allowNoEdit')) { + //if (ev && ev.keyCode && ((ev.keyCode == 46) || ev.keyCode == 63272)) { + if (ev && ev.keyCode && (ev.keyCode == 63272)) { + //Forward delete key + YAHOO.log('allowNoEdit is set, forward delete key has been disabled', 'warn', 'SimpleEditor'); + Event.stopEvent(ev); + } + } + if (this._isNonEditable(ev)) { + return false; + } + this._setCurrentEvent(ev); + if (this.browser.opera) { + if (ev.keyCode === 13) { + var tar = this._getSelectedElement(); + if (!this._isElement(tar, 'li')) { + this.execCommand('inserthtml', '
          '); + Event.stopEvent(ev); + } + } + } + if (this.browser.webkit) { + if (!this.browser.webkit3) { + if (ev.keyCode && (ev.keyCode == 122) && (ev.metaKey)) { + //This is CMD + z (for undo) + if (this._hasParent(this._getSelectedElement(), 'li')) { + YAHOO.log('We are in an LI and we found CMD + z, stopping the event', 'warn', 'SimpleEditor'); + Event.stopEvent(ev); + } + } + } + this._listFix(ev); + } + this._fixListDupIds(); + this.fireEvent('editorKeyPress', { type: 'editorKeyPress', target: this, ev: ev }); + }, + /** + * @private + * @method _handleKeyDown + * @param {Event} ev The event we are working on. + * @description Handles all keydown events inside the iFrame document. + */ + _handleKeyDown: function(ev) { + var ret = this.fireEvent('beforeEditorKeyDown', { type: 'beforeEditorKeyDown', target: this, ev: ev }); + if (ret === false) { + return false; + } + var tar = null, _range = null; + if (this._isNonEditable(ev)) { + return false; + } + this._setCurrentEvent(ev); + if (this.currentWindow) { + this.closeWindow(); + } + if (this.currentWindow) { + this.closeWindow(); + } + var doExec = false, + action = null, + value = null, + exec = false; + + //YAHOO.log('keyCode: ' + ev.keyCode, 'info', 'SimpleEditor'); + + switch (ev.keyCode) { + case this._keyMap.FOCUS_TOOLBAR.key: + if (this._checkKey(this._keyMap.FOCUS_TOOLBAR, ev)) { + var h = this.toolbar.getElementsByTagName('h2')[0]; + if (h && h.firstChild) { + h.firstChild.focus(); + } + } else if (this._checkKey(this._keyMap.FOCUS_AFTER, ev)) { + //Focus After Element - Esc + this.afterElement.focus(); + } + Event.stopEvent(ev); + doExec = false; + break; + //case 76: //L + case this._keyMap.CREATE_LINK.key: //L + if (this._hasSelection()) { + if (this._checkKey(this._keyMap.CREATE_LINK, ev)) { + var makeLink = true; + if (this.get('limitCommands')) { + if (!this.toolbar.getButtonByValue('createlink')) { + YAHOO.log('Toolbar Button for (createlink) was not found, skipping exec.', 'info', 'SimpleEditor'); + makeLink = false; + } + } + if (makeLink) { + this.execCommand('createlink', ''); + this.toolbar.fireEvent('createlinkClick', { type: 'createlinkClick', target: this.toolbar }); + this.fireEvent('afterExecCommand', { type: 'afterExecCommand', target: this }); + doExec = false; + } + } + } + break; + //case 90: //Z + case this._keyMap.UNDO.key: + case this._keyMap.REDO.key: + if (this._checkKey(this._keyMap.REDO, ev)) { + action = 'redo'; + doExec = true; + } else if (this._checkKey(this._keyMap.UNDO, ev)) { + action = 'undo'; + doExec = true; + } + break; + //case 66: //B + case this._keyMap.BOLD.key: + if (this._checkKey(this._keyMap.BOLD, ev)) { + action = 'bold'; + doExec = true; + } + break; + case this._keyMap.FONT_SIZE_UP.key: + case this._keyMap.FONT_SIZE_DOWN.key: + var uk = false, dk = false; + if (this._checkKey(this._keyMap.FONT_SIZE_UP, ev)) { + uk = true; + } + if (this._checkKey(this._keyMap.FONT_SIZE_DOWN, ev)) { + dk = true; + } + if (uk || dk) { + var fs_button = this.toolbar.getButtonByValue('fontsize'), + label = parseInt(fs_button.get('label'), 10), + newValue = (label + 1); + + if (dk) { + newValue = (label - 1); + } + + action = 'fontsize'; + value = newValue + 'px'; + doExec = true; + } + break; + //case 73: //I + case this._keyMap.ITALIC.key: + if (this._checkKey(this._keyMap.ITALIC, ev)) { + action = 'italic'; + doExec = true; + } + break; + //case 85: //U + case this._keyMap.UNDERLINE.key: + if (this._checkKey(this._keyMap.UNDERLINE, ev)) { + action = 'underline'; + doExec = true; + } + break; + case 9: + if (this.browser.ie) { + //Insert a tab in Internet Explorer + _range = this._getRange(); + tar = this._getSelectedElement(); + if (!this._isElement(tar, 'li')) { + if (_range) { + _range.pasteHTML('    '); + _range.collapse(false); + _range.select(); + } + Event.stopEvent(ev); + } + } + //Firefox 3 code + if (this.browser.gecko > 1.8) { + tar = this._getSelectedElement(); + if (this._isElement(tar, 'li')) { + if (ev.shiftKey) { + this._getDoc().execCommand('outdent', null, ''); + } else { + this._getDoc().execCommand('indent', null, ''); + } + + } else if (!this._hasSelection()) { + this.execCommand('inserthtml', '    '); + } + Event.stopEvent(ev); + } + break; + case 13: + var p = null, i = 0; + if (this.get('ptags') && !ev.shiftKey) { + if (this.browser.gecko) { + tar = this._getSelectedElement(); + if (!this._hasParent(tar, 'li')) { + if (this._hasParent(tar, 'p')) { + p = this._getDoc().createElement('p'); + p.innerHTML = ' '; + Dom.insertAfter(p, tar); + this._selectNode(p.firstChild); + } else if (this._isElement(tar, 'body')) { + this.execCommand('insertparagraph', null); + var ps = this._getDoc().body.getElementsByTagName('p'); + for (i = 0; i < ps.length; i++) { + if (ps[i].getAttribute('_moz_dirty') !== null) { + p = this._getDoc().createElement('p'); + p.innerHTML = ' '; + Dom.insertAfter(p, ps[i]); + this._selectNode(p.firstChild); + ps[i].removeAttribute('_moz_dirty'); + } + } + } else { + YAHOO.log('Something went wrong with paragraphs, please file a bug!!', 'error', 'SimpleEditor'); + doExec = true; + action = 'insertparagraph'; + } + Event.stopEvent(ev); + } + } + if (this.browser.webkit) { + tar = this._getSelectedElement(); + if (!this._hasParent(tar, 'li')) { + this.execCommand('insertparagraph', null); + var divs = this._getDoc().body.getElementsByTagName('div'); + for (i = 0; i < divs.length; i++) { + if (!Dom.hasClass(divs[i], 'yui-wk-div')) { + Dom.addClass(divs[i], 'yui-wk-p'); + } + } + Event.stopEvent(ev); + } + } + } else { + if (this.browser.webkit) { + tar = this._getSelectedElement(); + if (!this._hasParent(tar, 'li')) { + if (this.browser.webkit4) { + this.execCommand('insertlinebreak'); + } else { + this.execCommand('inserthtml', ''); + var holder = this._getDoc().getElementById('yui-br'), + br = this._getDoc().createElement('br'), + caret = this._getDoc().createElement('span'); + + holder.parentNode.replaceChild(br, holder); + caret.className = 'yui-non'; + caret.innerHTML = ' '; + Dom.insertAfter(caret, br); + this._selectNode(caret); + } + Event.stopEvent(ev); + } + } + if (this.browser.ie) { + YAHOO.log('Stopping P tags', 'info', 'SimpleEditor'); + //Insert a
          instead of a

          in Internet Explorer + _range = this._getRange(); + tar = this._getSelectedElement(); + if (!this._isElement(tar, 'li')) { + if (_range) { + _range.pasteHTML('
          '); + _range.collapse(false); + _range.select(); + } + Event.stopEvent(ev); + } + } + } + break; + } + if (this.browser.ie) { + this._listFix(ev); + } + if (doExec && action) { + this.execCommand(action, value); + Event.stopEvent(ev); + this.nodeChange(); + } + this.fireEvent('editorKeyDown', { type: 'editorKeyDown', target: this, ev: ev }); + }, + /** + * @private + * @property _fixListRunning + * @type Boolean + * @description Keeps more than one _fixListDupIds from running at the same time. + */ + _fixListRunning: null, + /** + * @private + * @method _fixListDupIds + * @description Some browsers will duplicate the id of an LI when created in designMode. + * This method will fix the duplicate id issue. However it will only preserve the first element + * in the document list with the unique id. + */ + _fixListDupIds: function() { + if (this._fixListRunning) { + return false; + } + if (this._getDoc()) { + this._fixListRunning = true; + var lis = this._getDoc().body.getElementsByTagName('li'), + i = 0, ids = {}; + for (i = 0; i < lis.length; i++) { + if (lis[i].id) { + if (ids[lis[i].id]) { + lis[i].id = ''; + } + ids[lis[i].id] = true; + } + } + this._fixListRunning = false; + } + }, + /** + * @private + * @method _listFix + * @param {Event} ev The event we are working on. + * @description Handles the Enter key, Tab Key and Shift + Tab keys for List Items. + */ + _listFix: function(ev) { + //YAHOO.log('Lists Fix (' + ev.keyCode + ')', 'info', 'SimpleEditor'); + var testLi = null, par = null, preContent = false, range = null; + //Enter Key + if (this.browser.webkit) { + if (ev.keyCode && (ev.keyCode == 13)) { + if (this._hasParent(this._getSelectedElement(), 'li')) { + var tar = this._hasParent(this._getSelectedElement(), 'li'); + if (tar.previousSibling) { + if (tar.firstChild && (tar.firstChild.length == 1)) { + this._selectNode(tar); + } + } + } + } + } + //Shift + Tab Key + if (ev.keyCode && ((!this.browser.webkit3 && (ev.keyCode == 25)) || ((this.browser.webkit3 || !this.browser.webkit) && ((ev.keyCode == 9) && ev.shiftKey)))) { + testLi = this._getSelectedElement(); + if (this._hasParent(testLi, 'li')) { + testLi = this._hasParent(testLi, 'li'); + YAHOO.log('We have a SHIFT tab in an LI, reverse it..', 'info', 'SimpleEditor'); + if (this._hasParent(testLi, 'ul') || this._hasParent(testLi, 'ol')) { + YAHOO.log('We have a double parent, move up a level', 'info', 'SimpleEditor'); + par = this._hasParent(testLi, 'ul'); + if (!par) { + par = this._hasParent(testLi, 'ol'); + } + //YAHOO.log(par.previousSibling + ' :: ' + par.previousSibling.innerHTML); + if (this._isElement(par.previousSibling, 'li')) { + par.removeChild(testLi); + par.parentNode.insertBefore(testLi, par.nextSibling); + if (this.browser.ie) { + range = this._getDoc().body.createTextRange(); + range.moveToElementText(testLi); + range.collapse(false); + range.select(); + } + if (this.browser.webkit) { + this._selectNode(testLi.firstChild); + } + Event.stopEvent(ev); + } + } + } + } + //Tab Key + if (ev.keyCode && ((ev.keyCode == 9) && (!ev.shiftKey))) { + YAHOO.log('List Fix - Tab', 'info', 'SimpleEditor'); + var preLi = this._getSelectedElement(); + if (this._hasParent(preLi, 'li')) { + preContent = this._hasParent(preLi, 'li').innerHTML; + } + //YAHOO.log('preLI: ' + preLi.tagName + ' :: ' + preLi.innerHTML); + if (this.browser.webkit) { + this._getDoc().execCommand('inserttext', false, '\t'); + } + testLi = this._getSelectedElement(); + if (this._hasParent(testLi, 'li')) { + YAHOO.log('We have a tab in an LI', 'info', 'SimpleEditor'); + par = this._hasParent(testLi, 'li'); + YAHOO.log('parLI: ' + par.tagName + ' :: ' + par.innerHTML); + var newUl = this._getDoc().createElement(par.parentNode.tagName.toLowerCase()); + if (this.browser.webkit) { + var span = Dom.getElementsByClassName('Apple-tab-span', 'span', par); + //Remove the span element that Safari puts in + if (span[0]) { + par.removeChild(span[0]); + par.innerHTML = Lang.trim(par.innerHTML); + //Put the HTML from the LI into this new LI + if (preContent) { + par.innerHTML = '' + preContent + ' '; + } else { + par.innerHTML = '  '; + } + } + } else { + if (preContent) { + par.innerHTML = preContent + ' '; + } else { + par.innerHTML = ' '; + } + } + + par.parentNode.replaceChild(newUl, par); + newUl.appendChild(par); + if (this.browser.webkit) { + this._getSelection().setBaseAndExtent(par.firstChild, 1, par.firstChild, par.firstChild.innerText.length); + if (!this.browser.webkit3) { + par.parentNode.parentNode.style.display = 'list-item'; + setTimeout(function() { + par.parentNode.parentNode.style.display = 'block'; + }, 1); + } + } else if (this.browser.ie) { + range = this._getDoc().body.createTextRange(); + range.moveToElementText(par); + range.collapse(false); + range.select(); + } else { + this._selectNode(par); + } + Event.stopEvent(ev); + } + if (this.browser.webkit) { + Event.stopEvent(ev); + } + this.nodeChange(); + } + }, + /** + * @method nodeChange + * @param {Boolean} force Optional paramenter to skip the threshold counter + * @description Handles setting up the toolbar buttons, getting the Dom path, fixing nodes. + */ + nodeChange: function(force) { + var NCself = this; + this._storeUndo(); + if (this.get('nodeChangeDelay')) { + this._nodeChangeDelayTimer = window.setTimeout(function() { + NCself._nodeChangeDelayTimer = null; + NCself._nodeChange.apply(NCself, arguments); + }, 0); + } else { + this._nodeChange(); + } + }, + /** + * @private + * @method _nodeChange + * @param {Boolean} force Optional paramenter to skip the threshold counter + * @description Fired from nodeChange in a setTimeout. + */ + _nodeChange: function(force) { + var threshold = parseInt(this.get('nodeChangeThreshold'), 10), + thisNodeChange = Math.round(new Date().getTime() / 1000), + self = this; + + if (force === true) { + this._lastNodeChange = 0; + } + + if ((this._lastNodeChange + threshold) < thisNodeChange) { + if (this._fixNodesTimer === null) { + this._fixNodesTimer = window.setTimeout(function() { + self._fixNodes.call(self); + self._fixNodesTimer = null; + }, 0); + } + } + this._lastNodeChange = thisNodeChange; + if (this.currentEvent) { + try { + this._lastNodeChangeEvent = this.currentEvent.type; + } catch (e) {} + } + + var beforeNodeChange = this.fire('beforeNodeChange', { type: 'beforeNodeChange' }); + if (beforeNodeChange === false) { + return false; + } + if (this.get('dompath')) { + Y.later(0, this, this._writeDomPath); + } + //Check to see if we are disabled before continuing + if (!this.get('disabled')) { + if (this.STOP_NODE_CHANGE) { + //Reset this var for next action + this.STOP_NODE_CHANGE = false; + return false; + } else { + var sel = this._getSelection(), + range = this._getRange(), + el = this._getSelectedElement(), + fn_button = this.toolbar.getButtonByValue('fontname'), + fs_button = this.toolbar.getButtonByValue('fontsize'), + undo_button = this.toolbar.getButtonByValue('undo'), + redo_button = this.toolbar.getButtonByValue('redo'); + + //Handle updating the toolbar with active buttons + var _ex = {}; + if (this._lastButton) { + _ex[this._lastButton.id] = true; + //this._lastButton = null; + } + if (!this._isElement(el, 'body')) { + if (fn_button) { + _ex[fn_button.get('id')] = true; + } + if (fs_button) { + _ex[fs_button.get('id')] = true; + } + } + if (redo_button) { + delete _ex[redo_button.get('id')]; + } + this.toolbar.resetAllButtons(_ex); + + //Handle disabled buttons + for (var d = 0; d < this._disabled.length; d++) { + var _button = this.toolbar.getButtonByValue(this._disabled[d]); + if (_button && _button.get) { + if (this._lastButton && (_button.get('id') === this._lastButton.id)) { + //Skip + } else { + if (!this._hasSelection() && !this.get('insert')) { + switch (this._disabled[d]) { + case 'fontname': + case 'fontsize': + break; + default: + //No Selection - disable + this.toolbar.disableButton(_button); + } + } else { + if (!this._alwaysDisabled[this._disabled[d]]) { + this.toolbar.enableButton(_button); + } + } + if (!this._alwaysEnabled[this._disabled[d]]) { + this.toolbar.deselectButton(_button); + } + } + } + } + var path = this._getDomPath(); + var tag = null, cmd = null; + for (var i = 0; i < path.length; i++) { + tag = path[i].tagName.toLowerCase(); + if (path[i].getAttribute('tag')) { + tag = path[i].getAttribute('tag').toLowerCase(); + } + cmd = this._tag2cmd[tag]; + if (cmd === undefined) { + cmd = []; + } + if (!Lang.isArray(cmd)) { + cmd = [cmd]; + } + + //Bold and Italic styles + if (path[i].style.fontWeight.toLowerCase() == 'bold') { + cmd[cmd.length] = 'bold'; + } + if (path[i].style.fontStyle.toLowerCase() == 'italic') { + cmd[cmd.length] = 'italic'; + } + if (path[i].style.textDecoration.toLowerCase() == 'underline') { + cmd[cmd.length] = 'underline'; + } + if (path[i].style.textDecoration.toLowerCase() == 'line-through') { + cmd[cmd.length] = 'strikethrough'; + } + if (cmd.length > 0) { + for (var j = 0; j < cmd.length; j++) { + this.toolbar.selectButton(cmd[j]); + this.toolbar.enableButton(cmd[j]); + } + } + //Handle Alignment + switch (path[i].style.textAlign.toLowerCase()) { + case 'left': + case 'right': + case 'center': + case 'justify': + var alignType = path[i].style.textAlign.toLowerCase(); + if (path[i].style.textAlign.toLowerCase() == 'justify') { + alignType = 'full'; + } + this.toolbar.selectButton('justify' + alignType); + this.toolbar.enableButton('justify' + alignType); + break; + } + } + //After for loop + + //Reset Font Family and Size to the inital configs + if (fn_button) { + var family = fn_button._conf.data.initValue.label; + fn_button.set('label', '' + family + ''); + this._updateMenuChecked('fontname', family); + } + + if (fs_button) { + fs_button.set('label', fs_button._conf.data.initValue.label); + } + + var hd_button = this.toolbar.getButtonByValue('heading'); + if (hd_button) { + hd_button.set('label', hd_button._conf.data.initValue.label); + this._updateMenuChecked('heading', 'none'); + } + var img_button = this.toolbar.getButtonByValue('insertimage'); + if (img_button && this.currentWindow && (this.currentWindow.name == 'insertimage')) { + this.toolbar.disableButton(img_button); + } + if (this._lastButton && this._lastButton.isSelected) { + this.toolbar.deselectButton(this._lastButton.id); + } + this._undoNodeChange(); + } + } + + this.fire('afterNodeChange', { type: 'afterNodeChange' }); + }, + /** + * @private + * @method _updateMenuChecked + * @param {Object} button The command identifier of the button you want to check + * @param {String} value The value of the menu item you want to check + * @param {YAHOO.widget.Toolbar} The Toolbar instance the button belongs to (defaults to this.toolbar) + * @description Gets the menu from a button instance, if the menu is not rendered it will render it. It will then search the menu for the specified value, unchecking all other items and checking the specified on. + */ + _updateMenuChecked: function(button, value, tbar) { + if (!tbar) { + tbar = this.toolbar; + } + var _button = tbar.getButtonByValue(button); + _button.checkValue(value); + }, + /** + * @private + * @method _handleToolbarClick + * @param {Event} ev The event that triggered the button click + * @description This is an event handler attached to the Toolbar's buttonClick event. It will fire execCommand with the command identifier from the Toolbar Button. + */ + _handleToolbarClick: function(ev) { + var value = ''; + var str = ''; + var cmd = ev.button.value; + if (ev.button.menucmd) { + value = cmd; + cmd = ev.button.menucmd; + } + this._lastButton = ev.button; + if (this.STOP_EXEC_COMMAND) { + YAHOO.log('execCommand skipped because we found the STOP_EXEC_COMMAND flag set to true', 'warn', 'SimpleEditor'); + YAHOO.log('NOEXEC::execCommand::(' + cmd + '), (' + value + ')', 'warn', 'SimpleEditor'); + this.STOP_EXEC_COMMAND = false; + return false; + } else { + this.execCommand(cmd, value); + if (!this.browser.webkit) { + var Fself = this; + setTimeout(function() { + Fself.focus.call(Fself); + }, 5); + } + } + Event.stopEvent(ev); + }, + /** + * @private + * @method _setupAfterElement + * @description Creates the accessibility h2 header and places it after the iframe in the Dom for navigation. + */ + _setupAfterElement: function() { + if (!this.beforeElement) { + this.beforeElement = Y.Node.create('

          '); + this.beforeElement.set('innerHTML', this.STR_BEFORE_EDITOR); + this.get('element_cont').query('.first-child').insertBefore(this.beforeElement, this.toolbar.get('node').get('nextSibling')); + } + if (!this.afterElement) { + this.afterElement = Y.Node.create('

          ' + this.STR_LEAVE_EDITOR + '

          '); + this.get('element_cont').get('firstChild').appendChild(this.afterElement); + } + }, + /** + * @private + * @method _disableEditor + * @param {Boolean} disabled Pass true to disable, false to enable + * @description Creates a mask to place over the Editor. + */ + _disableEditor: function(disabled) { + var iframe, par, html, height; + if (!this.get('disabled_iframe') && disabled) { + iframe = this._createIframe(); + iframe.set('id', 'disabled_' + this.get('iframe').get('id')); + iframe.setStyle('height', '100%'); + iframe.setStyle('display', 'none'); + iframe.setStyle('visibility', 'visible'); + this.set('disabled_iframe', iframe); + par = this.get('iframe').get('parentNode'); + par.appendChild(iframe); + } + if (!iframe) { + iframe = this.get('disabled_iframe'); + } + if (disabled) { + this._orgIframe = this.get('iframe'); + + if (this.toolbar) { + this.toolbar.set('disabled', true); + } + + html = this.getEditorHTML(); + height = this.get('iframe').get('offsetHeight'); + iframe.setStyle('visibility', ''); + iframe.setStyle('position', ''); + iframe.setStyle('top', ''); + iframe.setStyle('left', ''); + this._orgIframe.setStyle('visibility', 'hidden'); + this._orgIframe.setStyle('position', 'absolute'); + this._orgIframe.setStyle('top', '-99999px'); + this._orgIframe.setStyle('left', '-99999px'); + this.set('iframe', iframe); + this._setInitialContent(true); + + if (!this._mask) { + this._mask = Y.Node.create('
          '); + if (this.browser.ie) { + this._mask.setStyle('height', height + 'px'); + } + this.get('iframe').get('parentNode').appendChild(this._mask); + } + this.on('editorContentReloaded', function() { + this._getDoc().body._rteLoaded = false; + this.setEditorHTML(html); + iframe.setStyle('display', 'block'); + }); + } else { + if (this._mask) { + this._mask.get('parentNode').removeChild(this._mask); + this._mask = null; + if (this.toolbar) { + this.toolbar.set('disabled', false); + } + iframe.setStyle('visibility', 'hidden'); + iframe.setStyle('position', 'absolute'); + iframe.setStyle('top', '-99999px'); + iframe.setStyle('left', '-99999px'); + this._orgIframe.setStyle('visibility', ''); + this._orgIframe.setStyle('position', ''); + this._orgIframe.setStyle('top', ''); + this._orgIframe.setStyle('left', ''); + this.set('iframe', this._orgIframe); + + this.focus(); + Y.later(100, this, this.nodeChange); + } + } + }, + /** + * @property SEP_DOMPATH + * @description The value to place in between the Dom path items + * @type String + */ + SEP_DOMPATH: '<', + /** + * @property STR_LEAVE_EDITOR + * @description The accessibility string for the element after the iFrame + * @type String + */ + STR_LEAVE_EDITOR: 'You have left the Rich Text Editor.', + /** + * @property STR_BEFORE_EDITOR + * @description The accessibility string for the element before the iFrame + * @type String + */ + STR_BEFORE_EDITOR: 'This text field can contain stylized text and graphics. To cycle through all formatting options, use the keyboard shortcut Shift + Escape to place focus on the toolbar and navigate between options with your arrow keys. To exit this text editor use the Escape key and continue tabbing.

          Common formatting keyboard shortcuts:

          • Control Shift B sets text to bold
          • Control Shift I sets text to italic
          • Control Shift U underlines text
          • Control Shift L adds an HTML link
          ', + /** + * @property STR_TITLE + * @description The Title of the HTML document that is created in the iFrame + * @type String + */ + STR_TITLE: 'Rich Text Area.', + /** + * @property STR_IMAGE_HERE + * @description The text to place in the URL textbox when using the blankimage. + * @type String + */ + STR_IMAGE_HERE: 'Image URL Here', + /** + * @property STR_IMAGE_URL + * @description The label string for Image URL + * @type String + */ + STR_IMAGE_URL: 'Image URL', + /** + * @property STR_LINK_URL + * @description The label string for the Link URL. + * @type String + */ + STR_LINK_URL: 'Link URL', + /** + * @protected + * @property STOP_EXEC_COMMAND + * @description Set to true when you want the default execCommand function to not process anything + * @type Boolean + */ + STOP_EXEC_COMMAND: false, + /** + * @protected + * @property STOP_NODE_CHANGE + * @description Set to true when you want the default nodeChange function to not process anything + * @type Boolean + */ + STOP_NODE_CHANGE: false, + /** + * @protected + * @property CLASS_NOEDIT + * @description CSS class applied to elements that are not editable. + * @type String + */ + CLASS_NOEDIT: 'yui-noedit', + /** + * @protected + * @property CLASS_CONTAINER + * @description Default CSS class to apply to the editors container element + * @type String + */ + CLASS_CONTAINER: 'yui-editor-container', + /** + * @protected + * @property CLASS_EDITABLE + * @description Default CSS class to apply to the editors iframe element + * @type String + */ + CLASS_EDITABLE: 'yui-editor-editable', + /** + * @protected + * @property CLASS_EDITABLE_CONT + * @description Default CSS class to apply to the editors iframe's parent element + * @type String + */ + CLASS_EDITABLE_CONT: 'yui-editor-editable-container', + /** + * @protected + * @property CLASS_PREFIX + * @description Default prefix for dynamically created class names + * @type String + */ + CLASS_PREFIX: 'yui-editor', + /** + * @property browser + * @description Standard browser detection + * @type Object + */ + browser: function() { + var br = Y.UA; + //Check for webkit3 + if (br.webkit >= 420) { + br.webkit3 = br.webkit; + } else { + br.webkit3 = 0; + } + if (br.webkit >= 530) { + br.webkit4 = br.webkit; + } else { + br.webkit4 = 0; + } + br.mac = false; + //Check for Mac + if (navigator.userAgent.indexOf('Macintosh') !== -1) { + br.mac = true; + } + + return br; + }(), + /** + * @method init + * @description The Editor class' initialization method + */ + initializer: function() { + Y.log('SimpleEditor initializer'); + + if (!this._defaultToolbar) { + this._defaultToolbar = { + collapse: true, + titlebar: 'Text Editing Tools', + draggable: false, + buttons: [ + { group: 'fontstyle', label: 'Font Name and Size', + buttons: [ + { type: 'select', label: 'Arial', value: 'fontname', disabled: true, + menu: [ + { text: 'Arial', checked: true }, + { text: 'Arial Black' }, + { text: 'Comic Sans MS' }, + { text: 'Courier New' }, + { text: 'Lucida Console' }, + { text: 'Tahoma' }, + { text: 'Times New Roman' }, + { text: 'Trebuchet MS' }, + { text: 'Verdana' } + ] + }, + { type: 'spin', label: '13', value: 'fontsize', range: [ 9, 75 ], disabled: true } + ] + }, + { type: 'separator' }, + { group: 'textstyle', label: 'Font Style', + buttons: [ + { type: 'push', label: 'Bold CTRL + SHIFT + B', value: 'bold' }, + { type: 'push', label: 'Italic CTRL + SHIFT + I', value: 'italic' }, + { type: 'push', label: 'Underline CTRL + SHIFT + U', value: 'underline' }, + { type: 'push', label: 'Strike Through', value: 'strikethrough' }, + { type: 'separator' }, + { type: 'color', label: 'Font Color', value: 'forecolor', disabled: true }, + { type: 'color', label: 'Background Color', value: 'backcolor', disabled: true } + + ] + }, + { type: 'separator' }, + { group: 'indentlist', label: 'Lists', + buttons: [ + { type: 'push', label: 'Create an Unordered List', value: 'insertunorderedlist' }, + { type: 'push', label: 'Create an Ordered List', value: 'insertorderedlist' } + ] + }, + { type: 'separator' }, + { group: 'insertitem', label: 'Insert Item', + buttons: [ + { type: 'push', label: 'HTML Link CTRL + SHIFT + L', value: 'createlink', disabled: true }, + { type: 'push', label: 'Insert Image', value: 'insertimage' } + ] + } + ] + }; + } + + Y.Editor.Info._instances[this.get('node').get('id')] = this; + + this.currentElement = []; + + this._handleDOMPath(); + this.on('dompathChange', Y.bind(this._handleDOMPath, this)); + this.set('blankimage', this._getBlankImage()); + this._handleInsert(); + this.set('saveEl', this.get('node')); + this._handleSubmit(); + this.on('handleSubmitChange', Y.bind(this._handleSubmit, this)); + if (this.get('focusAtStart')) { + this.on('editorContentLoaded', function() { + Y.later(400, this, function() { + this.focus(); + this.editorDirty = false; + }); + }); + } + }, + /** + * @private + * @method _getBlankImage + * @description Retrieves the full url of the image to use as the blank image. + * @return {String} The URL to the blank image + */ + _getBlankImage: function() { + var img = ''; + if (!this._blankImageLoaded) { + if (Y.Editor.Info.blankImage) { + this.set('blankimage', Y.Editor.Info.blankImage); + this._blankImageLoaded = true; + } else { + var div = document.createElement('div'); + div.style.position = 'absolute'; + div.style.top = '-9999px'; + div.style.left = '-9999px'; + div.className = this.CLASS_PREFIX + '-blankimage'; + document.body.appendChild(div); + img = Dom.getStyle(div, 'backgroundImage'); + img = img.replace('url(', '').replace(')', '').replace(/"/g, ''); + //Adobe AIR Code + img = img.replace('app:/', ''); + this.set('blankimage', img); + this._blankImageLoaded = true; + div.parentNode.removeChild(div); + Y.Editor.Info.blankImage = img; + } + } else { + img = this.get('blankimage'); + } + return img; + }, + /** + * @private + * @method _handleAutoHeight + * @description Handles resizing the editor's height based on the content + */ + _handleAutoHeight: function() { + var doc = this._getDoc(), + body = doc.body, + docEl = doc.documentElement; + + var height = parseInt(Dom.getStyle(this.get('editor_wrapper'), 'height'), 10); + var newHeight = body.scrollHeight; + if (this.browser.webkit) { + newHeight = docEl.scrollHeight; + } + if (newHeight < parseInt(this.get('height'), 10)) { + newHeight = parseInt(this.get('height'), 10); + } + if ((height != newHeight) && (newHeight >= parseInt(this.get('height'), 10))) { + var anim = this.get('animate'); + this.set('animate', false); + this.set('height', newHeight + 'px'); + this.set('animate', anim); + if (this.browser.ie) { + //Internet Explorer needs this + this.get('iframe').setStyle('height', '99%'); + this.get('iframe').setStyle('zoom', '1'); + var self = this; + window.setTimeout(function() { + self.get('iframe').setStyle('height', '100%'); + }, 1); + } + } + }, + /** + * @private + * @property _formButtons + * @description Array of buttons that are in the Editor's parent form (for handleSubmit) + * @type Array + */ + _formButtons: null, + /** + * @private + * @property _formButtonClicked + * @description The form button that was clicked to submit the form. + * @type HTMLElement + */ + _formButtonClicked: null, + /** + * @private + * @method _handleFormButtonClick + * @description The click listener assigned to each submit button in the Editor's parent form. + * @param {Event} ev The click event + */ + _handleFormButtonClick: function(ev) { + var tar = Event.getTarget(ev); + this._formButtonClicked = tar; + }, + /** + * @private + * @method _handleFormSubmit + * @description Handles the form submission. + * @param {Object} ev The Form Submit Event + */ + _handleFormSubmit: function(ev) { + this.saveHTML(); + + var form = this.get('node').get('form'), + tar = this._formButtonClicked || false; + + form.detach('submit', Y.bind(this._handleFormSubmit, this)); + //console.log(form); + /* + if (this.browser.ie) { + if (tar && !tar.disabled) { + tar.click(); + } + } else { // Gecko, Opera, and Safari + if (tar && !tar.disabled) { + tar.click(); + } + var oEvent = document.createEvent("HTMLEvents"); + oEvent.initEvent("submit", true, true); + form.dispatchEvent(oEvent); + if (YAHOO.env.ua.webkit) { + if (YAHOO.lang.isFunction(form.submit)) { + form.submit(); + } + } + } + */ + //2.6.0 + //Removed this, not need since removing Safari 2.x + //Event.stopEvent(ev); + }, + /** + * @private + * @method _handleFontSize + * @description Handles the font size button in the toolbar. + * @param {Object} o Object returned from Toolbar's buttonClick Event + */ + _handleFontSize: function(o) { + var button = this.toolbar.getButtonById(o.button.id); + var value = button.get('label') + 'px'; + this.execCommand('fontsize', value); + return false; + }, + /** + * @private + * @description Handles the colorpicker buttons in the toolbar. + * @param {Object} o Object returned from Toolbar's buttonClick Event + */ + _handleColorPicker: function(o) { + var cmd = o.button; + var value = '#' + o.color; + if ((cmd == 'forecolor') || (cmd == 'backcolor')) { + this.execCommand(cmd, value); + } + }, + /** + * @private + * @method _handleAlign + * @description Handles the alignment buttons in the toolbar. + * @param {Object} o Object returned from Toolbar's buttonClick Event + */ + _handleAlign: function(o) { + var cmd = null; + for (var i = 0; i < o.button.menu.length; i++) { + if (o.button.menu[i].value == o.button.value) { + cmd = o.button.menu[i].value; + } + } + var value = this._getSelection(); + + this.execCommand(cmd, value); + return false; + }, + /** + * @private + * @method _handleAfterNodeChange + * @description Fires after a nodeChange happens to setup the things that where reset on the node change (button state). + */ + _handleAfterNodeChange: function() { + var path = this._getDomPath(), + elm = null, + family = null, + fontsize = null, + validFont = false, + fn_button = this.toolbar.getButtonByValue('fontname'), + fs_button = this.toolbar.getButtonByValue('fontsize'), + hd_button = this.toolbar.getButtonByValue('heading'); + + for (var i = 0; i < path.length; i++) { + elm = path[i]; + + var tag = elm.tagName.toLowerCase(); + + + if (elm.getAttribute('tag')) { + tag = elm.getAttribute('tag'); + } + + family = elm.getAttribute('face'); + if (Dom.getStyle(elm, 'fontFamily')) { + family = Dom.getStyle(elm, 'fontFamily'); + //Adobe AIR Code + family = family.replace(/'/g, ''); + } + + if (tag.substring(0, 1) == 'h') { + if (hd_button) { + /* + for (var h = 0; h < hd_button._conf.data.initValue.menu.length; h++) { + if (hd_button._conf.data.initValue.menu.value[h].value.toLowerCase() == tag) { + hd_button.set('label', hd_button._configs.menu.value[h].text); + } + } + this._updateMenuChecked('heading', tag); + */ + } + } + } + + if (fn_button) { + /* + for (var b = 0; b < fn_button._conf.data.initValue.menu.value.length; b++) { + if (family && fn_button._configs.menu.value[b].text.toLowerCase() == family.toLowerCase()) { + validFont = true; + family = fn_button._configs.menu.value[b].text; //Put the proper menu name in the button + } + } + */ + if (!validFont) { + family = fn_button._conf.data.initValue.label; + } + var familyLabel = '' + family + ''; + if (fn_button.get('label') != familyLabel) { + fn_button.set('label', familyLabel); + this._updateMenuChecked('fontname', family); + } + } + + if (fs_button) { + fontsize = parseInt(Dom.getStyle(elm, 'fontSize'), 10); + if ((fontsize === null) || isNaN(fontsize)) { + fontsize = fs_button._conf.data.initValue.label; + } + fs_button.set('label', ''+fontsize); + } + + if (!this._isElement(elm, 'body') && !this._isElement(elm, 'img')) { + this.toolbar.enableButton(fn_button); + this.toolbar.enableButton(fs_button); + this.toolbar.enableButton('forecolor'); + this.toolbar.enableButton('backcolor'); + } + if (this._isElement(elm, 'img')) { + if (Y.Overlay) { + this.toolbar.enableButton('createlink'); + } + } + if (this._hasParent(elm, 'blockquote')) { + this.toolbar.selectButton('indent'); + this.toolbar.disableButton('indent'); + this.toolbar.enableButton('outdent'); + } + if (this._hasParent(elm, 'ol') || this._hasParent(elm, 'ul')) { + this.toolbar.disableButton('indent'); + } + this._lastButton = null; + + }, + /** + * @private + * @method _handleInsertImageClick + * @description Opens the Image Properties Window when the insert Image button is clicked or an Image is Double Clicked. + */ + _handleInsertImageClick: function() { + if (this.get('limitCommands')) { + if (!this.toolbar.getButtonByValue('insertimage')) { + YAHOO.log('Toolbar Button for (insertimage) was not found, skipping exec.', 'info', 'SimpleEditor'); + return false; + } + } + + this.toolbar.set('disabled', true); //Disable the toolbar when the prompt is showing + var _handleAEC = function() { + var el = this.currentElement[0], + src = 'http://'; + if (!el) { + el = this._getSelectedElement(); + } + if (el) { + if (el.getAttribute('src')) { + src = el.getAttribute('src', 2); + if (src.indexOf(this.get('blankimage')) != -1) { + src = this.STR_IMAGE_HERE; + } + } + } + var str = prompt(this.STR_IMAGE_URL + ': ', src); + if ((str !== '') && (str !== null)) { + el.setAttribute('src', str); + } else if (str === '') { + el.parentNode.removeChild(el); + this.currentElement = []; + this.nodeChange(); + } else if ((str === null)) { + src = el.getAttribute('src', 2); + if (src.indexOf(this.get('blankimage')) != -1) { + el.parentNode.removeChild(el); + this.currentElement = []; + this.nodeChange(); + } + } + this.closeWindow(); + this.toolbar.set('disabled', false); + this.unsubscribe('afterExecCommand', _handleAEC, this, true); + }; + this.on('afterExecCommand', _handleAEC, this, true); + }, + /** + * @private + * @method _handleInsertImageWindowClose + * @description Handles the closing of the Image Properties Window. + */ + _handleInsertImageWindowClose: function() { + this.nodeChange(); + }, + /** + * @private + * @method _isLocalFile + * @param {String} url THe url/string to check + * @description Checks to see if a string (href or img src) is possibly a local file reference.. + */ + _isLocalFile: function(url) { + if ((url) && (url !== '') && ((url.indexOf('file:/') != -1) || (url.indexOf(':\\') != -1))) { + return true; + } + return false; + }, + /** + * @private + * @method _handleCreateLinkClick + * @description Handles the opening of the Link Properties Window when the Create Link button is clicked or an href is doubleclicked. + */ + _handleCreateLinkClick: function() { + if (this.get('limitCommands')) { + if (!this.toolbar.getButtonByValue('createlink')) { + YAHOO.log('Toolbar Button for (createlink) was not found, skipping exec.', 'info', 'SimpleEditor'); + return false; + } + } + + this.toolbar.set('disabled', true); //Disable the toolbar when the prompt is showing + + var _handleAEC = function() { + var el = this.currentElement[0], + url = ''; + + if (el) { + if (el.getAttribute('href', 2) !== null) { + url = el.getAttribute('href', 2); + } + } + var str = prompt(this.STR_LINK_URL + ': ', url); + if ((str !== '') && (str !== null)) { + var urlValue = str; + if ((urlValue.indexOf(':/'+'/') == -1) && (urlValue.substring(0,1) != '/') && (urlValue.substring(0, 6).toLowerCase() != 'mailto')) { + if ((urlValue.indexOf('@') != -1) && (urlValue.substring(0, 6).toLowerCase() != 'mailto')) { + //Found an @ sign, prefix with mailto: + urlValue = 'mailto:' + urlValue; + } else { + /* :// not found adding */ + if (urlValue.substring(0, 1) != '#') { + //urlValue = 'http:/'+'/' + urlValue; + } + } + } + el.setAttribute('href', urlValue); + } else if (str !== null) { + var _span = this._getDoc().createElement('span'); + _span.innerHTML = el.innerHTML; + Dom.addClass(_span, 'yui-non'); + el.parentNode.replaceChild(_span, el); + } + this.closeWindow(); + this.toolbar.set('disabled', false); + this.unsubscribe('afterExecCommand', _handleAEC, this, true); + }; + this.on('afterExecCommand', _handleAEC, this); + + }, + /** + * @private + * @method _handleCreateLinkWindowClose + * @description Handles the closing of the Link Properties Window. + */ + _handleCreateLinkWindowClose: function() { + this.nodeChange(); + this.currentElement = []; + }, + /** + * @method render + * @description Calls the private method _render in a setTimeout to allow for other things on the page to continue to load. + */ + render: function() { + if (this._rendered) { + return false; + } + YAHOO.log('Render', 'info', 'SimpleEditor'); + + if (this.get('node')) { + this._textarea = true; + if (this.get('node').get('tagName').toLowerCase() !== 'textarea') { + this._textarea = false; + } + } else { + YAHOO.log('No Element', 'error', 'SimpleEditor'); + return false; + } + this._rendered = true; + Y.later(4, this, this._render); + }, + /** + * @private + * @method _render + * @description Causes the toolbar and the editor to render and replace the textarea. + */ + _render: function() { + YAHOO.log('_render'); + this.set('textarea', this.get('node')); + + this.get('element_cont').setStyle('display', 'none'); + this.get('element_cont').addClass(this.CLASS_CONTAINER); + + this.set('iframe', this._createIframe()); + + Y.later(10, this, this._setInitialContent, false); + + this.get('editor_wrapper').appendChild(this.get('iframe')); + + if (this.get('disabled')) { + this._disableEditor(true); + } + + var tbarConf = this.get('toolbar'); + if (!tbarConf) { + tbarConf = this._defaultToolbar; + } + //Create Toolbar instance + if (tbarConf instanceof Y.Toolbar.Bar) { + this.toolbar = tbarConf; + //Set the toolbar to disabled until content is loaded + this.toolbar.set('disabled', true); + } else { + tbarConf.node = this.get('toolbar_cont'); + //Set the toolbar to disabled until content is loaded + tbarConf.disabled = true; + this.toolbar = new Y.Toolbar.Bar(tbarConf); + } + + YAHOO.log('fireEvent::toolbarLoaded', 'info', 'SimpleEditor'); + this.fire('toolbarLoaded', { type: 'toolbarLoaded' }); + + + this.toolbar.on('toolbarCollapsed', function() { + if (this.currentWindow) { + this.moveWindow(); + } + }, this, true); + this.toolbar.on('toolbarExpanded', function() { + if (this.currentWindow) { + this.moveWindow(); + } + }, this, true); + this.toolbar.on('fontsizeClick', this._handleFontSize, this, true); + + this.toolbar.on('colorPickerClicked', function(o) { + this._handleColorPicker(o); + this.STOP_EXEC_COMMAND = true; + }, this, true); + + this.toolbar.on('alignClick', this._handleAlign, this, true); + this.on('afterNodeChange', this._handleAfterNodeChange, this, true); + this.toolbar.on('insertimageClick', this._handleInsertImageClick, this, true); + this.on('windowinsertimageClose', this._handleInsertImageWindowClose, this, true); + this.toolbar.on('createlinkClick', this._handleCreateLinkClick, this, true); + this.on('windowcreatelinkClose', this._handleCreateLinkWindowClose, this, true); + + + //Replace Textarea with editable area + this.get('node').get('parentNode').replaceChild(this.get('element_cont'), this.get('node')); + + this.get('node').setStyle('visibility', 'hidden'); + this.get('node').setStyle('position', 'absolute'); + this.get('node').setStyle('top', '-9999px'); + this.get('node').setStyle('left', '-9999px'); + this.get('element_cont').appendChild(this.get('node')); + this.get('element_cont').setStyle('display', 'block'); + + + this.get('iframe').get('parentNode').addClass(this.CLASS_EDITABLE_CONT); + this.get('iframe').addClass(this.CLASS_EDITABLE); + + //Set height and width of editor container + this.get('element_cont').setStyle('width', this.get('width')); + this.get('iframe').get('parentNode').setStyle('height', this.get('height')); + + this.get('iframe').setStyle('width', '100%'); //WIDTH + this.get('iframe').setStyle('height', '100%'); + + Y.later(0, this, this._setupAfterElement); + this.fire('afterRender', { type: 'afterRender' }); + }, + /** + * @method execCommand + * @param {String} action The "execCommand" action to try to execute (Example: bold, insertimage, inserthtml) + * @param {String} value (optional) The value for a given action such as action: fontname value: 'Verdana' + * @description This method attempts to try and level the differences in the various browsers and their support for execCommand actions + */ + execCommand: function(action, value) { + var beforeExec = this.fireEvent('beforeExecCommand', { type: 'beforeExecCommand', target: this, args: arguments }); + if ((beforeExec === false) || (this.STOP_EXEC_COMMAND)) { + this.STOP_EXEC_COMMAND = false; + return false; + } + this._lastCommand = action; + this._setMarkupType(action); + if (this.browser.ie) { + this._getWindow().focus(); + } + var exec = true; + + if (this.get('limitCommands')) { + if (!this.toolbar.getButtonByValue(action)) { + YAHOO.log('Toolbar Button for (' + action + ') was not found, skipping exec.', 'info', 'SimpleEditor'); + exec = false; + } + } + + this.editorDirty = true; + + if ((typeof this['cmd_' + action.toLowerCase()] == 'function') && exec) { + YAHOO.log('Found execCommand override method: (cmd_' + action.toLowerCase() + ')', 'info', 'SimpleEditor'); + var retValue = this['cmd_' + action.toLowerCase()](value); + exec = retValue[0]; + if (retValue[1]) { + action = retValue[1]; + } + if (retValue[2]) { + value = retValue[2]; + } + } + if (exec) { + YAHOO.log('execCommand::(' + action + '), (' + value + ')', 'info', 'SimpleEditor'); + try { + this._getDoc().execCommand(action, false, value); + } catch(e) { + YAHOO.log('execCommand Failed', 'error', 'SimpleEditor'); + } + } else { + YAHOO.log('OVERRIDE::execCommand::(' + action + '),(' + value + ') skipped', 'warn', 'SimpleEditor'); + } + this.on('afterExecCommand', function() { + this.unsubscribeAll('afterExecCommand'); + this.nodeChange(); + }, this, true); + this.fireEvent('afterExecCommand', { type: 'afterExecCommand', target: this }); + + }, + /* {{{ Command Overrides */ + + /** + * @method cmd_bold + * @param value Value passed from the execCommand method + * @description This is an execCommand override method. It is called from execCommand when the execCommand('bold') is used. + */ + cmd_bold: function(value) { + if (!this.browser.webkit) { + var el = this._getSelectedElement(); + if (el && this._isElement(el, 'span') && this._hasSelection()) { + if (el.style.fontWeight == 'bold') { + el.style.fontWeight = ''; + var b = this._getDoc().createElement('b'), + par = el.parentNode; + par.replaceChild(b, el); + b.appendChild(el); + } + } + } + return [true]; + }, + /** + * @method cmd_italic + * @param value Value passed from the execCommand method + * @description This is an execCommand override method. It is called from execCommand when the execCommand('italic') is used. + */ + + cmd_italic: function(value) { + if (!this.browser.webkit) { + var el = this._getSelectedElement(); + if (el && this._isElement(el, 'span') && this._hasSelection()) { + if (el.style.fontStyle == 'italic') { + el.style.fontStyle = ''; + var i = this._getDoc().createElement('i'), + par = el.parentNode; + par.replaceChild(i, el); + i.appendChild(el); + } + } + } + return [true]; + }, + + + /** + * @method cmd_underline + * @param value Value passed from the execCommand method + * @description This is an execCommand override method. It is called from execCommand when the execCommand('underline') is used. + */ + cmd_underline: function(value) { + if (!this.browser.webkit) { + var el = this._getSelectedElement(); + if (el && this._isElement(el, 'span')) { + if (el.style.textDecoration == 'underline') { + el.style.textDecoration = 'none'; + } else { + el.style.textDecoration = 'underline'; + } + return [false]; + } + } + return [true]; + }, + /** + * @method cmd_backcolor + * @param value Value passed from the execCommand method + * @description This is an execCommand override method. It is called from execCommand when the execCommand('backcolor') is used. + */ + cmd_backcolor: function(value) { + var exec = true, + el = this._getSelectedElement(), + action = 'backcolor'; + + if (this.browser.gecko || this.browser.opera) { + this._setEditorStyle(true); + action = 'hilitecolor'; + } + + if (!this._isElement(el, 'body') && !this._hasSelection()) { + el.style.backgroundColor = value; + this._selectNode(el); + exec = false; + } else { + if (this.get('insert')) { + el = this._createInsertElement({ backgroundColor: value }); + } else { + this._createCurrentElement('span', { backgroundColor: value, color: el.style.color, fontSize: el.style.fontSize, fontFamily: el.style.fontFamily }); + this._selectNode(this.currentElement[0]); + } + exec = false; + } + + return [exec, action]; + }, + /** + * @method cmd_forecolor + * @param value Value passed from the execCommand method + * @description This is an execCommand override method. It is called from execCommand when the execCommand('forecolor') is used. + */ + cmd_forecolor: function(value) { + var exec = true, + el = this._getSelectedElement(); + + if (!this._isElement(el, 'body') && !this._hasSelection()) { + Dom.setStyle(el, 'color', value); + this._selectNode(el); + exec = false; + } else { + if (this.get('insert')) { + el = this._createInsertElement({ color: value }); + } else { + this._createCurrentElement('span', { color: value, fontSize: el.style.fontSize, fontFamily: el.style.fontFamily, backgroundColor: el.style.backgroundColor }); + this._selectNode(this.currentElement[0]); + } + exec = false; + } + return [exec]; + }, + /** + * @method cmd_unlink + * @param value Value passed from the execCommand method + * @description This is an execCommand override method. It is called from execCommand when the execCommand('unlink') is used. + */ + cmd_unlink: function(value) { + this._swapEl(this.currentElement[0], 'span', function(el) { + el.className = 'yui-non'; + }); + return [false]; + }, + /** + * @method cmd_createlink + * @param value Value passed from the execCommand method + * @description This is an execCommand override method. It is called from execCommand when the execCommand('createlink') is used. + */ + cmd_createlink: function(value) { + var el = this._getSelectedElement(), _a = null; + if (this._hasParent(el, 'a')) { + this.currentElement[0] = this._hasParent(el, 'a'); + } else if (this._isElement(el, 'li')) { + _a = this._getDoc().createElement('a'); + _a.innerHTML = el.innerHTML; + el.innerHTML = ''; + el.appendChild(_a); + this.currentElement[0] = _a; + } else if (!this._isElement(el, 'a')) { + this._createCurrentElement('a'); + _a = this._swapEl(this.currentElement[0], 'a'); + this.currentElement[0] = _a; + } else { + this.currentElement[0] = el; + } + return [false]; + }, + /** + * @method cmd_insertimage + * @param value Value passed from the execCommand method + * @description This is an execCommand override method. It is called from execCommand when the execCommand('insertimage') is used. + */ + cmd_insertimage: function(value) { + var exec = true, _img = null, action = 'insertimage', + el = this._getSelectedElement(); + + if (value === '') { + value = this.get('blankimage'); + } + + /* + * @knownissue Safari Cursor Position + * @browser Safari 2.x + * @description The issue here is that we have no way of knowing where the cursor position is + * inside of the iframe, so we have to place the newly inserted data in the best place that we can. + */ + + YAHOO.log('InsertImage: ' + el.tagName, 'info', 'SimpleEditor'); + if (this._isElement(el, 'img')) { + this.currentElement[0] = el; + exec = false; + } else { + if (this._getDoc().queryCommandEnabled(action)) { + this._getDoc().execCommand(action, false, value); + var imgs = this._getDoc().getElementsByTagName('img'); + for (var i = 0; i < imgs.length; i++) { + if (!Dom.hasClass(imgs[i], 'yui-img')) { + Dom.addClass(imgs[i], 'yui-img'); + this.currentElement[0] = imgs[i]; + } + } + exec = false; + } else { + if (el == this._getDoc().body) { + _img = this._getDoc().createElement('img'); + _img.setAttribute('src', value); + Dom.addClass(_img, 'yui-img'); + this._getDoc().body.appendChild(_img); + } else { + this._createCurrentElement('img'); + _img = this._getDoc().createElement('img'); + _img.setAttribute('src', value); + Dom.addClass(_img, 'yui-img'); + this.currentElement[0].parentNode.replaceChild(_img, this.currentElement[0]); + } + this.currentElement[0] = _img; + exec = false; + } + } + return [exec]; + }, + /** + * @method cmd_inserthtml + * @param value Value passed from the execCommand method + * @description This is an execCommand override method. It is called from execCommand when the execCommand('inserthtml') is used. + */ + cmd_inserthtml: function(value) { + var exec = true, action = 'inserthtml', _span = null, _range = null; + /* + * @knownissue Safari cursor position + * @browser Safari 2.x + * @description The issue here is that we have no way of knowing where the cursor position is + * inside of the iframe, so we have to place the newly inserted data in the best place that we can. + */ + if (this.browser.webkit && !this._getDoc().queryCommandEnabled(action)) { + YAHOO.log('More Safari DOM tricks (inserthtml)', 'info', 'EditorSafari'); + this._createCurrentElement('img'); + _span = this._getDoc().createElement('span'); + _span.innerHTML = value; + this.currentElement[0].parentNode.replaceChild(_span, this.currentElement[0]); + exec = false; + } else if (this.browser.ie) { + _range = this._getRange(); + if (_range.item) { + _range.item(0).outerHTML = value; + } else { + _range.pasteHTML(value); + } + exec = false; + } + return [exec]; + }, + /** + * @method cmd_list + * @param tag The tag of the list you want to create (eg, ul or ol) + * @description This is a combined execCommand override method. It is called from the cmd_insertorderedlist and cmd_insertunorderedlist methods. + */ + cmd_list: function(tag) { + var exec = true, list = null, li = 0, el = null, str = '', + selEl = this._getSelectedElement(), action = 'insertorderedlist'; + if (tag == 'ul') { + action = 'insertunorderedlist'; + } + /* + * @knownissue Safari 2.+ doesn't support ordered and unordered lists + * @browser Safari 2.x + * The issue with this workaround is that when applied to a set of text + * that has BR's in it, Safari may or may not pick up the individual items as + * list items. This is fixed in WebKit (Safari 3) + * 2.6.0: Seems there are still some issues with List Creation and Safari 3, reverting to previously working Safari 2.x code + */ + //if ((this.browser.webkit && !this._getDoc().queryCommandEnabled(action))) { + if (this.browser.webkit && !this.browser.webkit4) { + if (this._isElement(selEl, 'li') && this._isElement(selEl.parentNode, tag)) { + YAHOO.log('We already have a list, undo it', 'info', 'SimpleEditor'); + el = selEl.parentNode; + list = this._getDoc().createElement('span'); + Dom.addClass(list, 'yui-non'); + str = ''; + var lis = el.getElementsByTagName('li'); + for (li = 0; li < lis.length; li++) { + str += '
          ' + lis[li].innerHTML + '
          '; + } + list.innerHTML = str; + this.currentElement[0] = el; + this.currentElement[0].parentNode.replaceChild(list, this.currentElement[0]); + } else { + YAHOO.log('Create list item', 'info', 'SimpleEditor'); + this._createCurrentElement(tag.toLowerCase()); + list = this._getDoc().createElement(tag); + for (li = 0; li < this.currentElement.length; li++) { + var newli = this._getDoc().createElement('li'); + newli.innerHTML = this.currentElement[li].innerHTML + '  '; + list.appendChild(newli); + if (li > 0) { + this.currentElement[li].parentNode.removeChild(this.currentElement[li]); + } + } + + var items = list.firstChild.innerHTML.split('
          '); + if (items.length > 0) { + list.innerHTML = ''; + for (var i = 0; i < items.length; i++) { + var item = this._getDoc().createElement('li'); + item.innerHTML = items[i]; + list.appendChild(item); + } + } + + this.currentElement[0].parentNode.replaceChild(list, this.currentElement[0]); + this.currentElement[0] = list; + var _h = this.currentElement[0].firstChild; + _h = Dom.getElementsByClassName('yui-non', 'span', _h)[0]; + this._getSelection().setBaseAndExtent(_h, 1, _h, _h.innerText.length); + } + exec = false; + } else { + el = this._getSelectedElement(); + YAHOO.log(el.tagName); + if (this._isElement(el, 'li') && this._isElement(el.parentNode, tag) || (this.browser.ie && this._isElement(this._getRange().parentElement, 'li')) || (this.browser.ie && this._isElement(el, 'ul')) || (this.browser.ie && this._isElement(el, 'ol'))) { //we are in a list.. + YAHOO.log('We already have a list, undo it', 'info', 'SimpleEditor'); + if (this.browser.ie) { + if ((this.browser.ie && this._isElement(el, 'ul')) || (this.browser.ie && this._isElement(el, 'ol'))) { + el = el.getElementsByTagName('li')[0]; + } + YAHOO.log('Undo IE', 'info', 'SimpleEditor'); + str = ''; + var lis2 = el.parentNode.getElementsByTagName('li'); + for (var j = 0; j < lis2.length; j++) { + str += lis2[j].innerHTML + '
          '; + } + var newEl = this._getDoc().createElement('span'); + newEl.innerHTML = str; + el.parentNode.parentNode.replaceChild(newEl, el.parentNode); + } else { + this.nodeChange(); + this._getDoc().execCommand(action, '', el.parentNode); + this.nodeChange(); + } + exec = false; + } + if (this.browser.opera) { + var self = this; + window.setTimeout(function() { + var liso = self._getDoc().getElementsByTagName('li'); + for (var i = 0; i < liso.length; i++) { + if (liso[i].innerHTML.toLowerCase() == '
          ') { + liso[i].parentNode.parentNode.removeChild(liso[i].parentNode); + } + } + },30); + } + if (this.browser.ie && exec) { + var html = ''; + if (this._getRange().html) { + html = '
        • ' + this._getRange().html+ '
        • '; + } else { + var t = this._getRange().text.split('\n'); + if (t.length > 1) { + html = ''; + for (var ie = 0; ie < t.length; ie++) { + html += '
        • ' + t[ie] + '
        • '; + } + } else { + var txt = this._getRange().text; + if (txt === '') { + html = '
        • ' + txt + '
        • '; + } else { + html = '
        • ' + txt + '
        • '; + } + } + } + this._getRange().pasteHTML('<' + tag + '>' + html + ''); + var new_item = this._getDoc().getElementById('new_list_item'); + if (new_item) { + var range = this._getDoc().body.createTextRange(); + range.moveToElementText(new_item); + range.collapse(false); + range.select(); + new_item.id = ''; + } + exec = false; + } + } + return exec; + }, + /** + * @method cmd_insertorderedlist + * @param value Value passed from the execCommand method + * @description This is an execCommand override method. It is called from execCommand when the execCommand('insertorderedlist ') is used. + */ + cmd_insertorderedlist: function(value) { + return [this.cmd_list('ol')]; + }, + /** + * @method cmd_insertunorderedlist + * @param value Value passed from the execCommand method + * @description This is an execCommand override method. It is called from execCommand when the execCommand('insertunorderedlist') is used. + */ + cmd_insertunorderedlist: function(value) { + return [this.cmd_list('ul')]; + }, + /** + * @method cmd_fontname + * @param value Value passed from the execCommand method + * @description This is an execCommand override method. It is called from execCommand when the execCommand('fontname') is used. + */ + cmd_fontname: function(value) { + var exec = true, + selEl = this._getSelectedElement(); + + this.currentFont = value; + if (selEl && selEl.tagName && !this._hasSelection() && !this._isElement(selEl, 'body') && !this.get('insert')) { + Dom.setStyle(selEl, 'fontFamily', value); + exec = false; + } else if (this.get('insert') && !this._hasSelection()) { + YAHOO.log('No selection and no selected element and we are in insert mode', 'info', 'SimpleEditor'); + var el = this._createInsertElement({ fontFamily: value }); + exec = false; + } + return [exec]; + }, + /** + * @method cmd_fontsize + * @param value Value passed from the execCommand method + * @description This is an execCommand override method. It is called from execCommand when the execCommand('fontsize') is used. + */ + cmd_fontsize: function(value) { + var el = null, go = true; + el = this._getSelectedElement(); + if (this.browser.webkit) { + if (this.currentElement[0]) { + if (el == this.currentElement[0]) { + go = false; + Dom.setStyle(el, 'fontSize', value); + this._selectNode(el); + this.currentElement[0] = el; + } + } + } + if (go) { + if (!this._isElement(this._getSelectedElement(), 'body') && (!this._hasSelection())) { + el = this._getSelectedElement(); + Dom.setStyle(el, 'fontSize', value); + if (this.get('insert') && this.browser.ie) { + var r = this._getRange(); + r.collapse(false); + r.select(); + } else { + this._selectNode(el); + } + } else if (this.currentElement && (this.currentElement.length > 0) && (!this._hasSelection()) && (!this.get('insert'))) { + Dom.setStyle(this.currentElement, 'fontSize', value); + } else { + if (this.get('insert') && !this._hasSelection()) { + el = this._createInsertElement({ fontSize: value }); + this.currentElement[0] = el; + this._selectNode(this.currentElement[0]); + } else { + this._createCurrentElement('span', {'fontSize': value, fontFamily: el.style.fontFamily, color: el.style.color, backgroundColor: el.style.backgroundColor }); + this._selectNode(this.currentElement[0]); + } + } + } + return [false]; + }, + /* }}} */ + /** + * @private + * @method _swapEl + * @param {HTMLElement} el The element to swap with + * @param {String} tagName The tagname of the element that you wish to create + * @param {Function} callback (optional) A function to run on the element after it is created, but before it is replaced. An element reference is passed to this function. + * @description This function will create a new element in the DOM and populate it with the contents of another element. Then it will assume it's place. + */ + _swapEl: function(el, tagName, callback) { + var _el = this._getDoc().createElement(tagName); + if (el) { + _el.innerHTML = el.innerHTML; + } + if (typeof callback == 'function') { + callback.call(this, _el); + } + if (el) { + el.parentNode.replaceChild(_el, el); + } + return _el; + }, + /** + * @private + * @method _createInsertElement + * @description Creates a new "currentElement" then adds some text (and other things) to make it selectable and stylable. Then the user can continue typing. + * @param {Object} css (optional) Object literal containing styles to apply to the new element. + * @return {HTMLElement} + */ + _createInsertElement: function(css) { + this._createCurrentElement('span', css); + var el = this.currentElement[0]; + if (this.browser.webkit) { + //Little Safari Hackery here.. + el.innerHTML = ' '; + el = el.firstChild; + this._getSelection().setBaseAndExtent(el, 1, el, el.innerText.length); + } else if (this.browser.ie || this.browser.opera) { + el.innerHTML = ' '; + } + this.focus(); + this._selectNode(el, true); + return el; + }, + /** + * @private + * @method _createCurrentElement + * @param {String} tagName (optional defaults to a) The tagname of the element that you wish to create + * @param {Object} tagStyle (optional) Object literal containing styles to apply to the new element. + * @description This is a work around for the various browser issues with execCommand. This method will run execCommand('fontname', false, 'yui-tmp') on the given selection. + * It will then search the document for an element with the font-family set to yui-tmp and replace that with another span that has other information in it, then assign the new span to the + * this.currentElement array, so we now have element references to the elements that were just modified. At this point we can use standard DOM manipulation to change them as we see fit. + */ + _createCurrentElement: function(tagName, tagStyle) { + tagName = ((tagName) ? tagName : 'a'); + var tar = null, + el = [], + _doc = this._getDoc(); + + if (this.currentFont) { + if (!tagStyle) { + tagStyle = {}; + } + tagStyle.fontFamily = this.currentFont; + this.currentFont = null; + } + this.currentElement = []; + + var _elCreate = function(tagName, tagStyle) { + var el = null; + tagName = ((tagName) ? tagName : 'span'); + tagName = tagName.toLowerCase(); + switch (tagName) { + case 'h1': + case 'h2': + case 'h3': + case 'h4': + case 'h5': + case 'h6': + el = _doc.createElement(tagName); + break; + default: + el = _doc.createElement(tagName); + if (tagName === 'span') { + Dom.addClass(el, 'yui-tag-' + tagName); + Dom.addClass(el, 'yui-tag'); + el.setAttribute('tag', tagName); + } + //console.log(tagStyle); + if (tagStyle) { + Y.each(tagStyle, function(v, k) { + el.style[k] = v; + }); + } + break; + } + return el; + }; + + if (!this._hasSelection()) { + if (this._getDoc().queryCommandEnabled('insertimage')) { + this._getDoc().execCommand('insertimage', false, 'yui-tmp-img'); + var imgs = this._getDoc().getElementsByTagName('img'); + for (var j = 0; j < imgs.length; j++) { + if (imgs[j].getAttribute('src', 2) == 'yui-tmp-img') { + el = _elCreate(tagName, tagStyle); + imgs[j].parentNode.replaceChild(el, imgs[j]); + this.currentElement[this.currentElement.length] = el; + } + } + } else { + if (this.currentEvent) { + tar = Event.getTarget(this.currentEvent); + } else { + //For Safari.. + tar = this._getDoc().body; + } + } + if (tar) { + /* + * @knownissue Safari Cursor Position + * @browser Safari 2.x + * @description The issue here is that we have no way of knowing where the cursor position is + * inside of the iframe, so we have to place the newly inserted data in the best place that we can. + */ + el = _elCreate(tagName, tagStyle); + if (this._isElement(tar, 'body') || this._isElement(tar, 'html')) { + if (this._isElement(tar, 'html')) { + tar = this._getDoc().body; + } + tar.appendChild(el); + } else if (tar.nextSibling) { + tar.parentNode.insertBefore(el, tar.nextSibling); + } else { + tar.parentNode.appendChild(el); + } + //this.currentElement = el; + this.currentElement[this.currentElement.length] = el; + this.currentEvent = null; + if (this.browser.webkit) { + //Force Safari to focus the new element + this._getSelection().setBaseAndExtent(el, 0, el, 0); + if (this.browser.webkit3) { + this._getSelection().collapseToStart(); + } else { + this._getSelection().collapse(true); + } + } + } + } else { + //Force CSS Styling for this action... + this._setEditorStyle(true); + this._getDoc().execCommand('fontname', false, 'yui-tmp'); + var _tmp = [], __tmp, __els = ['font', 'span', 'i', 'b', 'u', 'li']; + + if (!this._isElement(this._getSelectedElement(), 'body')) { + __els[__els.length] = this._getDoc().getElementsByTagName(this._getSelectedElement().tagName); + __els[__els.length] = this._getDoc().getElementsByTagName(this._getSelectedElement().parentNode.tagName); + } + for (var _els = 0; _els < __els.length; _els++) { + var _tmp1 = this._getDoc().getElementsByTagName(__els[_els]); + for (var e = 0; e < _tmp1.length; e++) { + _tmp[_tmp.length] = _tmp1[e]; + } + } + + + for (var i = 0; i < _tmp.length; i++) { + if ((Dom.getStyle(_tmp[i], 'fontFamily') == 'yui-tmp') || (_tmp[i].face && (_tmp[i].face == 'yui-tmp'))) { + if (tagName !== 'span') { + el = _elCreate(tagName, tagStyle); + } else { + el = _elCreate(_tmp[i].tagName, tagStyle); + } + el.innerHTML = _tmp[i].innerHTML; + if (this._isElement(_tmp[i], 'ol') || (this._isElement(_tmp[i], 'ul'))) { + var fc = _tmp[i].getElementsByTagName('li')[0]; + _tmp[i].style.fontFamily = 'inherit'; + fc.style.fontFamily = 'inherit'; + el.innerHTML = fc.innerHTML; + fc.innerHTML = ''; + fc.appendChild(el); + this.currentElement[this.currentElement.length] = el; + } else if (this._isElement(_tmp[i], 'li')) { + _tmp[i].innerHTML = ''; + _tmp[i].appendChild(el); + _tmp[i].style.fontFamily = 'inherit'; + this.currentElement[this.currentElement.length] = el; + } else { + if (_tmp[i].parentNode) { + _tmp[i].parentNode.replaceChild(el, _tmp[i]); + this.currentElement[this.currentElement.length] = el; + this.currentEvent = null; + if (this.browser.webkit) { + //Force Safari to focus the new element + this._getSelection().setBaseAndExtent(el, 0, el, 0); + if (this.browser.webkit3) { + this._getSelection().collapseToStart(); + } else { + this._getSelection().collapse(true); + } + } + if (this.browser.ie && tagStyle && tagStyle.fontSize) { + this._getSelection().empty(); + } + if (this.browser.gecko) { + this._getSelection().collapseToStart(); + } + } + } + } + } + var len = this.currentElement.length; + for (var o = 0; o < len; o++) { + if ((o + 1) != len) { //Skip the last one in the list + if (this.currentElement[o] && this.currentElement[o].nextSibling) { + if (this._isElement(this.currentElement[o], 'br')) { + this.currentElement[this.currentElement.length] = this.currentElement[o].nextSibling; + } + } + } + } + } + }, + /** + * @method saveHTML + * @description Cleans the HTML with the cleanHTML method then places that string back into the textarea. + * @return String + */ + saveHTML: function() { + var html = this.cleanHTML(); + if (this._textarea) { + this.get('node').set('value', html); + } else { + this.get('node').set('innerHTML', html); + } + if (this.get('saveEl') !== this.get('node')) { + var out = this.get('saveEl'); + if (out) { + if (out.get('tagName').toLowerCase() === 'textarea') { + out.set('value', html); + } else { + out.set('innerHTML', html); + } + } + } + return html; + }, + /** + * @method setEditorHTML + * @param {String} incomingHTML The html content to load into the editor + * @description Loads HTML into the editors body + */ + setEditorHTML: function(incomingHTML) { + var html = this._cleanIncomingHTML(incomingHTML); + html = html.replace(/RIGHT_BRACKET/gi, '{'); + html = html.replace(/LEFT_BRACKET/gi, '}'); + this._getDoc().body.innerHTML = html; + this.nodeChange(); + }, + /** + * @method getEditorHTML + * @description Gets the unprocessed/unfiltered HTML from the editor + */ + getEditorHTML: function() { + try { + var b = this._getDoc().body; + if (b === null) { + YAHOO.log('Body is null, returning null.', 'error', 'SimpleEditor'); + return null; + } + return this._getDoc().body.innerHTML; + } catch (e) { + return ''; + } + }, + /** + * @method show + * @description This method needs to be called if the Editor was hidden (like in a TabView or Panel). It is used to reset the editor after being in a container that was set to display none. + */ + show: function() { + if (this.browser.gecko) { + this._setDesignMode('on'); + this.focus(); + } + if (this.browser.webkit) { + var self = this; + window.setTimeout(function() { + self._setInitialContent.call(self); + }, 10); + } + //Adding this will close all other Editor window's when showing this one. + if (this.currentWindow) { + this.closeWindow(); + } + //Put the iframe back in place + this.get('iframe').setStyle('position', 'static'); + this.get('iframe').setStyle('left', ''); + }, + /** + * @method hide + * @description This method needs to be called if the Editor is to be hidden (like in a TabView or Panel). It should be called to clear timeouts and close open editor windows. + */ + hide: function() { + //Adding this will close all other Editor window's. + if (this.currentWindow) { + this.closeWindow(); + } + if (this._fixNodesTimer) { + clearTimeout(this._fixNodesTimer); + this._fixNodesTimer = null; + } + if (this._nodeChangeTimer) { + clearTimeout(this._nodeChangeTimer); + this._nodeChangeTimer = null; + } + this._lastNodeChange = 0; + //Move the iframe off of the screen, so that in containers with visiblity hidden, IE will not cover other elements. + this.get('iframe').setStyle('position', 'absolute'); + this.get('iframe').setStyle('left', '-9999px'); + }, + /** + * @method _cleanIncomingHTML + * @param {String} html The unfiltered HTML + * @description Process the HTML with a few regexes to clean it up and stabilize the input + * @return {String} The filtered HTML + */ + _cleanIncomingHTML: function(html) { + html = html.replace(/{/gi, 'RIGHT_BRACKET'); + html = html.replace(/}/gi, 'LEFT_BRACKET'); + + html = html.replace(/]*)>/gi, ''); + html = html.replace(/<\/strong>/gi, ''); + + //replace embed before em check + html = html.replace(/]*)>/gi, ''); + html = html.replace(/<\/embed>/gi, ''); + + html = html.replace(/]*)>/gi, ''); + html = html.replace(/<\/em>/gi, ''); + html = html.replace(/_moz_dirty=""/gi, ''); + + //Put embed tags back in.. + html = html.replace(/]*)>/gi, ''); + html = html.replace(/<\/YUI_EMBED>/gi, ''); + if (this.get('plainText')) { + YAHOO.log('Filtering as plain text', 'info', 'SimpleEditor'); + html = html.replace(/\n/g, '
          ').replace(/\r/g, '
          '); + html = html.replace(/ /gi, '  '); //Replace all double spaces + html = html.replace(/\t/gi, '    '); //Replace all tabs + } + //Removing Script Tags from the Editor + html = html.replace(/]*)>/gi, ''); + html = html.replace(/<\/script([^>]*)>/gi, ''); + html = html.replace(/<script([^>]*)>/gi, ''); + html = html.replace(/<\/script([^>]*)>/gi, ''); + //Replace the line feeds + html = html.replace(/\r\n/g, '').replace(/\n/g, '').replace(/\r/g, ''); + + //Remove Bad HTML elements (used to be script nodes) + html = html.replace(new RegExp(']*)>(.*?)<\/bad>', 'gi'), ''); + //Replace the lines feeds + html = html.replace(//g, '\n'); + return html; + }, + /** + * @method cleanHTML + * @param {String} html The unfiltered HTML + * @description Process the HTML with a few regexes to clean it up and stabilize the output + * @return {String} The filtered HTML + */ + cleanHTML: function(html) { + //Start Filtering Output + //Begin RegExs.. + if (!html) { + html = this.getEditorHTML(); + } + var markup = this.get('markup'); + //Make some backups... + html = this.pre_filter_linebreaks(html, markup); + + //Filter MS Word + html = this.filter_msword(html); + + html = html.replace(/]*)\/>/gi, ''); + html = html.replace(/]*)>/gi, ''); + + html = html.replace(/]*)\/>/gi, ''); + html = html.replace(/]*)>/gi, ''); + + html = html.replace(/]*)>/gi, ''); + html = html.replace(/<\/ul>/gi, '<\/YUI_UL>'); + html = html.replace(/]*)>/gi, ''); + html = html.replace(/<\/blockquote>/gi, '<\/YUI_BQ>'); + + html = html.replace(/]*)>/gi, ''); + html = html.replace(/<\/embed>/gi, '<\/YUI_EMBED>'); + + //Convert b and i tags to strong and em tags + if ((markup == 'semantic') || (markup == 'xhtml')) { + html = html.replace(/]*)?>/gi, ''); + html = html.replace(/<\/i>/gi, ''); + html = html.replace(/]*)?>/gi, ''); + html = html.replace(/<\/b>/gi, ''); + } + + html = html.replace(/_moz_dirty=""/gi, ''); + + //normalize strikethrough + html = html.replace(//gi, '/span>'); + + + //Case Changing + if (this.browser.ie) { + html = html.replace(/text-decoration/gi, 'text-decoration'); + html = html.replace(/font-weight/gi, 'font-weight'); + html = html.replace(/_width="([^>]*)"/gi, ''); + html = html.replace(/_height="([^>]*)"/gi, ''); + //Cleanup Image URL's + var url = this._baseHREF.replace(/\//gi, '\\/'), + re = new RegExp('src="' + url, 'gi'); + html = html.replace(re, 'src="'); + } + html = html.replace(//gi, ''); + html = html.replace(//gi, ''); + if ((markup == 'semantic') || (markup == 'xhtml') || (markup == 'css')) { + html = html.replace(new RegExp(']*)face="([^>]*)">(.*?)<\/font>', 'gi'), '$3'); + html = html.replace(/([^>]*)<\/span>', 'gi'), '$1'); + html = html.replace(new RegExp('([^>]*)<\/span>', 'gi'), '$1'); + } + html = html.replace(/\/u>/gi, '/span>'); + if (markup == 'css') { + html = html.replace(/]*)>/gi, ''); + html = html.replace(/<\/em>/gi, ''); + html = html.replace(/]*)>/gi, ''); + html = html.replace(/<\/strong>/gi, ''); + html = html.replace(//gi, '/span>'); + html = html.replace(//gi, '/span>'); + } + html = html.replace(/ /gi, ' '); //Replace all double spaces and replace with a single + } else { + html = html.replace(//gi, '/u>'); + } + html = html.replace(/]*)>/gi, ''); + html = html.replace(/\/ol>/gi, '/ol>'); + html = html.replace(/
        • /gi, '/li>'); + html = this.filter_safari(html); + + html = this.filter_internals(html); + + html = this.filter_all_rgb(html); + + //Replace our backups with the real thing + html = this.post_filter_linebreaks(html, markup); + + if (markup == 'xhtml') { + html = html.replace(/]*)>/g, ''); + html = html.replace(/]*)>/g, ''); + } else { + html = html.replace(/]*)>/g, ''); + html = html.replace(/]*)>/g, ''); + } + html = html.replace(/]*)>/g, ''); + html = html.replace(/<\/YUI_UL>/g, '<\/ul>'); + + html = this.filter_invalid_lists(html); + + html = html.replace(/]*)>/g, ''); + html = html.replace(/<\/YUI_BQ>/g, '<\/blockquote>'); + + html = html.replace(/]*)>/g, ''); + html = html.replace(/<\/YUI_EMBED>/g, '<\/embed>'); + + //This should fix &'s in URL's + html = html.replace(/ & /gi, ' YUI_AMP '); + html = html.replace(/ &/gi, ' YUI_AMP_F '); + html = html.replace(/& /gi, ' YUI_AMP_R '); + html = html.replace(/&/gi, '&'); + html = html.replace(/ YUI_AMP /gi, ' & '); + html = html.replace(/ YUI_AMP_F /gi, ' &'); + html = html.replace(/ YUI_AMP_R /gi, '& '); + + //Trim the output, removing whitespace from the beginning and end + html = YAHOO.lang.trim(html); + + if (this.get('removeLineBreaks')) { + html = html.replace(/\n/g, '').replace(/\r/g, ''); + html = html.replace(/ /gi, ' '); //Replace all double spaces and replace with a single + } + + for (var v in this.invalidHTML) { + if (YAHOO.lang.hasOwnProperty(this.invalidHTML, v)) { + if (Lang.isObject(v) && v.keepContents) { + html = html.replace(new RegExp('<' + v + '([^>]*)>(.*?)<\/' + v + '>', 'gi'), '$1'); + } else { + html = html.replace(new RegExp('<' + v + '([^>]*)>(.*?)<\/' + v + '>', 'gi'), ''); + } + } + } + + /* LATER -- Add DOM manipulation + console.log(html); + var frag = document.createDocumentFragment(); + frag.innerHTML = html; + + var ps = frag.getElementsByTagName('p'), + len = ps.length; + for (var i = 0; i < len; i++) { + var ps2 = ps[i].getElementsByTagName('p'); + if (ps2.length) { + + } + + } + html = frag.innerHTML; + console.log(html); + */ + + this.fireEvent('cleanHTML', { type: 'cleanHTML', target: this, html: html }); + + return html; + }, + /** + * @method filter_msword + * @param String html The HTML string to filter + * @description Filters out msword html attributes and other junk. Activate with filterWord: true in config + */ + filter_msword: function(html) { + if (!this.get('filterWord')) { + return html; + } + //Remove the ms o: tags + html = html.replace(/\s*<\/o:p>/g, ''); + html = html.replace(/[\s\S]*?<\/o:p>/g, ' '); + + //Remove the ms w: tags + html = html.replace( /]*>[\s\S]*?<\/w:[^>]*>/gi, ''); + + //Remove mso-? styles. + html = html.replace( /\s*mso-[^:]+:[^;"]+;?/gi, ''); + + //Remove more bogus MS styles. + html = html.replace( /\s*MARGIN: 0cm 0cm 0pt\s*;/gi, ''); + html = html.replace( /\s*MARGIN: 0cm 0cm 0pt\s*"/gi, "\""); + html = html.replace( /\s*TEXT-INDENT: 0cm\s*;/gi, ''); + html = html.replace( /\s*TEXT-INDENT: 0cm\s*"/gi, "\""); + html = html.replace( /\s*PAGE-BREAK-BEFORE: [^\s;]+;?"/gi, "\""); + html = html.replace( /\s*FONT-VARIANT: [^\s;]+;?"/gi, "\"" ); + html = html.replace( /\s*tab-stops:[^;"]*;?/gi, ''); + html = html.replace( /\s*tab-stops:[^"]*/gi, ''); + + //Remove XML declarations + html = html.replace(/<\\?\?xml[^>]*>/gi, ''); + + //Remove lang + html = html.replace(/<(\w[^>]*) lang=([^ |>]*)([^>]*)/gi, "<$1$3"); + + //Remove language tags + html = html.replace( /<(\w[^>]*) language=([^ |>]*)([^>]*)/gi, "<$1$3"); + + //Remove onmouseover and onmouseout events (from MS Word comments effect) + html = html.replace( /<(\w[^>]*) onmouseover="([^\"]*)"([^>]*)/gi, "<$1$3"); + html = html.replace( /<(\w[^>]*) onmouseout="([^\"]*)"([^>]*)/gi, "<$1$3"); + + return html; + }, + /** + * @method filter_invalid_lists + * @param String html The HTML string to filter + * @description Filters invalid ol and ul list markup, converts this:
          1. ..
          to this:
          1. ..
        • + */ + filter_invalid_lists: function(html) { + html = html.replace(/<\/li>\n/gi, ''); + + html = html.replace(/<\/li>
            /gi, '
            1. '); + html = html.replace(/<\/ol>/gi, '
          1. '); + html = html.replace(/<\/ol><\/li>\n/gi, "
          "); + + html = html.replace(/<\/li>
            /gi, '
            • '); + html = html.replace(/<\/ul>/gi, '
          • '); + html = html.replace(/<\/ul><\/li>\n?/gi, "
          "); + + html = html.replace(/<\/li>/gi, ""); + html = html.replace(/<\/ol>/gi, ""); + html = html.replace(/
            /gi, "
              "); + html = html.replace(/
                /gi, "
                  "); + return html; + }, + /** + * @method filter_safari + * @param String html The HTML string to filter + * @description Filters strings specific to Safari + * @return String + */ + filter_safari: function(html) { + if (this.browser.webkit) { + // + html = html.replace(/([^>])<\/span>/gi, '    '); + html = html.replace(/Apple-style-span/gi, ''); + html = html.replace(/style="line-height: normal;"/gi, ''); + html = html.replace(/yui-wk-div/gi, ''); + html = html.replace(/yui-wk-p/gi, ''); + + + //Remove bogus LI's + html = html.replace(/
                • <\/li>/gi, ''); + html = html.replace(/
                • <\/li>/gi, ''); + html = html.replace(/
                • <\/li>/gi, ''); + //Remove bogus DIV's - updated from just removing the div's to replacing /div with a break + if (this.get('ptags')) { + html = html.replace(/]*)>/g, ''); + html = html.replace(/<\/div>/gi, '

                  '); + } else { + //html = html.replace(/
                  /gi, '
                  '); + html = html.replace(/]*)>([ tnr]*)<\/div>/gi, '
                  '); + html = html.replace(/<\/div>/gi, ''); + } + } + return html; + }, + /** + * @method filter_internals + * @param String html The HTML string to filter + * @description Filters internal RTE strings and bogus attrs we don't want + * @return String + */ + filter_internals: function(html) { + html = html.replace(/\r/g, ''); + //Fix stuff we don't want + html = html.replace(/<\/?(body|head|html)[^>]*>/gi, ''); + //Fix last BR in LI + html = html.replace(/<\/li>/gi, '
                • '); + + html = html.replace(/yui-tag-span/gi, ''); + html = html.replace(/yui-tag/gi, ''); + html = html.replace(/yui-non/gi, ''); + html = html.replace(/yui-img/gi, ''); + html = html.replace(/ tag="span"/gi, ''); + html = html.replace(/ class=""/gi, ''); + html = html.replace(/ style=""/gi, ''); + html = html.replace(/ class=" "/gi, ''); + html = html.replace(/ class=" "/gi, ''); + html = html.replace(/ target=""/gi, ''); + html = html.replace(/ title=""/gi, ''); + + if (this.browser.ie) { + html = html.replace(/ class= /gi, ''); + html = html.replace(/ class= >/gi, ''); + } + + return html; + }, + /** + * @method filter_all_rgb + * @param String str The HTML string to filter + * @description Converts all RGB color strings found in passed string to a hex color, example: style="color: rgb(0, 255, 0)" converts to style="color: #00ff00" + * @return String + */ + filter_all_rgb: function(str) { + var exp = new RegExp("rgb\\s*?\\(\\s*?([0-9]+).*?,\\s*?([0-9]+).*?,\\s*?([0-9]+).*?\\)", "gi"); + var arr = str.match(exp); + if (Lang.isArray(arr)) { + for (var i = 0; i < arr.length; i++) { + var color = this.filter_rgb(arr[i]); + str = str.replace(arr[i].toString(), color); + } + } + + return str; + }, + /** + * @method filter_rgb + * @param String css The CSS string containing rgb(#,#,#); + * @description Converts an RGB color string to a hex color, example: rgb(0, 255, 0) converts to #00ff00 + * @return String + */ + filter_rgb: function(css) { + if (css.toLowerCase().indexOf('rgb') != -1) { + var exp = new RegExp("(.*?)rgb\\s*?\\(\\s*?([0-9]+).*?,\\s*?([0-9]+).*?,\\s*?([0-9]+).*?\\)(.*?)", "gi"); + var rgb = css.replace(exp, "$1,$2,$3,$4,$5").split(','); + + if (rgb.length == 5) { + var r = parseInt(rgb[1], 10).toString(16); + var g = parseInt(rgb[2], 10).toString(16); + var b = parseInt(rgb[3], 10).toString(16); + + r = r.length == 1 ? '0' + r : r; + g = g.length == 1 ? '0' + g : g; + b = b.length == 1 ? '0' + b : b; + + css = "#" + r + g + b; + } + } + return css; + }, + /** + * @method pre_filter_linebreaks + * @param String html The HTML to filter + * @param String markup The markup type to filter to + * @description HTML Pre Filter + * @return String + */ + pre_filter_linebreaks: function(html, markup) { + if (this.browser.webkit) { + html = html.replace(/
                  /gi, ''); + html = html.replace(/
                  /gi, ''); + } + html = html.replace(/
                  /gi, ''); + html = html.replace(/
                  /gi, ''); + html = html.replace(//gi, ''); + html = html.replace(/
                  /gi, ''); + html = html.replace(/
                  <\/div>/gi, ''); + html = html.replace(/

                  ( | )<\/p>/g, ''); + html = html.replace(/


                   <\/p>/gi, ''); + html = html.replace(/

                   <\/p>/gi, ''); + //Fix last BR + html = html.replace(/$/, ''); + //Fix last BR in P + html = html.replace(/<\/p>/g, '

                  '); + if (this.browser.ie) { + html = html.replace(/    /g, '\t'); + } + return html; + }, + /** + * @method post_filter_linebreaks + * @param String html The HTML to filter + * @param String markup The markup type to filter to + * @description HTML Pre Filter + * @return String + */ + post_filter_linebreaks: function(html, markup) { + if (markup == 'xhtml') { + html = html.replace(//g, '
                  '); + } else { + html = html.replace(//g, '
                  '); + } + return html; + }, + /** + * @method clearEditorDoc + * @description Clear the doc of the Editor + */ + clearEditorDoc: function() { + this._getDoc().body.innerHTML = ' '; + }, + /** + * @method openWindow + * @description Override Method for Advanced Editor + */ + openWindow: function(win) { + }, + /** + * @method moveWindow + * @description Override Method for Advanced Editor + */ + moveWindow: function() { + }, + /** + * @private + * @method _closeWindow + * @description Override Method for Advanced Editor + */ + _closeWindow: function() { + }, + /** + * @method closeWindow + * @description Override Method for Advanced Editor + */ + closeWindow: function() { + //this.unsubscribeAll('afterExecCommand'); + this.toolbar.resetAllButtons(); + this.focus(); + }, + /** + * @method destroy + * @description Destroys the editor, all of it's elements and objects. + * @return {Boolean} + */ + destructor: function() { + if (this._nodeChangeDelayTimer) { + clearTimeout(this._nodeChangeDelayTimer); + } + this.hide(); + + YAHOO.log('Destroying Editor', 'warn', 'SimpleEditor'); + if (this.resize) { + YAHOO.log('Destroying Resize', 'warn', 'SimpleEditor'); + this.resize.destroy(); + } + if (this.dd) { + YAHOO.log('Unreg DragDrop Instance', 'warn', 'SimpleEditor'); + this.dd.unreg(); + } + if (this.get('panel')) { + YAHOO.log('Destroying Editor Panel', 'warn', 'SimpleEditor'); + this.get('panel').destroy(); + } + this.saveHTML(); + this.toolbar.destroy(); + YAHOO.log('Restoring TextArea', 'info', 'SimpleEditor'); + this.setStyle('visibility', 'visible'); + this.setStyle('position', 'static'); + this.setStyle('top', ''); + this.setStyle('left', ''); + var textArea = this.get('element'); + this.get('element_cont').get('parentNode').replaceChild(textArea, this.get('element_cont').get('element')); + this.get('element_cont').get('element').innerHTML = ''; + this.set('handleSubmit', false); //Remove the submit handler + return true; + }, + /** + * @method toString + * @description Returns a string representing the editor. + * @return {String} + */ + toString: function() { + var str = 'SimpleEditor'; + return str; + } + }); + + Y.namespace('Editor'); + Y.Editor.Simple = SimpleEditor; + +/** +* @event toolbarLoaded +* @description Event is fired during the render process directly after the Toolbar is loaded. Allowing you to attach events to the toolbar. See Element.addListener for more information on listening for this event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event cleanHTML +* @description Event is fired after the cleanHTML method is called. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event afterRender +* @description Event is fired after the render process finishes. See Element.addListener for more information on listening for this event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event editorContentLoaded +* @description Event is fired after the editor iframe's document fully loads and fires it's onload event. From here you can start injecting your own things into the document. See Element.addListener for more information on listening for this event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event beforeNodeChange +* @description Event fires at the beginning of the nodeChange process. See Element.addListener for more information on listening for this event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event afterNodeChange +* @description Event fires at the end of the nodeChange process. See Element.addListener for more information on listening for this event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event beforeExecCommand +* @description Event fires at the beginning of the execCommand process. See Element.addListener for more information on listening for this event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event afterExecCommand +* @description Event fires at the end of the execCommand process. See Element.addListener for more information on listening for this event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event editorMouseUp +* @param {Event} ev The DOM Event that occured +* @description Passed through HTML Event. See Element.addListener for more information on listening for this event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event editorMouseDown +* @param {Event} ev The DOM Event that occured +* @description Passed through HTML Event. See Element.addListener for more information on listening for this event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event editorDoubleClick +* @param {Event} ev The DOM Event that occured +* @description Passed through HTML Event. See Element.addListener for more information on listening for this event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event editorClick +* @param {Event} ev The DOM Event that occured +* @description Passed through HTML Event. See Element.addListener for more information on listening for this event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event editorKeyUp +* @param {Event} ev The DOM Event that occured +* @description Passed through HTML Event. See Element.addListener for more information on listening for this event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event editorKeyPress +* @param {Event} ev The DOM Event that occured +* @description Passed through HTML Event. See Element.addListener for more information on listening for this event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event editorKeyDown +* @param {Event} ev The DOM Event that occured +* @description Passed through HTML Event. See Element.addListener for more information on listening for this event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event beforeEditorMouseUp +* @param {Event} ev The DOM Event that occured +* @description Fires before editor event, returning false will stop the internal processing. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event beforeEditorMouseDown +* @param {Event} ev The DOM Event that occured +* @description Fires before editor event, returning false will stop the internal processing. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event beforeEditorDoubleClick +* @param {Event} ev The DOM Event that occured +* @description Fires before editor event, returning false will stop the internal processing. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event beforeEditorClick +* @param {Event} ev The DOM Event that occured +* @description Fires before editor event, returning false will stop the internal processing. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event beforeEditorKeyUp +* @param {Event} ev The DOM Event that occured +* @description Fires before editor event, returning false will stop the internal processing. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event beforeEditorKeyPress +* @param {Event} ev The DOM Event that occured +* @description Fires before editor event, returning false will stop the internal processing. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event beforeEditorKeyDown +* @param {Event} ev The DOM Event that occured +* @description Fires before editor event, returning false will stop the internal processing. +* @type YAHOO.util.CustomEvent +*/ + +/** +* @event editorWindowFocus +* @description Fires when the iframe is focused. Note, this is window focus event, not an Editor focus event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event editorWindowBlur +* @description Fires when the iframe is blurred. Note, this is window blur event, not an Editor blur event. +* @type YAHOO.util.CustomEvent +*/ + + +/** + * @description Singleton object used to track the open window objects and panels across the various open editors + * @class Editor.Info + * @static +*/ +Y.Editor.Info = { + /** + * @private + * @property _instances + * @description A reference to all editors on the page. + * @type Object + */ + _instances: {}, + /** + * @private + * @property blankImage + * @description A reference to the blankImage url + * @type String + */ + blankImage: '', + /** + * @private + * @property window + * @description A reference to the currently open window object in any editor on the page. + * @type Object YAHOO.widget.EditorWindow + */ + window: {}, + /** + * @private + * @property panel + * @description A reference to the currently open panel in any editor on the page. + * @type Object YAHOO.widget.Overlay + */ + panel: null, + /** + * @method getEditorById + * @description Returns a reference to the Editor object associated with the given textarea + * @param {String/HTMLElement} id The id or reference of the textarea to return the Editor instance of + * @return Object YAHOO.widget.Editor + */ + getEditorById: function(id) { + if (!Lang.isString(id)) { + //Not a string, assume a node Reference + id = id.id; + } + if (this._instances[id]) { + return this._instances[id]; + } + return false; + }, + /** + * @method saveAll + * @description Saves all Editor instances on the page. If a form reference is passed, only Editor's bound to this form will be saved. + * @param {HTMLElement} form The form to check if this Editor instance belongs to + */ + saveAll: function(form) { + var i, e, items = Y.Editor.Info._instances; + if (form) { + Y.each(items, function(v, e) { + if (e.get('element').form && (e.get('element').form == form)) { + e.saveHTML(); + } + }); + } else { + Y.each(items, function(v) { + v.saveHTML(); + }); + } + }, + /** + * @method toString + * @description Returns a string representing the Editor.Info. + * @return {String} + */ + toString: function() { + var len = 0; + Y.each(this._instances, function() { + len++; + }); + return 'Editor Info (' + len + ' registered intance' + ((len > 1) ? 's' : '') + ')'; + } +}; + + +}, 'gallery-2009.11.02-20' ,{requires:['node','gallery-port','substitute']}); diff --git a/build/gallery-simple-editor/gallery-simple-editor-min.js b/build/gallery-simple-editor/gallery-simple-editor-min.js new file mode 100644 index 0000000000..5d94351310 --- /dev/null +++ b/build/gallery-simple-editor/gallery-simple-editor-min.js @@ -0,0 +1,17 @@ +YUI.add("gallery-simple-editor",function(C){var D=function(){D.superclass.constructor.apply(this,arguments);};D.NAME="ToolbarButton";D.ATTRS={node:{setter:function(J){var K=C.get(J);if(!K){C.error("Invalid Node Given: "+J);}else{K=K.item(0);}return K;}},id:{},value:{},menu:{value:false},type:{value:"push",writeOnce:true},disabled:{value:false,setter:function(J){if(J){this.get("node").addClass("yui-button-disabled");this.get("node").addClass("yui-"+this.get("type")+"-button-disabled");}else{this.get("node").removeClass("yui-button-disabled");this.get("node").removeClass("yui-"+this.get("type")+"-button-disabled");}if(this.get("type")=="select"){this.get("node").set("disabled",J);}}},label:{value:"LABEL",setter:function(J){if(this.get("type")=="push"){this.get("node").query("a").set("innerHTML",J);}}},title:{value:"TITLE",setter:function(J){this.get("node").set("title",J);}},container:{value:null,writeOnce:true,setter:function(J){var K=C.get(J);if(!K){C.error("Invalid Node Given: "+J);}else{K=K.item(0);}return K;}}};C.extend(D,C.Base,{buttonType:"normal",_handleMouseOver:function(){if(!this.get("disabled")){this.get("node").addClass("yui-button-hover");this.get("node").addClass("yui-"+this.get("type")+"-button-hover");}},_handleMouseOut:function(){this.get("node").removeClass("yui-button-hover");this.get("node").removeClass("yui-"+this.get("type")+"-button-hover");},checkValue:function(K){if(this.get("type")=="select"){var J=this.get("node").get("options");J.each(function(M,L){if(M.get("value")==K){this.get("node").set("selectedIndex",L);}},this);}},initializer:function(){var O,L,M,N,K,J;switch(this.get("type")){case"select":case"menu":M=C.Node.create("");N=this.get("menu");for(L=0;L'+N[L].text+"");M.appendChild(K);if(N[L].checked){K.set("selected",true);}}M.on("change",C.bind(this._handleSelect,this));O=C.stamp(M);M.set("id",O);this.set("node",M);break;default:J='';this.set("node",C.Node.create(J));this.get("node").on("mouseover",C.bind(this._handleMouseOver,this));this.get("node").on("mouseout",C.bind(this._handleMouseOut,this));break;}O=C.stamp(this.get("node"));this.set("id",O);this.get("node").set("id",O);this.set("title",this.get("title"));this.set("label",this.get("label"));this.set("disabled",this.get("disabled"));this.get("container").appendChild(this.get("node"));this.get("node").on("click",C.bind(function(P){P.halt();},this));},_handleSelect:function(J){this.fire("change",{type:"change",value:J.target.get("value"),target:this.get("node")});},getMenu:function(){return this.get("menu");},destructor:function(){},toString:function(){return"Toolbar.Button ("+this.get("node").get("id")+")";}});C.namespace("Toolbar");C.Toolbar.Button=D;var B=function(K){var J=K;if(C.Lang.isString(K)){J=this.getButtonById(K);}if(C.Lang.isNumber(K)){J=this.getButtonByIndex(K);}if(K instanceof C.Toolbar.Button){J=K;}else{J=this.getButtonByValue(K);}return J;};var G=function(){this._configuredButtons=[];G.superclass.constructor.apply(this,arguments);};G.NAME="Toolbar";G.ATTRS={node:{setter:function(J){var K=C.get(J);if(!K){C.error("Invalid Node Given: "+J);}else{K=K.item(0);}return K;}},buttonType:{value:"basic",writeOnce:true,validator:function(J){switch(J){case"advanced":case"basic":return J;}return"basic";},setter:function(J){this.buttonType=C.Toolbar.Button;}},buttons:{value:[]},cont:{value:null},id:{value:null},grouplabels:{value:true,setter:function(J){if(J){this.get("cont").removeClass(this.CLASS_PREFIX+"-nogrouplabels");}else{this.get("cont").addClass(this.CLASS_PREFIX+"-nogrouplabels");}}},titlebar:{value:false},collapse:{value:false}};C.extend(G,C.Base,{fireEvent:function(K,J){if(J.target){delete J.target;}this.fire(K,J);},_handleCollapse:function(){var L=this.get("collapse");if(this._titlebar){var K=null;var J=this._titlebar.query("span.collapse");if(L){if(J){return true;}K=C.Node.create('X');this._titlebar.appendChild(K);K.on("click",C.bind(function(){if(this.get("cont").get("parentNode").hasClass("yui-toolbar-container-collapsed")){this.collapse(false);}else{this.collapse();}},this));}else{K=this._titlebar.query("span.collapse");if(K){if(this.get("cont").get("parentNode").hasClass("yui-toolbar-container-collapsed")){this.collapse(false);}K.get("parentNode").removeChild(K);}}}},_renderTitle:function(){var K=this.get("titlebar");if(K){if(this._titlebar&&this._titlebar.get("parentNode")){this._titlebar.get("parentNode").removeChild(this._titlebar);}this._titlebar=C.Node.create('
                  ');this._titlebar.on("focus",C.bind(this._handleFocus,this));if(C.Lang.isString(K)){var J=C.Node.create('

                  '+K+"

                  ");this._titlebar.appendChild(J);J.get("firstChild").on("click",function(L){L.halt();});}if(this.get("node").get("firstChild")){this.get("node").insertBefore(this._titlebar,this.get("node").get("firstChild"));}else{this.get("node").appendChild(this._titlebar);}if(this.get("collapse")){this.set("collapse",true);}}else{if(this._titlebar){if(this._titlebar&&this._titlebar.get("parentNode")){this._titlebar.get("parentNode").removeChild(this._titlebar);}}}},_configuredButtons:null,_addMenuClasses:function(M,J,N){F.addClass(this.element,"yui-toolbar-"+N.get("value")+"-menu");if(F.hasClass(N._button.parentNode.parentNode,"yui-toolbar-select")){F.addClass(this.element,"yui-toolbar-select-menu");}var K=this.getItems();for(var L=0;LToolbar
                  ');this.set("id",this.get("node").get("id"));this.get("node").appendChild(J);this.set("cont",J.query("div.yui-toolbar-subcont"));this.get("node").set("tabIndex","-1");this.get("node").addClass(this.CLASS_CONTAINER);this._renderTitle();this._handleCollapse();this.on("titlebarChange",C.bind(this._renderTitle,this));this.on("collapseChange",C.bind(this._handleCollapse,this));this.processButtons();},processButtons:function(){var L,M,K,J,N=this.get("buttons");C.each(N,function(P,O){if(P.type=="separator"){this.addSeparator();}else{if(P.group!==undefined){M=this.addButtonGroup(P);if(M){K=M.length;for(J=0;J
                  ');if(M.label){var J=C.Node.create("

                  "+M.label+"

                  ");O.appendChild(J);}if(!this.get("grouplabels")){this.get("cont").addClass(this.CLASS_PREFIX,"-nogrouplabels");}this.get("cont").appendChild(O);var L=C.Node.create("
                    ");O.appendChild(L);if(!this._buttonGroupList){this._buttonGroupList={};}this._buttonGroupList[M.group]=L;var N=[],K;C.each(M.buttons,function(Q){var P=C.Node.create('
                  • ');L.appendChild(P);if((Q.type!==undefined)&&Q.type=="separator"){this.addSeparator(P);}else{Q.container=P;K=this.addButton(Q);if(K){N[N.length]=K.id;}}},this);return N;},addButtonToGroup:function(L,M,N){var K=this._buttonGroupList[M],J=document.createElement("li");J.className=this.CLASS_PREFIX+"-groupitem";L.container=J;this.addButton(L,N);K.appendChild(J);},addButton:function(O,P){if(!this._buttonList){this._buttonList=[];}if(!O.container){O.container=this.get("cont");}var L={},N=false;C.each(O,function(R,Q){if(!this._toolbarConfigs[Q]){L[Q]=R;}},this);if(O.type=="select"){L.type="menu";}if(O.type=="spin"){L.type="push";}if(L.type=="color"){if(C.Overlay){L=this._makeColorButton(L);}else{N=true;}}if(L.menu){if((C.Overlay)&&(O.menu instanceof C.Overlay)){O.menu.on("visibleChange",function(){this._button=L;});}else{}}if(N){O=false;}else{var K=new this.buttonType(L);K.get("node").set("tabIndex","-1");K.get("node").set("role","button");if(!L.id||!O.id){O.id=K.get("id");L.id=K.get("id");}if(this.get("disabled")){K.set("disabled",true);}K.get("node").addClass(this.CLASS_PREFIX+"-"+K.get("value"));var M=C.Node.create('');K.get("node").insertBefore(M,K.get("node").get("firstChild"));if(O.type=="spin"){if(!C.Lang.isArray(O.range)){O.range=[10,100];}this._makeSpinButton(K,O);}K.get("node").set("title",K.get("label"));if(O.type!="spin"){if((C.Overlay)&&(L.menu instanceof C.Overlay)){var J=function(R){var Q=true;if(R.keyCode&&(R.keyCode==9)){Q=false;}if(Q){if(this._colorPicker){this._colorPicker._button=O.value;}K.getMenu().set("visible",!K.getMenu().get("visible"));}R.halt();};K.get("node").on("mousedown",C.bind(J,this));K.get("node").on("keydown",C.bind(J,this));}else{if((O.type!="menu")&&(O.type!="select")){K.get("node").on("keypress",C.bind(this._buttonClick,this));K.get("node").on("mousedown",C.bind(function(Q){Q.halt();this._buttonClick(Q,O);},this));K.get("node").on("click",function(Q){Q.halt();});}else{K.on("mousedown",function(Q){Q.halt();});K.on("click",function(Q){Q.halt();});K.on("change",function(Q){if(!O.menucmd){O.menucmd=O.value;}O.value=Q.value;this._buttonClick(Q,O);},this,true);}}}else{K.on("mousedown",function(Q){Q.halt();});K.on("click",function(Q){Q.halt();});}if(this.browser.ie){}if(this.browser.webkit){}this._buttonList[this._buttonList.length]=K;}return O;},addSeparator:function(J,M){var K=((J)?J:this.get("cont"));if(this._sepCount===null){this._sepCount=0;}if(!this._sep){this._sep=C.Node.create('|');}var L=this._sep.cloneNode(true);this._sepCount++;L.addClass(this.CLASS_SEPARATOR+"-"+this._sepCount);if(M){var N=null;if(M.get){N=M.get("element").nextSibling;}else{if(M.nextSibling){N=M.nextSibling;}else{N=M;}}if(N){if(N==M){N.parentNode.appendChild(L);}else{N.parentNode.insertBefore(L,N);}}}else{K.appendChild(L);}return L;},_createColorPicker:function(M){var L=C.get("#"+M+"_colors");if(L){L.get("parentNode").removeChild(L);}var J=C.Node.create('');C.get("body").appendChild(J);this._colorPicker=J;var K="";C.each(this._colorData,function(O,N){K+=''+N.replace("#","")+"";},this);K+="X";J.set("innerHTML",K);J.on("mouseover",C.bind(function(R){var P=this._colorPicker;var Q=P.query("em");var O=P.query("strong");var N=R.target;if(N.get("tagName").toLowerCase()=="a"){Q.setStyle("backgroundColor",N.getStyle("backgroundColor"));O.set("innerHTML",this._colorData["#"+N.get("innerHTML")]+"
                    "+N.get("innerHTML"));}},this));J.on("focus",function(N){N.halt();});J.on("click",function(N){N.halt();});J.on("mousedown",C.bind(function(O){O.halt();var N=O.target,P;if(N.get("tagName").toLowerCase()=="a"){P=N.get("innerHTML");var R=this.fire("colorPickerClicked",{type:"colorPickerClicked",button:this._colorPicker._button,color:P,colorName:this._colorData["#"+P]});var Q={color:P,colorName:this._colorData["#"+P],value:this._colorPicker._button};this.fire("buttonClick",{type:"buttonClick",button:Q});this.getButtonByValue(this._colorPicker._button).getMenu().hide();this._colorPicker.setStyle("display","none");}},this));},_resetColorPicker:function(){var K=this._colorPicker.query("em");var J=this._colorPicker.query("strong");K.setStyle("backgroundColor","transparent");J.set("innerHTML","");},_makeColorButton:function(J){if(!this._colorPicker){this._createColorPicker(this.get("id"));}J.type="color";J.menu=new C.Overlay({contentBox:"#"+this.get("id")+"_"+J.value+"_menu",bodyContent:"FOO",visible:false,zIndex:999});J.menu.render(this.get("cont"));J.menu.set("visible",false);J.menu.get("contentBox").addClass("yui-button-menu");J.menu.get("contentBox").addClass("yui-color-button-menu");J.menu.after("visibleChange",C.bind(function(){J.menu.set("align",{node:this.getButtonById(J.id).get("node"),points:["tl","bl"]});this._resetColorPicker(); +var K=this._colorPicker;if(K.get("parentNode")){K.get("parentNode").removeChild(K);}J.menu.get("contentBox").query(".yui-widget-bd").set("innerHTML","");J.menu.get("contentBox").query(".yui-widget-bd").appendChild(K);this._colorPicker.setStyle("display","block");},this));return J;},_makeSpinButton:function(W,Q){W.get("node").addClass(this.CLASS_PREFIX+"-spinbutton");var X=this,S=W.get("node"),N=Q.range,M=C.Node.create(''+this.STR_SPIN_UP+""),L=C.Node.create(''+this.STR_SPIN_DOWN+"");S.appendChild(M);S.appendChild(L);var R=C.Lang.substitute(this.STR_SPIN_LABEL,{VALUE:W.get("label")});W.set("title",R);var V=function(Y){Y=((YN[1])?N[1]:Y);return Y;};var U=this.browser;var K=false;var P=this.STR_SPIN_LABEL;if(this._titlebar&&this._titlebar.get("firstChild")){K=this._titlebar.get("firstChild");}var J=function(Z){Z.halt();if(!W.get("disabled")&&(Z.keyCode!=9)){var a=parseInt(W.get("label"),10);a++;a=V(a);W.set("label",""+a);var Y=C.Lang.substitute(P,{VALUE:W.get("label")});W.set("title",Y);X._buttonClick(Z,Q);}};var T=function(Z){Z.halt();if(!W.get("disabled")&&(Z.keyCode!=9)){var a=parseInt(W.get("label"),10);a--;a=V(a);W.set("label",""+a);var Y=C.Lang.substitute(P,{VALUE:W.get("label")});W.set("title",Y);X._buttonClick(Z,Q);}};var O=function(Y){if(Y.keyCode==38){J(Y);}else{if(Y.keyCode==40){T(Y);}else{if(Y.keyCode==107&&Y.shiftKey){J(Y);}else{if(Y.keyCode==109&&Y.shiftKey){T(Y);}}}}};W.on("keydown",C.bind(O,this));M.on("mousedown",function(Y){Y.halt();});L.on("mousedown",function(Y){Y.halt();});M.on("click",C.bind(J,this));L.on("click",C.bind(T,this));},_buttonClick:function(Q,K){var J=true;if(Q&&Q.type=="keypress"){if(Q.keyCode==9){J=false;}else{if((Q.keyCode===13)||(Q.keyCode===0)||(Q.keyCode===32)){}else{J=false;}}}if(J){var S=true,M=false;K.isSelected=this.isSelected(K.id);if(K.value){M=this.fire(K.value+"Click",{type:K.value+"Click",target:this.get("element"),button:K});if(M===false){S=false;}}if(K.menucmd&&S){M=this.fire(K.menucmd+"Click",{type:K.menucmd+"Click",target:this.get("element"),button:K});if(M===false){S=false;}}if(S){this.fire("buttonClick",{type:"buttonClick",target:this.get("element"),button:K});}if(K.type=="select"){var P=this.getButtonById(K.id);if(P.buttonType=="rich"){var O=K.value;for(var N=0;N'+O+"");var R=P.getMenu().getItems();for(var L=0;L(this._buttonList.length-1)){this._navCounter=0;}if(this._navCounter<0){this._navCounter=(this._buttonList.length-1);}if(this._buttonList[this._navCounter]){var J=this._buttonList[this._navCounter].get("node");if(this.browser.ie){J=this._buttonList[this._navCounter].get("node").query("a");}if(this._buttonList[this._navCounter].get("disabled")){this._navigateButtons(K);}else{J.focus();}}break;}},_handleFocus:function(){if(!this._keyNav){var J="keypress";if(this.browser.ie){J="keydown";}this.get("node").on(J,C.bind(this._navigateButtons,this));this._keyNav=true;this._navCounter=-1;}},getButtonById:function(L){var J=this._buttonList.length;for(var K=0;K'+M[J]._oText.nodeValue+"");}else{M[J].cfg.setProperty("checked",false);}}}}}else{return false;}},deselectButton:function(K){var J=B.call(this,K);if(J){J.get("node").removeClass("yui-button-selected");J.get("node").removeClass("yui-button-"+J.get("value")+"-selected");J.get("node").removeClass("yui-button-hover");J._selected=false;}else{return false;}},deselectAllButtons:function(){var J=this._buttonList.length;for(var K=0;K
                    ');C.get("body").appendChild(J);L.element_cont=J;L.editor_wrapper=J.query("div.wrapper");L.toolbar_cont=J.query("div.toolbar_cont");C.get("body").removeChild(J);if(!L.html){L.html='{TITLE}{CONTENT}';}if(!L.css){L.css=this._defaultCSS;}E.superclass.constructor.apply(this,arguments);};E.NAME="simpleeditor";E.ATTRS={node:{setter:function(J){var K=C.get(J);if(!K){C.error("Invalid Node Given: "+J);}else{K=K.item(0);}return K;}},"container":{writeOnce:true,value:false},"iframe":{value:null},"textarea":{value:null,writeOnce:true},"element_cont":{value:null},"editor_wrapper":{value:null},"toolbar_cont":{value:null,writeOnce:true},"toolbar":{value:null},"height":{value:null,setter:function(J){if(this._rendered){this.get("iframe").get("parentNode").setStyle("height",J);}}},"width":{value:null,setter:function(J){if(this._rendered){this.get("element_cont").setStyle("width",J);}}},"blankimage":{value:null},"css":{value:null,writeOnce:true},"html":{value:null,writeOnce:true},"extracss":{value:"",writeOnce:true},setDesignMode:{value:true},nodeChangeDelay:{value:true},dompath:{value:false},markup:{value:"semantic",validator:function(J){switch(J.toLowerCase()){case"semantic":case"css":case"default":case"xhtml":return true;}return false;}},maxUndo:{writeOnce:true,value:30},ptags:{writeOnce:true,value:false},plainText:{writeOnce:true,value:false},disabled_iframe:{value:null},nodeChangeThreshold:{value:3,validator:C.Lang.isNumber},allowNoEdit:{value:false,validator:C.Lang.isBoolean},limitCommands:{value:false,validator:C.Lang.isBoolean},removeLineBreaks:{value:false,validator:C.Lang.isBoolean},filterWord:{value:false,validator:C.Lang.isBoolean},insert:{value:false},saveEl:{value:null,setter:function(J){var K=C.get(J);if(!K){return false;}else{K=K.item(0);}return K;}},disabled:{value:false,setter:function(J){if(this._rendered){this._disableEditor(J);}}},focusAtStart:{value:false,writeOnce:true},handleSubmit:{value:false}};C.extend(E,C.PortBase,{_handleSubmit:function(){var J=this.get("handleSubmit");if(this.get("node").get("form")){if(!this._formButtons){this._formButtons=[];}if(J){this.get("node").get("form").on("submit",C.bind(this._handleFormSubmit,this));var K=this.get("node").get("form").query("input");if(K){K.each(function(M,L){var N=M.get("type");if(N&&(N.toLowerCase()=="submit")){M.on("click",C.bind(this._handleFormButtonClick,this));this._formButtons.push(M);}},this);}}else{}}},_handleInsert:function(){if(this.get("insert")){var M={fontname:true,fontsize:true,forecolor:true,backcolor:true};var L=this._defaultToolbar.buttons;for(var K=0;K');this.get("element_cont").get("firstChild").appendChild(this.dompath);if(this.get("iframe")){this._writeDomPath();}}else{if(!J&&this.dompath){this.dompath.get("parentNode").removeChild(this.dompath);this.dompath=null;}}},_lastCommand:null,_undoNodeChange:function(){},_storeUndo:function(){},_checkKey:function(J,M){var K=false;if((M.keyCode===J.key)){if(J.mods&&(J.mods.length>0)){var N=0;for(var L=0;L',editorDirty:null,_defaultCSS:"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; } .warning-localfile { border-bottom: 1px dashed red !important; } .yui-busy { cursor: wait !important; } img.selected { border: 2px dotted #808080; } img { cursor: pointer !important; border: none; } body.ptags.webkit div.yui-wk-p { margin: 11px 0; } body.ptags.webkit div.yui-wk-div { margin: 0; }",_defaultToolbar:null,_lastButton:null,_baseHREF:function(){var J=document.location.href;if(J.indexOf("?")!==-1){J=J.substring(0,J.indexOf("?"));}J=J.substring(0,J.lastIndexOf("/"))+"/";return J;}(),_lastImage:null,_blankImageLoaded:null,_fixNodesTimer:null,_nodeChangeTimer:null,_nodeChangeDelayTimer:null,_lastNodeChangeEvent:null,_lastNodeChange:0,_rendered:null,DOMReady:null,_selection:null,_mask:null,_showingHiddenElements:null,currentWindow:null,currentEvent:null,operaEvent:null,currentFont:null,currentElement:null,dompath:null,beforeElement:null,afterElement:null,invalidHTML:{form:true,input:true,button:true,select:true,link:true,html:true,body:true,iframe:true,script:true,style:true,textarea:true},toolbar:null,_contentTimer:null,_contentTimerMax:500,_contentTimerCounter:0,_disabled:["createlink","fontname","fontsize","forecolor","backcolor"],_alwaysDisabled:{undo:true,redo:true},_alwaysEnabled:{},_semantic:{"bold":true,"italic":true,"underline":true},_tag2cmd:{"b":"bold","strong":"bold","i":"italic","em":"italic","u":"underline","sup":"superscript","sub":"subscript","img":"insertimage","a":"createlink","ul":"insertunorderedlist","ol":"insertorderedlist"},_createIframe:function(){var M=document.createElement("iframe");M.id=this.get("node").get("id")+"_editor";var L={border:"0",frameBorder:"0",marginWidth:"0",marginHeight:"0",leftMargin:"0",topMargin:"0",allowTransparency:"true",width:"100%"};if(this.get("autoHeight")){L.scrolling="no";}C.each(L,function(O,N){M.setAttribute(N,O);});var K="javascript:;";if(this.browser.ie){K="javascript:false;";}M.setAttribute("src",K);var J=C.get(M);J.setStyle("visibility","hidden");return J;},_isElement:function(K,J){if(K&&K.tagName&&(K.tagName.toLowerCase()==J)){return true;}if(K&&K.getAttribute&&(K.getAttribute("tag")==J)){return true;}return false;},_hasParent:function(K,J){if(!K||!K.parentNode){return false;}while(K.parentNode){if(this._isElement(K,J)){return K;}if(K.parentNode){K=K.parentNode;}else{return false;}}return false;},_getDoc:function(){var K=false;try{var J=document.getElementById(this.get("iframe").get("id"));if(J&&J.contentWindow.document){K=J.contentWindow.document;return K;}}catch(L){return false;}},_getWindow:function(){var J=document.getElementById(this.get("iframe").get("id"));return J.contentWindow;},focus:function(){this._getWindow().focus();},_focusWindow:function(){A.log("_focusWindow: depreciated in favor of this.focus()","warn","Editor");this.focus();},_hasSelection:function(){var L=this._getSelection();var J=this._getRange();var K=false;if(!L||!J){return K;}if(this.browser.ie||this.browser.opera){if(J.text){K=true;}if(J.html){K=true;}}else{if(this.browser.webkit){if(L+""!==""){K=true;}}else{if(L&&(L.toString()!=="")&&(L!==undefined)){K=true;}}}return K;},_getSelection:function(){var J=null;if(this._getDoc()&&this._getWindow()){if(this._getDoc().selection){J=this._getDoc().selection;}else{J=this._getWindow().getSelection();}if(this.browser.webkit){if(J.baseNode){this._selection={};this._selection.baseNode=J.baseNode;this._selection.baseOffset=J.baseOffset;this._selection.extentNode=J.extentNode;this._selection.extentOffset=J.extentOffset;}else{if(this._selection!==null){J=this._getWindow().getSelection();J.setBaseAndExtent(this._selection.baseNode,this._selection.baseOffset,this._selection.extentNode,this._selection.extentOffset);this._selection=null;}}}}return J;},_selectNode:function(K,N){if(!K){return false;}var L=this._getSelection(),J=null;if(this.browser.ie){try{J=this._getDoc().body.createTextRange();J.moveToElementText(K);J.select();}catch(M){A.log("IE failed to select element: "+K.tagName,"warn","SimpleEditor");}}else{if(this.browser.webkit){if(N){L.setBaseAndExtent(K,1,K,K.innerText.length);}else{L.setBaseAndExtent(K,0,K,K.innerText.length);}}else{if(this.browser.opera){L=this._getWindow().getSelection();J=this._getDoc().createRange();J.selectNode(K);L.removeAllRanges();L.addRange(J);}else{J=this._getDoc().createRange();J.selectNodeContents(K);L.removeAllRanges();L.addRange(J);}}}this.nodeChange();},_getRange:function(){var J=this._getSelection();if(J===null){return null;}if(this.browser.webkit&&!J.getRangeAt){var M=this._getDoc().createRange();try{M.setStart(J.anchorNode,J.anchorOffset);M.setEnd(J.focusNode,J.focusOffset);}catch(L){M=this._getWindow().getSelection()+"";}return M;}if(this.browser.ie||this.browser.opera){try{return J.createRange();}catch(K){return null;}}if(J.rangeCount>0){return J.getRangeAt(0);}return null;},_setDesignMode:function(J){if(this.get("setDesignMode")){try{this._getDoc().designMode=((J.toLowerCase()=="off")?"off":"on");}catch(K){}}},_toggleDesignMode:function(){A.log("It is not recommended to use this method and it will be depreciated.","warn","SimpleEditor");var K=this._getDoc().designMode,J=((K.toLowerCase()=="on")?"off":"on");this._setDesignMode(J);return J;},_focused:null,_handleFocus:function(J){if(!this._focused){A.log("Editor Window Focused","info","SimpleEditor");this._focused=true;this.fireEvent("editorWindowFocus",{type:"editorWindowFocus",target:this});}},_handleBlur:function(J){if(this._focused){A.log("Editor Window Blurred","info","SimpleEditor");this._focused=false;this.fireEvent("editorWindowBlur",{type:"editorWindowBlur",target:this});}},_initEditorEvents:function(){var K=this._getDoc(),J=this._getWindow(); +C.Event.nativeAdd(K,"mouseup",C.bind(this._handleMouseUp,this));C.Event.nativeAdd(K,"mousedown",C.bind(this._handleMouseDown,this));C.Event.nativeAdd(K,"click",C.bind(this._handleClick,this));C.Event.nativeAdd(K,"dblclick",C.bind(this._handleDoubleClick,this));C.Event.nativeAdd(K,"keypress",C.bind(this._handleKeyPress,this));C.Event.nativeAdd(K,"keyup",C.bind(this._handleKeyUp,this));C.Event.nativeAdd(K,"keydown",C.bind(this._handleKeyDown,this));C.Event.nativeAdd(J,"focus",C.bind(this._handleFocus,this));C.Event.nativeAdd(J,"blur",C.bind(this._handleBlur,this));},_removeEditorEvents:function(){var K=this._getDoc(),J=this._getWindow();C.Event.nativeRremove(K,"mouseup",C.bind(this._handleMouseUp,this));C.Event.nativeRremove(K,"mousedown",C.bind(this._handleMouseDown,this));C.Event.nativeRremove(K,"click",C.bind(this._handleClick,this));C.Event.nativeRremove(K,"dblclick",C.bind(this._handleDoubleClick,this));C.Event.nativeRremove(K,"keypress",C.bind(this._handleKeyPress,this));C.Event.nativeRremove(K,"keyup",C.bind(this._handleKeyUp,this));C.Event.nativeRremove(K,"keydown",C.bind(this._handleKeyDown,this));C.Event.nativeRremove(J,"focus",C.bind(this._handleFocus,this));C.Event.nativeRremove(J,"blur",C.bind(this._handleBlur,this));},_fixWebkitDivs:function(){if(this.browser.webkit){var J=this._getDoc().body.getElementsByTagName("div");F.addClass(J,"yui-wk-div");}},_initEditor:function(J){if(this._editorInit){return;}this._editorInit=true;if(this.browser.ie){this._getDoc().body.style.margin="0";}if(!this.get("disabled")){this._setDesignMode("on");this._contentTimerCounter=0;}if(!this._getDoc().body){A.log("Body is null, check again","error","SimpleEditor");this._contentTimerCounter=0;this._editorInit=false;this._checkLoaded();return false;}A.log("editorLoaded","info","SimpleEditor");if(!J){this.toolbar.on("buttonClick",C.bind(this._handleToolbarClick,this));}if(!this.get("disabled")){this._initEditorEvents();this.toolbar.set("disabled",false);}if(J){this.fire("editorContentReloaded",{type:"editorreloaded"});}else{this.fire("editorContentLoaded",{type:"editorLoaded"});}this._fixWebkitDivs();if(this.get("dompath")){A.log("Delayed DomPath write","info","SimpleEditor");C.later(150,this,this._writeDomPath);}var K=[];C.each(this.browser,function(M,L){if(M){K.push(L);}},this);if(this.get("ptags")){K.push("ptags");}C.DOM.addClass(this._getDoc().body,K.join(" "));this.nodeChange(true);},_checkLoaded:function(K){this._editorInit=false;this._contentTimerCounter++;if(this._contentTimer){clearTimeout(this._contentTimer);}if(this._contentTimerCounter>this._contentTimerMax){A.log("ERROR: Body Did Not load","error","SimpleEditor");return false;}var M=false;try{if(this._getDoc()&&this._getDoc().body){if(this.browser.ie){if(this._getDoc().body.readyState=="complete"){M=true;}}else{if(this._getDoc().body._rteLoaded===true){M=true;}}}}catch(L){M=false;A.log("checking body (e)"+L,"error","SimpleEditor");}if(M===true){A.log("Firing _initEditor","info","SimpleEditor");this._initEditor(K);}else{var J=this;this._contentTimer=setTimeout(function(){J._checkLoaded.call(J,K);},20);}},_setInitialContent:function(K){A.log("Populating editor body with contents of the text area","info","SimpleEditor");var N=((this._textarea)?this.get("node").get("value"):this.get("node").get("innerHTML")),P=null;if(N===""){N="
                    ";}var L=C.Lang.substitute(this.get("html"),{TITLE:this.STR_TITLE,CONTENT:this._cleanIncomingHTML(N),CSS:this.get("css"),HIDDEN_CSS:((this.get("hiddencss"))?this.get("hiddencss"):"/* No Hidden CSS */"),EXTRA_CSS:((this.get("extracss"))?this.get("extracss"):"/* No Extra CSS */")}),J=true;L=L.replace(/RIGHT_BRACKET/gi,"{");L=L.replace(/LEFT_BRACKET/gi,"}");if(document.compatMode!="BackCompat"){A.log("Adding Doctype to editable area","info","SimpleEditor");L=this._docType+"\n"+L;}else{A.log("DocType skipped because we are in BackCompat Mode.","warn","SimpleEditor");}if(this.browser.ie||this.browser.webkit||this.browser.opera||(navigator.userAgent.indexOf("Firefox/1.5")!=-1)){try{if(this.browser.air){P=this._getDoc().implementation.createHTMLDocument();var Q=this._getDoc();Q.open();Q.close();P.open();P.write(L);P.close();var M=Q.importNode(P.getElementsByTagName("html")[0],true);Q.replaceChild(M,Q.getElementsByTagName("html")[0]);Q.body._rteLoaded=true;}else{P=this._getDoc();P.open();P.write(L);P.close();}}catch(O){A.log("Setting doc failed.. (_setInitialContent)","error","SimpleEditor");J=false;}}else{this.get("iframe").set("src","data:text/html;charset=utf-8,"+encodeURIComponent(L));}this.get("iframe").setStyle("visibility","");if(J){this._checkLoaded(K);}},_setMarkupType:function(J){switch(this.get("markup")){case"css":this._setEditorStyle(true);break;case"default":this._setEditorStyle(false);break;case"semantic":case"xhtml":if(this._semantic[J]){this._setEditorStyle(false);}else{this._setEditorStyle(true);}break;}},_setEditorStyle:function(K){try{this._getDoc().execCommand("useCSS",false,!K);}catch(J){}},_getSelectedElement:function(){var O=this._getDoc(),L=null,M=null,P=null,K=true;if(this.browser.ie){this.currentEvent=this._getWindow().event;L=this._getRange();if(L){P=L.item?L.item(0):L.parentElement();if(this._hasSelection()){}if(P===O.body){P=null;}}if((this.currentEvent!==null)&&(this.currentEvent.keyCode===0)){P=I.getTarget(this.currentEvent);}}else{M=this._getSelection();L=this._getRange();if(!M||!L){return null;}if(!this._hasSelection()&&this.browser.webkit3){}if(this.browser.gecko){if(L.startContainer){if(L.startContainer.nodeType===3){P=L.startContainer.parentNode;}else{if(L.startContainer.nodeType===1){P=L.startContainer;}}if(this.currentEvent){var J=I.getTarget(this.currentEvent);if(!this._isElement(J,"html")){if(P!==J){P=J;}}}}}if(K){if(M.anchorNode&&(M.anchorNode.nodeType==3)){if(M.anchorNode.parentNode){P=M.anchorNode.parentNode;}if(M.anchorNode.nextSibling!=M.focusNode.nextSibling){P=M.anchorNode.nextSibling;}}if(this._isElement(P,"br")){P=null;}if(!P){P=L.commonAncestorContainer;if(!L.collapsed){if(L.startContainer==L.endContainer){if(L.startOffset-L.endOffset<2){if(L.startContainer.hasChildNodes()){P=L.startContainer.childNodes[L.startOffset]; +}}}}}}}if(this.currentEvent!==null){try{switch(this.currentEvent.type){case"click":case"mousedown":case"mouseup":if(this.browser.webkit){P=I.getTarget(this.currentEvent);}break;default:break;}}catch(N){A.log("Firefox 1.5 errors here: "+N,"error","SimpleEditor");}}else{if((this.currentElement&&this.currentElement[0])&&(!this.browser.ie)){}}if(this.browser.opera||this.browser.webkit){if(this.currentEvent&&!P){P=I.getTarget(this.currentEvent);}}if(!P||!P.tagName){P=O.body;}if(this._isElement(P,"html")){P=O.body;}if(this._isElement(P,"body")){P=O.body;}if(P&&!P.parentNode){P=O.body;}if(P===undefined){P=null;}return P;},_getSelectedNode:function(){return C.get(this._getSelectedElement());},_getDomPath:function(J){if(!J){J=this._getSelectedElement();}var K=[];while(J!==null){if(J.ownerDocument!=this._getDoc()){J=null;break;}if(J.nodeName&&J.nodeType&&(J.nodeType==1)){K[K.length]=J;}if(this._isElement(J,"body")){break;}J=J.parentNode;}if(K.length===0){if(this._getDoc()&&this._getDoc().body){K[0]=this._getDoc().body;}}return K.reverse();},_writeDomPath:function(){var P=this._getDomPath(),N=[],L="",Q="";for(var J=0;J10){Q=''+Q.substring(0,10)+"..."+"";}else{Q=''+Q+"";}N[N.length]=Q;}}var M=N.join(" "+this.SEP_DOMPATH+" ");if(this.dompath.get("innerHTML")!=M){this.dompath.set("innerHTML",M);}},_fixNodes:function(){try{var M=this._getDoc(),J=[];C.each(this.invalidHTML,function(P,O){if(O.toLowerCase()!="span"){var Q=M.body.getElementsByTagName(O);if(Q.length){for(var R=0;R-1;J--){if(F.hasClass(O[J],this.CLASS_NOEDIT)){try{this._getDoc().execCommand("enableObjectResizing",false,"false");}catch(N){}this.nodeChange();I.stopEvent(L);A.log("CLASS_NOEDIT found in DOM Path, stopping event","info","SimpleEditor");return true;}}try{this._getDoc().execCommand("enableObjectResizing",false,"true");}catch(M){}}return false;},_setCurrentEvent:function(J){this.currentEvent=J;},_handleClick:function(L){var K=this.fireEvent("beforeEditorClick",{type:"beforeEditorClick",target:this,ev:L});if(K===false){return false;}if(this._isNonEditable(L)){return false;}this._setCurrentEvent(L);if(this.currentWindow){this.closeWindow();}if(this.currentWindow){this.closeWindow();}if(this.browser.webkit){var J=I.getTarget(L);if(this._isElement(J,"a")||this._isElement(J.parentNode,"a")){I.stopEvent(L);this.nodeChange();}}else{this.nodeChange();}this.fireEvent("editorClick",{type:"editorClick",target:this,ev:L});},_handleMouseUp:function(L){var K=this.fireEvent("beforeEditorMouseUp",{type:"beforeEditorMouseUp",target:this,ev:L});if(K===false){return false;}if(this._isNonEditable(L)){return false;}var J=this;if(this.browser.opera){var M=I.getTarget(L);if(this._isElement(M,"img")){this.nodeChange();if(this.operaEvent){clearTimeout(this.operaEvent);this.operaEvent=null;this._handleDoubleClick(L);}else{this.operaEvent=window.setTimeout(function(){J.operaEvent=false;},700);}}}if(this.browser.webkit||this.browser.opera){if(this.browser.webkit){I.stopEvent(L);}}this.nodeChange();this.fireEvent("editorMouseUp",{type:"editorMouseUp",target:this,ev:L});},_handleMouseDown:function(K){var J=this.fireEvent("beforeEditorMouseDown",{type:"beforeEditorMouseDown",target:this,ev:K});if(J===false){return false;}if(this._isNonEditable(K)){return false;}this._setCurrentEvent(K);var L=I.getTarget(K);if(this.browser.webkit&&this._hasSelection()){var M=this._getSelection();if(!this.browser.webkit3){M.collapse(true);}else{M.collapseToStart();}}if(this.browser.webkit&&this._lastImage){F.removeClass(this._lastImage,"selected");this._lastImage=null;}if(this._isElement(L,"img")||this._isElement(L,"a")){if(this.browser.webkit){I.stopEvent(K);if(this._isElement(L,"img")){F.addClass(L,"selected");this._lastImage=L;}}if(this.currentWindow){this.closeWindow();}this.nodeChange();}this.fireEvent("editorMouseDown",{type:"editorMouseDown",target:this,ev:K});},_handleDoubleClick:function(K){var J=this.fireEvent("beforeEditorDoubleClick",{type:"beforeEditorDoubleClick",target:this,ev:K});if(J===false){return false;}if(this._isNonEditable(K)){return false;}this._setCurrentEvent(K);var L=I.getTarget(K);if(this._isElement(L,"img")){this.currentElement[0]=L;this.toolbar.fireEvent("insertimageClick",{type:"insertimageClick",target:this.toolbar});this.fireEvent("afterExecCommand",{type:"afterExecCommand",target:this});}else{if(this._hasParent(L,"a")){this.currentElement[0]=this._hasParent(L,"a");this.toolbar.fireEvent("createlinkClick",{type:"createlinkClick",target:this.toolbar}); +this.fireEvent("afterExecCommand",{type:"afterExecCommand",target:this});}}this.nodeChange();this.fireEvent("editorDoubleClick",{type:"editorDoubleClick",target:this,ev:K});},_handleKeyUp:function(L){var K=this.fireEvent("beforeEditorKeyUp",{type:"beforeEditorKeyUp",target:this,ev:L});if(K===false){return false;}if(this._isNonEditable(L)){return false;}this._setCurrentEvent(L);switch(L.keyCode){case this._keyMap.SELECT_ALL.key:if(this._checkKey(this._keyMap.SELECT_ALL,L)){this.nodeChange();}break;case 32:case 35:case 36:case 37:case 38:case 39:case 40:case 46:case 8:case this._keyMap.CLOSE_WINDOW.key:if((L.keyCode==this._keyMap.CLOSE_WINDOW.key)&&this.currentWindow){if(this._checkKey(this._keyMap.CLOSE_WINDOW,L)){this.closeWindow();}}else{if(!this.browser.ie){if(this._nodeChangeTimer){clearTimeout(this._nodeChangeTimer);}var J=this;this._nodeChangeTimer=setTimeout(function(){J._nodeChangeTimer=null;J.nodeChange.call(J);},100);}else{this.nodeChange();}this.editorDirty=true;}break;}this.fireEvent("editorKeyUp",{type:"editorKeyUp",target:this,ev:L});this._storeUndo();},_handleKeyPress:function(L){var K=this.fireEvent("beforeEditorKeyPress",{type:"beforeEditorKeyPress",target:this,ev:L});if(K===false){return false;}if(this.get("allowNoEdit")){if(L&&L.keyCode&&(L.keyCode==63272)){A.log("allowNoEdit is set, forward delete key has been disabled","warn","SimpleEditor");I.stopEvent(L);}}if(this._isNonEditable(L)){return false;}this._setCurrentEvent(L);if(this.browser.opera){if(L.keyCode===13){var J=this._getSelectedElement();if(!this._isElement(J,"li")){this.execCommand("inserthtml","
                    ");I.stopEvent(L);}}}if(this.browser.webkit){if(!this.browser.webkit3){if(L.keyCode&&(L.keyCode==122)&&(L.metaKey)){if(this._hasParent(this._getSelectedElement(),"li")){A.log("We are in an LI and we found CMD + z, stopping the event","warn","SimpleEditor");I.stopEvent(L);}}}this._listFix(L);}this._fixListDupIds();this.fireEvent("editorKeyPress",{type:"editorKeyPress",target:this,ev:L});},_handleKeyDown:function(c){var e=this.fireEvent("beforeEditorKeyDown",{type:"beforeEditorKeyDown",target:this,ev:c});if(e===false){return false;}var Z=null,J=null;if(this._isNonEditable(c)){return false;}this._setCurrentEvent(c);if(this.currentWindow){this.closeWindow();}if(this.currentWindow){this.closeWindow();}var S=false,X=null,U=null,W=false;switch(c.keyCode){case this._keyMap.FOCUS_TOOLBAR.key:if(this._checkKey(this._keyMap.FOCUS_TOOLBAR,c)){var b=this.toolbar.getElementsByTagName("h2")[0];if(b&&b.firstChild){b.firstChild.focus();}}else{if(this._checkKey(this._keyMap.FOCUS_AFTER,c)){this.afterElement.focus();}}I.stopEvent(c);S=false;break;case this._keyMap.CREATE_LINK.key:if(this._hasSelection()){if(this._checkKey(this._keyMap.CREATE_LINK,c)){var K=true;if(this.get("limitCommands")){if(!this.toolbar.getButtonByValue("createlink")){A.log("Toolbar Button for (createlink) was not found, skipping exec.","info","SimpleEditor");K=false;}}if(K){this.execCommand("createlink","");this.toolbar.fireEvent("createlinkClick",{type:"createlinkClick",target:this.toolbar});this.fireEvent("afterExecCommand",{type:"afterExecCommand",target:this});S=false;}}}break;case this._keyMap.UNDO.key:case this._keyMap.REDO.key:if(this._checkKey(this._keyMap.REDO,c)){X="redo";S=true;}else{if(this._checkKey(this._keyMap.UNDO,c)){X="undo";S=true;}}break;case this._keyMap.BOLD.key:if(this._checkKey(this._keyMap.BOLD,c)){X="bold";S=true;}break;case this._keyMap.FONT_SIZE_UP.key:case this._keyMap.FONT_SIZE_DOWN.key:var P=false,Y=false;if(this._checkKey(this._keyMap.FONT_SIZE_UP,c)){P=true;}if(this._checkKey(this._keyMap.FONT_SIZE_DOWN,c)){Y=true;}if(P||Y){var M=this.toolbar.getButtonByValue("fontsize"),L=parseInt(M.get("label"),10),N=(L+1);if(Y){N=(L-1);}X="fontsize";U=N+"px";S=true;}break;case this._keyMap.ITALIC.key:if(this._checkKey(this._keyMap.ITALIC,c)){X="italic";S=true;}break;case this._keyMap.UNDERLINE.key:if(this._checkKey(this._keyMap.UNDERLINE,c)){X="underline";S=true;}break;case 9:if(this.browser.ie){J=this._getRange();Z=this._getSelectedElement();if(!this._isElement(Z,"li")){if(J){J.pasteHTML("    ");J.collapse(false);J.select();}I.stopEvent(c);}}if(this.browser.gecko>1.8){Z=this._getSelectedElement();if(this._isElement(Z,"li")){if(c.shiftKey){this._getDoc().execCommand("outdent",null,"");}else{this._getDoc().execCommand("indent",null,"");}}else{if(!this._hasSelection()){this.execCommand("inserthtml","    ");}}I.stopEvent(c);}break;case 13:var R=null,a=0;if(this.get("ptags")&&!c.shiftKey){if(this.browser.gecko){Z=this._getSelectedElement();if(!this._hasParent(Z,"li")){if(this._hasParent(Z,"p")){R=this._getDoc().createElement("p");R.innerHTML=" ";F.insertAfter(R,Z);this._selectNode(R.firstChild);}else{if(this._isElement(Z,"body")){this.execCommand("insertparagraph",null);var T=this._getDoc().body.getElementsByTagName("p");for(a=0;a');var Q=this._getDoc().getElementById("yui-br"),d=this._getDoc().createElement("br"),O=this._getDoc().createElement("span");Q.parentNode.replaceChild(d,Q);O.className="yui-non";O.innerHTML=" ";F.insertAfter(O,d);this._selectNode(O);}I.stopEvent(c); +}}if(this.browser.ie){A.log("Stopping P tags","info","SimpleEditor");J=this._getRange();Z=this._getSelectedElement();if(!this._isElement(Z,"li")){if(J){J.pasteHTML("
                    ");J.collapse(false);J.select();}I.stopEvent(c);}}}break;}if(this.browser.ie){this._listFix(c);}if(S&&X){this.execCommand(X,U);I.stopEvent(c);this.nodeChange();}this.fireEvent("editorKeyDown",{type:"editorKeyDown",target:this,ev:c});},_fixListRunning:null,_fixListDupIds:function(){if(this._fixListRunning){return false;}if(this._getDoc()){this._fixListRunning=true;var J=this._getDoc().body.getElementsByTagName("li"),K=0,L={};for(K=0;K'+J+" ";}else{N.innerHTML='  ';}}}else{if(J){N.innerHTML=J+" ";}else{N.innerHTML=" ";}}N.parentNode.replaceChild(O,N);O.appendChild(N);if(this.browser.webkit){this._getSelection().setBaseAndExtent(N.firstChild,1,N.firstChild,N.firstChild.innerText.length);if(!this.browser.webkit3){N.parentNode.parentNode.style.display="list-item";setTimeout(function(){N.parentNode.parentNode.style.display="block";},1);}}else{if(this.browser.ie){L=this._getDoc().body.createTextRange();L.moveToElementText(N);L.collapse(false);L.select();}else{this._selectNode(N);}}I.stopEvent(P);}if(this.browser.webkit){I.stopEvent(P);}this.nodeChange();}},nodeChange:function(J){var K=this;this._storeUndo();if(this.get("nodeChangeDelay")){this._nodeChangeDelayTimer=window.setTimeout(function(){K._nodeChangeDelayTimer=null;K._nodeChange.apply(K,arguments);},0);}else{this._nodeChange();}},_nodeChange:function(K){var M=parseInt(this.get("nodeChangeThreshold"),10),T=Math.round(new Date().getTime()/1000),W=this;if(K===true){this._lastNodeChange=0;}if((this._lastNodeChange+M)0){for(var a=0;a'+f+"");this._updateMenuChecked("fontname",f);}if(Q){Q.set("label",Q._conf.data.initValue.label);}var P=this.toolbar.getButtonByValue("heading");if(P){P.set("label",P._conf.data.initValue.label);this._updateMenuChecked("heading","none");}var N=this.toolbar.getButtonByValue("insertimage");if(N&&this.currentWindow&&(this.currentWindow.name=="insertimage")){this.toolbar.disableButton(N);}if(this._lastButton&&this._lastButton.isSelected){this.toolbar.deselectButton(this._lastButton.id);}this._undoNodeChange();}}this.fire("afterNodeChange",{type:"afterNodeChange"});},_updateMenuChecked:function(J,K,M){if(!M){M=this.toolbar;}var L=M.getButtonByValue(J);L.checkValue(K);},_handleToolbarClick:function(K){var M="";var N="";var L=K.button.value;if(K.button.menucmd){M=L;L=K.button.menucmd;}this._lastButton=K.button;if(this.STOP_EXEC_COMMAND){A.log("execCommand skipped because we found the STOP_EXEC_COMMAND flag set to true","warn","SimpleEditor");A.log("NOEXEC::execCommand::("+L+"), ("+M+")","warn","SimpleEditor");this.STOP_EXEC_COMMAND=false;return false;}else{this.execCommand(L,M);if(!this.browser.webkit){var J=this;setTimeout(function(){J.focus.call(J);},5);}}I.stopEvent(K);},_setupAfterElement:function(){if(!this.beforeElement){this.beforeElement=C.Node.create('

                    ');this.beforeElement.set("innerHTML",this.STR_BEFORE_EDITOR);this.get("element_cont").query(".first-child").insertBefore(this.beforeElement,this.toolbar.get("node").get("nextSibling"));}if(!this.afterElement){this.afterElement=C.Node.create('

                    '+this.STR_LEAVE_EDITOR+"

                    ");this.get("element_cont").get("firstChild").appendChild(this.afterElement);}},_disableEditor:function(N){var M,L,K,J;if(!this.get("disabled_iframe")&&N){M=this._createIframe();M.set("id","disabled_"+this.get("iframe").get("id"));M.setStyle("height","100%");M.setStyle("display","none");M.setStyle("visibility","visible");this.set("disabled_iframe",M);L=this.get("iframe").get("parentNode");L.appendChild(M);}if(!M){M=this.get("disabled_iframe");}if(N){this._orgIframe=this.get("iframe");if(this.toolbar){this.toolbar.set("disabled",true);}K=this.getEditorHTML();J=this.get("iframe").get("offsetHeight");M.setStyle("visibility","");M.setStyle("position","");M.setStyle("top","");M.setStyle("left","");this._orgIframe.setStyle("visibility","hidden");this._orgIframe.setStyle("position","absolute");this._orgIframe.setStyle("top","-99999px");this._orgIframe.setStyle("left","-99999px");this.set("iframe",M);this._setInitialContent(true);if(!this._mask){this._mask=C.Node.create('
                    ');if(this.browser.ie){this._mask.setStyle("height",J+"px");}this.get("iframe").get("parentNode").appendChild(this._mask);}this.on("editorContentReloaded",function(){this._getDoc().body._rteLoaded=false;this.setEditorHTML(K);M.setStyle("display","block");});}else{if(this._mask){this._mask.get("parentNode").removeChild(this._mask);this._mask=null;if(this.toolbar){this.toolbar.set("disabled",false);}M.setStyle("visibility","hidden");M.setStyle("position","absolute");M.setStyle("top","-99999px");M.setStyle("left","-99999px");this._orgIframe.setStyle("visibility","");this._orgIframe.setStyle("position","");this._orgIframe.setStyle("top","");this._orgIframe.setStyle("left","");this.set("iframe",this._orgIframe);this.focus();C.later(100,this,this.nodeChange);}}},SEP_DOMPATH:"<",STR_LEAVE_EDITOR:"You have left the Rich Text Editor.",STR_BEFORE_EDITOR:"This text field can contain stylized text and graphics. To cycle through all formatting options, use the keyboard shortcut Shift + Escape to place focus on the toolbar and navigate between options with your arrow keys. To exit this text editor use the Escape key and continue tabbing.

                    Common formatting keyboard shortcuts:

                    • Control Shift B sets text to bold
                    • Control Shift I sets text to italic
                    • Control Shift U underlines text
                    • Control Shift L adds an HTML link
                    ",STR_TITLE:"Rich Text Area.",STR_IMAGE_HERE:"Image URL Here",STR_IMAGE_URL:"Image URL",STR_LINK_URL:"Link URL",STOP_EXEC_COMMAND:false,STOP_NODE_CHANGE:false,CLASS_NOEDIT:"yui-noedit",CLASS_CONTAINER:"yui-editor-container",CLASS_EDITABLE:"yui-editor-editable",CLASS_EDITABLE_CONT:"yui-editor-editable-container",CLASS_PREFIX:"yui-editor",browser:function(){var J=C.UA;if(J.webkit>=420){J.webkit3=J.webkit;}else{J.webkit3=0;}if(J.webkit>=530){J.webkit4=J.webkit;}else{J.webkit4=0;}J.mac=false;if(navigator.userAgent.indexOf("Macintosh")!==-1){J.mac=true;}return J;}(),initializer:function(){if(!this._defaultToolbar){this._defaultToolbar={collapse:true,titlebar:"Text Editing Tools",draggable:false,buttons:[{group:"fontstyle",label:"Font Name and Size",buttons:[{type:"select",label:"Arial",value:"fontname",disabled:true,menu:[{text:"Arial",checked:true},{text:"Arial Black"},{text:"Comic Sans MS"},{text:"Courier New"},{text:"Lucida Console"},{text:"Tahoma"},{text:"Times New Roman"},{text:"Trebuchet MS"},{text:"Verdana"}]},{type:"spin",label:"13",value:"fontsize",range:[9,75],disabled:true}]},{type:"separator"},{group:"textstyle",label:"Font Style",buttons:[{type:"push",label:"Bold CTRL + SHIFT + B",value:"bold"},{type:"push",label:"Italic CTRL + SHIFT + I",value:"italic"},{type:"push",label:"Underline CTRL + SHIFT + U",value:"underline"},{type:"push",label:"Strike Through",value:"strikethrough"},{type:"separator"},{type:"color",label:"Font Color",value:"forecolor",disabled:true},{type:"color",label:"Background Color",value:"backcolor",disabled:true}]},{type:"separator"},{group:"indentlist",label:"Lists",buttons:[{type:"push",label:"Create an Unordered List",value:"insertunorderedlist"},{type:"push",label:"Create an Ordered List",value:"insertorderedlist"}]},{type:"separator"},{group:"insertitem",label:"Insert Item",buttons:[{type:"push",label:"HTML Link CTRL + SHIFT + L",value:"createlink",disabled:true},{type:"push",label:"Insert Image",value:"insertimage"}]}]}; +}C.Editor.Info._instances[this.get("node").get("id")]=this;this.currentElement=[];this._handleDOMPath();this.on("dompathChange",C.bind(this._handleDOMPath,this));this.set("blankimage",this._getBlankImage());this._handleInsert();this.set("saveEl",this.get("node"));this._handleSubmit();this.on("handleSubmitChange",C.bind(this._handleSubmit,this));if(this.get("focusAtStart")){this.on("editorContentLoaded",function(){C.later(400,this,function(){this.focus();this.editorDirty=false;});});}},_getBlankImage:function(){var J="";if(!this._blankImageLoaded){if(C.Editor.Info.blankImage){this.set("blankimage",C.Editor.Info.blankImage);this._blankImageLoaded=true;}else{var K=document.createElement("div");K.style.position="absolute";K.style.top="-9999px";K.style.left="-9999px";K.className=this.CLASS_PREFIX+"-blankimage";document.body.appendChild(K);J=F.getStyle(K,"backgroundImage");J=J.replace("url(","").replace(")","").replace(/"/g,"");J=J.replace("app:/","");this.set("blankimage",J);this._blankImageLoaded=true;K.parentNode.removeChild(K);C.Editor.Info.blankImage=J;}}else{J=this.get("blankimage");}return J;},_handleAutoHeight:function(){var O=this._getDoc(),K=O.body,P=O.documentElement;var J=parseInt(F.getStyle(this.get("editor_wrapper"),"height"),10);var L=K.scrollHeight;if(this.browser.webkit){L=P.scrollHeight;}if(L=parseInt(this.get("height"),10))){var N=this.get("animate");this.set("animate",false);this.set("height",L+"px");this.set("animate",N);if(this.browser.ie){this.get("iframe").setStyle("height","99%");this.get("iframe").setStyle("zoom","1");var M=this;window.setTimeout(function(){M.get("iframe").setStyle("height","100%");},1);}}},_formButtons:null,_formButtonClicked:null,_handleFormButtonClick:function(K){var J=I.getTarget(K);this._formButtonClicked=J;},_handleFormSubmit:function(L){this.saveHTML();var K=this.get("node").get("form"),J=this._formButtonClicked||false;K.detach("submit",C.bind(this._handleFormSubmit,this));},_handleFontSize:function(L){var J=this.toolbar.getButtonById(L.button.id);var K=J.get("label")+"px";this.execCommand("fontsize",K);return false;},_handleColorPicker:function(L){var K=L.button;var J="#"+L.color;if((K=="forecolor")||(K=="backcolor")){this.execCommand(K,J);}},_handleAlign:function(M){var L=null;for(var J=0;J'+M+"";if(N.get("label")!=R){N.set("label",R);this._updateMenuChecked("fontname",M);}}if(O){Q=parseInt(F.getStyle(P,"fontSize"),10);if((Q===null)||isNaN(Q)){Q=O._conf.data.initValue.label;}O.set("label",""+Q);}if(!this._isElement(P,"body")&&!this._isElement(P,"img")){this.toolbar.enableButton(N);this.toolbar.enableButton(O);this.toolbar.enableButton("forecolor");this.toolbar.enableButton("backcolor");}if(this._isElement(P,"img")){if(C.Overlay){this.toolbar.enableButton("createlink");}}if(this._hasParent(P,"blockquote")){this.toolbar.selectButton("indent");this.toolbar.disableButton("indent");this.toolbar.enableButton("outdent");}if(this._hasParent(P,"ol")||this._hasParent(P,"ul")){this.toolbar.disableButton("indent");}this._lastButton=null;},_handleInsertImageClick:function(){if(this.get("limitCommands")){if(!this.toolbar.getButtonByValue("insertimage")){A.log("Toolbar Button for (insertimage) was not found, skipping exec.","info","SimpleEditor");return false;}}this.toolbar.set("disabled",true);var J=function(){var K=this.currentElement[0],M="http://";if(!K){K=this._getSelectedElement();}if(K){if(K.getAttribute("src")){M=K.getAttribute("src",2);if(M.indexOf(this.get("blankimage"))!=-1){M=this.STR_IMAGE_HERE;}}}var L=prompt(this.STR_IMAGE_URL+": ",M);if((L!=="")&&(L!==null)){K.setAttribute("src",L);}else{if(L===""){K.parentNode.removeChild(K);this.currentElement=[];this.nodeChange();}else{if((L===null)){M=K.getAttribute("src",2);if(M.indexOf(this.get("blankimage"))!=-1){K.parentNode.removeChild(K);this.currentElement=[];this.nodeChange();}}}}this.closeWindow();this.toolbar.set("disabled",false);this.unsubscribe("afterExecCommand",J,this,true);};this.on("afterExecCommand",J,this,true);},_handleInsertImageWindowClose:function(){this.nodeChange();},_isLocalFile:function(J){if((J)&&(J!=="")&&((J.indexOf("file:/")!=-1)||(J.indexOf(":\\")!=-1))){return true;}return false;},_handleCreateLinkClick:function(){if(this.get("limitCommands")){if(!this.toolbar.getButtonByValue("createlink")){A.log("Toolbar Button for (createlink) was not found, skipping exec.","info","SimpleEditor");return false;}}this.toolbar.set("disabled",true);var J=function(){var M=this.currentElement[0],L="";if(M){if(M.getAttribute("href",2)!==null){L=M.getAttribute("href",2);}}var O=prompt(this.STR_LINK_URL+": ",L);if((O!=="")&&(O!==null)){var N=O;if((N.indexOf(":/"+"/")==-1)&&(N.substring(0,1)!="/")&&(N.substring(0,6).toLowerCase()!="mailto")){if((N.indexOf("@")!=-1)&&(N.substring(0,6).toLowerCase()!="mailto")){N="mailto:"+N;}else{if(N.substring(0,1)!="#"){}}}M.setAttribute("href",N);}else{if(O!==null){var K=this._getDoc().createElement("span");K.innerHTML=M.innerHTML;F.addClass(K,"yui-non");M.parentNode.replaceChild(K,M);}}this.closeWindow();this.toolbar.set("disabled",false);this.unsubscribe("afterExecCommand",J,this,true);};this.on("afterExecCommand",J,this);},_handleCreateLinkWindowClose:function(){this.nodeChange(); +this.currentElement=[];},render:function(){if(this._rendered){return false;}A.log("Render","info","SimpleEditor");if(this.get("node")){this._textarea=true;if(this.get("node").get("tagName").toLowerCase()!=="textarea"){this._textarea=false;}}else{A.log("No Element","error","SimpleEditor");return false;}this._rendered=true;C.later(4,this,this._render);},_render:function(){A.log("_render");this.set("textarea",this.get("node"));this.get("element_cont").setStyle("display","none");this.get("element_cont").addClass(this.CLASS_CONTAINER);this.set("iframe",this._createIframe());C.later(10,this,this._setInitialContent,false);this.get("editor_wrapper").appendChild(this.get("iframe"));if(this.get("disabled")){this._disableEditor(true);}var J=this.get("toolbar");if(!J){J=this._defaultToolbar;}if(J instanceof C.Toolbar.Bar){this.toolbar=J;this.toolbar.set("disabled",true);}else{J.node=this.get("toolbar_cont");J.disabled=true;this.toolbar=new C.Toolbar.Bar(J);}A.log("fireEvent::toolbarLoaded","info","SimpleEditor");this.fire("toolbarLoaded",{type:"toolbarLoaded"});this.toolbar.on("toolbarCollapsed",function(){if(this.currentWindow){this.moveWindow();}},this,true);this.toolbar.on("toolbarExpanded",function(){if(this.currentWindow){this.moveWindow();}},this,true);this.toolbar.on("fontsizeClick",this._handleFontSize,this,true);this.toolbar.on("colorPickerClicked",function(K){this._handleColorPicker(K);this.STOP_EXEC_COMMAND=true;},this,true);this.toolbar.on("alignClick",this._handleAlign,this,true);this.on("afterNodeChange",this._handleAfterNodeChange,this,true);this.toolbar.on("insertimageClick",this._handleInsertImageClick,this,true);this.on("windowinsertimageClose",this._handleInsertImageWindowClose,this,true);this.toolbar.on("createlinkClick",this._handleCreateLinkClick,this,true);this.on("windowcreatelinkClose",this._handleCreateLinkWindowClose,this,true);this.get("node").get("parentNode").replaceChild(this.get("element_cont"),this.get("node"));this.get("node").setStyle("visibility","hidden");this.get("node").setStyle("position","absolute");this.get("node").setStyle("top","-9999px");this.get("node").setStyle("left","-9999px");this.get("element_cont").appendChild(this.get("node"));this.get("element_cont").setStyle("display","block");this.get("iframe").get("parentNode").addClass(this.CLASS_EDITABLE_CONT);this.get("iframe").addClass(this.CLASS_EDITABLE);this.get("element_cont").setStyle("width",this.get("width"));this.get("iframe").get("parentNode").setStyle("height",this.get("height"));this.get("iframe").setStyle("width","100%");this.get("iframe").setStyle("height","100%");C.later(0,this,this._setupAfterElement);this.fire("afterRender",{type:"afterRender"});},execCommand:function(L,K){var O=this.fireEvent("beforeExecCommand",{type:"beforeExecCommand",target:this,args:arguments});if((O===false)||(this.STOP_EXEC_COMMAND)){this.STOP_EXEC_COMMAND=false;return false;}this._lastCommand=L;this._setMarkupType(L);if(this.browser.ie){this._getWindow().focus();}var J=true;if(this.get("limitCommands")){if(!this.toolbar.getButtonByValue(L)){A.log("Toolbar Button for ("+L+") was not found, skipping exec.","info","SimpleEditor");J=false;}}this.editorDirty=true;if((typeof this["cmd_"+L.toLowerCase()]=="function")&&J){A.log("Found execCommand override method: (cmd_"+L.toLowerCase()+")","info","SimpleEditor");var N=this["cmd_"+L.toLowerCase()](K);J=N[0];if(N[1]){L=N[1];}if(N[2]){K=N[2];}}if(J){A.log("execCommand::("+L+"), ("+K+")","info","SimpleEditor");try{this._getDoc().execCommand(L,false,K);}catch(M){A.log("execCommand Failed","error","SimpleEditor");}}else{A.log("OVERRIDE::execCommand::("+L+"),("+K+") skipped","warn","SimpleEditor");}this.on("afterExecCommand",function(){this.unsubscribeAll("afterExecCommand");this.nodeChange();},this,true);this.fireEvent("afterExecCommand",{type:"afterExecCommand",target:this});},cmd_bold:function(M){if(!this.browser.webkit){var L=this._getSelectedElement();if(L&&this._isElement(L,"span")&&this._hasSelection()){if(L.style.fontWeight=="bold"){L.style.fontWeight="";var J=this._getDoc().createElement("b"),K=L.parentNode;K.replaceChild(J,L);J.appendChild(L);}}}return[true];},cmd_italic:function(M){if(!this.browser.webkit){var L=this._getSelectedElement();if(L&&this._isElement(L,"span")&&this._hasSelection()){if(L.style.fontStyle=="italic"){L.style.fontStyle="";var J=this._getDoc().createElement("i"),K=L.parentNode;K.replaceChild(J,L);J.appendChild(L);}}}return[true];},cmd_underline:function(K){if(!this.browser.webkit){var J=this._getSelectedElement();if(J&&this._isElement(J,"span")){if(J.style.textDecoration=="underline"){J.style.textDecoration="none";}else{J.style.textDecoration="underline";}return[false];}}return[true];},cmd_backcolor:function(M){var J=true,K=this._getSelectedElement(),L="backcolor";if(this.browser.gecko||this.browser.opera){this._setEditorStyle(true);L="hilitecolor";}if(!this._isElement(K,"body")&&!this._hasSelection()){K.style.backgroundColor=M;this._selectNode(K);J=false;}else{if(this.get("insert")){K=this._createInsertElement({backgroundColor:M});}else{this._createCurrentElement("span",{backgroundColor:M,color:K.style.color,fontSize:K.style.fontSize,fontFamily:K.style.fontFamily});this._selectNode(this.currentElement[0]);}J=false;}return[J,L];},cmd_forecolor:function(L){var J=true,K=this._getSelectedElement();if(!this._isElement(K,"body")&&!this._hasSelection()){F.setStyle(K,"color",L);this._selectNode(K);J=false;}else{if(this.get("insert")){K=this._createInsertElement({color:L});}else{this._createCurrentElement("span",{color:L,fontSize:K.style.fontSize,fontFamily:K.style.fontFamily,backgroundColor:K.style.backgroundColor});this._selectNode(this.currentElement[0]);}J=false;}return[J];},cmd_unlink:function(J){this._swapEl(this.currentElement[0],"span",function(K){K.className="yui-non";});return[false];},cmd_createlink:function(L){var K=this._getSelectedElement(),J=null;if(this._hasParent(K,"a")){this.currentElement[0]=this._hasParent(K,"a");}else{if(this._isElement(K,"li")){J=this._getDoc().createElement("a"); +J.innerHTML=K.innerHTML;K.innerHTML="";K.appendChild(J);this.currentElement[0]=J;}else{if(!this._isElement(K,"a")){this._createCurrentElement("a");J=this._swapEl(this.currentElement[0],"a");this.currentElement[0]=J;}else{this.currentElement[0]=K;}}}return[false];},cmd_insertimage:function(O){var J=true,K=null,N="insertimage",M=this._getSelectedElement();if(O===""){O=this.get("blankimage");}A.log("InsertImage: "+M.tagName,"info","SimpleEditor");if(this._isElement(M,"img")){this.currentElement[0]=M;J=false;}else{if(this._getDoc().queryCommandEnabled(N)){this._getDoc().execCommand(N,false,O);var P=this._getDoc().getElementsByTagName("img");for(var L=0;L"+K[R].innerHTML+"";}c.innerHTML=X;this.currentElement[0]=L;this.currentElement[0].parentNode.replaceChild(c,this.currentElement[0]);}else{A.log("Create list item","info","SimpleEditor");this._createCurrentElement(g.toLowerCase());c=this._getDoc().createElement(g);for(R=0;R  ';c.appendChild(O);if(R>0){this.currentElement[R].parentNode.removeChild(this.currentElement[R]);}}var W=c.firstChild.innerHTML.split("
                    ");if(W.length>0){c.innerHTML="";for(var b=0;b";}var f=this._getDoc().createElement("span");f.innerHTML=X;L.parentNode.parentNode.replaceChild(f,L.parentNode);}else{this.nodeChange();this._getDoc().execCommand(Z,"",L.parentNode);this.nodeChange();}Y=false;}if(this.browser.opera){var V=this;window.setTimeout(function(){var h=V._getDoc().getElementsByTagName("li");for(var j=0;j"){h[j].parentNode.parentNode.removeChild(h[j].parentNode);}}},30);}if(this.browser.ie&&Y){var P="";if(this._getRange().html){P="
                  • "+this._getRange().html+"
                  • ";}else{var Q=this._getRange().text.split("\n");if(Q.length>1){P="";for(var U=0;U"+Q[U]+"";}}else{var T=this._getRange().text;if(T===""){P='
                  • '+T+"
                  • ";}else{P="
                  • "+T+"
                  • ";}}}this._getRange().pasteHTML("<"+g+">"+P+"");var J=this._getDoc().getElementById("new_list_item");if(J){var S=this._getDoc().body.createTextRange();S.moveToElementText(J);S.collapse(false);S.select();J.id="";}Y=false;}}return Y;},cmd_insertorderedlist:function(J){return[this.cmd_list("ol")];},cmd_insertunorderedlist:function(J){return[this.cmd_list("ul")];},cmd_fontname:function(M){var J=true,L=this._getSelectedElement();this.currentFont=M;if(L&&L.tagName&&!this._hasSelection()&&!this._isElement(L,"body")&&!this.get("insert")){F.setStyle(L,"fontFamily",M);J=false;}else{if(this.get("insert")&&!this._hasSelection()){A.log("No selection and no selected element and we are in insert mode","info","SimpleEditor");var K=this._createInsertElement({fontFamily:M});J=false;}}return[J];},cmd_fontsize:function(M){var J=null,L=true;J=this._getSelectedElement();if(this.browser.webkit){if(this.currentElement[0]){if(J==this.currentElement[0]){L=false;F.setStyle(J,"fontSize",M);this._selectNode(J);this.currentElement[0]=J;}}}if(L){if(!this._isElement(this._getSelectedElement(),"body")&&(!this._hasSelection())){J=this._getSelectedElement();F.setStyle(J,"fontSize",M);if(this.get("insert")&&this.browser.ie){var K=this._getRange();K.collapse(false);K.select();}else{this._selectNode(J);}}else{if(this.currentElement&&(this.currentElement.length>0)&&(!this._hasSelection())&&(!this.get("insert"))){F.setStyle(this.currentElement,"fontSize",M);}else{if(this.get("insert")&&!this._hasSelection()){J=this._createInsertElement({fontSize:M}); +this.currentElement[0]=J;this._selectNode(this.currentElement[0]);}else{this._createCurrentElement("span",{"fontSize":M,fontFamily:J.style.fontFamily,color:J.style.color,backgroundColor:J.style.backgroundColor});this._selectNode(this.currentElement[0]);}}}}return[false];},_swapEl:function(K,J,M){var L=this._getDoc().createElement(J);if(K){L.innerHTML=K.innerHTML;}if(typeof M=="function"){M.call(this,L);}if(K){K.parentNode.replaceChild(L,K);}return L;},_createInsertElement:function(J){this._createCurrentElement("span",J);var K=this.currentElement[0];if(this.browser.webkit){K.innerHTML=' ';K=K.firstChild;this._getSelection().setBaseAndExtent(K,1,K,K.innerText.length);}else{if(this.browser.ie||this.browser.opera){K.innerHTML=" ";}}this.focus();this._selectNode(K,true);return K;},_createCurrentElement:function(L,O){L=((L)?L:"a");var W=null,K=[],M=this._getDoc();if(this.currentFont){if(!O){O={};}O.fontFamily=this.currentFont;this.currentFont=null;}this.currentElement=[];var R=function(b,d){var c=null;b=((b)?b:"span");b=b.toLowerCase();switch(b){case"h1":case"h2":case"h3":case"h4":case"h5":case"h6":c=M.createElement(b);break;default:c=M.createElement(b);if(b==="span"){F.addClass(c,"yui-tag-"+b);F.addClass(c,"yui-tag");c.setAttribute("tag",b);}if(d){C.each(d,function(f,e){c.style[e]=f;});}break;}return c;};if(!this._hasSelection()){if(this._getDoc().queryCommandEnabled("insertimage")){this._getDoc().execCommand("insertimage",false,"yui-tmp-img");var Q=this._getDoc().getElementsByTagName("img");for(var V=0;V]*)>/gi,"");J=J.replace(/<\/strong>/gi,"
                    ");J=J.replace(/]*)>/gi,"");J=J.replace(/<\/embed>/gi,"");J=J.replace(/]*)>/gi,"");J=J.replace(/<\/em>/gi,"");J=J.replace(/_moz_dirty=""/gi,"");J=J.replace(/]*)>/gi,"");J=J.replace(/<\/YUI_EMBED>/gi,"");if(this.get("plainText")){A.log("Filtering as plain text","info","SimpleEditor"); +J=J.replace(/\n/g,"
                    ").replace(/\r/g,"
                    ");J=J.replace(/ /gi,"  ");J=J.replace(/\t/gi,"    ");}J=J.replace(/]*)>/gi,"");J=J.replace(/<\/script([^>]*)>/gi,"");J=J.replace(/<script([^>]*)>/gi,"");J=J.replace(/<\/script([^>]*)>/gi,"");J=J.replace(/\r\n/g,"").replace(/\n/g,"").replace(/\r/g,"");J=J.replace(new RegExp("]*)>(.*?)","gi"),"");J=J.replace(//g,"\n");return J;},cleanHTML:function(M){if(!M){M=this.getEditorHTML();}var K=this.get("markup");M=this.pre_filter_linebreaks(M,K);M=this.filter_msword(M);M=M.replace(/]*)\/>/gi,"");M=M.replace(/]*)>/gi,"");M=M.replace(/]*)\/>/gi,"");M=M.replace(/]*)>/gi,"");M=M.replace(/]*)>/gi,"");M=M.replace(/<\/ul>/gi,"");M=M.replace(/]*)>/gi,"");M=M.replace(/<\/blockquote>/gi,"");M=M.replace(/]*)>/gi,"");M=M.replace(/<\/embed>/gi,"");if((K=="semantic")||(K=="xhtml")){M=M.replace(/]*)?>/gi,"");M=M.replace(/<\/i>/gi,"");M=M.replace(/]*)?>/gi,"");M=M.replace(/<\/b>/gi,"");}M=M.replace(/_moz_dirty=""/gi,"");M=M.replace(//gi,"/span>");if(this.browser.ie){M=M.replace(/text-decoration/gi,"text-decoration");M=M.replace(/font-weight/gi,"font-weight");M=M.replace(/_width="([^>]*)"/gi,"");M=M.replace(/_height="([^>]*)"/gi,"");var L=this._baseHREF.replace(/\//gi,"\\/"),N=new RegExp('src="'+L,"gi");M=M.replace(N,'src="');}M=M.replace(//gi,"");M=M.replace(//gi,"");if((K=="semantic")||(K=="xhtml")||(K=="css")){M=M.replace(new RegExp(']*)face="([^>]*)">(.*?)',"gi"),'$3');M=M.replace(/([^>]*)',"gi"),"$1");M=M.replace(new RegExp('([^>]*)',"gi"),"$1");}M=M.replace(/\/u>/gi,"/span>");if(K=="css"){M=M.replace(/]*)>/gi,"");M=M.replace(/<\/em>/gi,"");M=M.replace(/]*)>/gi,"");M=M.replace(/<\/strong>/gi,"");M=M.replace(//gi,"/span>");M=M.replace(//gi,"/span>");}M=M.replace(/ /gi," ");}else{M=M.replace(//gi,"/u>");}M=M.replace(/]*)>/gi,"");M=M.replace(/\/ol>/gi,"/ol>");M=M.replace(/
                  • /gi,"/li>");M=this.filter_safari(M);M=this.filter_internals(M);M=this.filter_all_rgb(M);M=this.post_filter_linebreaks(M,K);if(K=="xhtml"){M=M.replace(/]*)>/g,"");M=M.replace(/]*)>/g,"");}else{M=M.replace(/]*)>/g,"");M=M.replace(/]*)>/g,"");}M=M.replace(/]*)>/g,"");M=M.replace(/<\/YUI_UL>/g,"
                  ");M=this.filter_invalid_lists(M);M=M.replace(/]*)>/g,"");M=M.replace(/<\/YUI_BQ>/g,"");M=M.replace(/]*)>/g,"");M=M.replace(/<\/YUI_EMBED>/g,"");M=M.replace(/ & /gi," YUI_AMP ");M=M.replace(/ &/gi," YUI_AMP_F ");M=M.replace(/& /gi," YUI_AMP_R ");M=M.replace(/&/gi,"&");M=M.replace(/ YUI_AMP /gi," & ");M=M.replace(/ YUI_AMP_F /gi," &");M=M.replace(/ YUI_AMP_R /gi,"& ");M=A.lang.trim(M);if(this.get("removeLineBreaks")){M=M.replace(/\n/g,"").replace(/\r/g,"");M=M.replace(/ /gi," ");}for(var J in this.invalidHTML){if(A.lang.hasOwnProperty(this.invalidHTML,J)){if(H.isObject(J)&&J.keepContents){M=M.replace(new RegExp("<"+J+"([^>]*)>(.*?)","gi"),"$1");}else{M=M.replace(new RegExp("<"+J+"([^>]*)>(.*?)","gi"),"");}}}this.fireEvent("cleanHTML",{type:"cleanHTML",target:this,html:M});return M;},filter_msword:function(J){if(!this.get("filterWord")){return J;}J=J.replace(/\s*<\/o:p>/g,"");J=J.replace(/[\s\S]*?<\/o:p>/g," ");J=J.replace(/]*>[\s\S]*?<\/w:[^>]*>/gi,"");J=J.replace(/\s*mso-[^:]+:[^;"]+;?/gi,"");J=J.replace(/\s*MARGIN: 0cm 0cm 0pt\s*;/gi,"");J=J.replace(/\s*MARGIN: 0cm 0cm 0pt\s*"/gi,'"');J=J.replace(/\s*TEXT-INDENT: 0cm\s*;/gi,"");J=J.replace(/\s*TEXT-INDENT: 0cm\s*"/gi,'"');J=J.replace(/\s*PAGE-BREAK-BEFORE: [^\s;]+;?"/gi,'"');J=J.replace(/\s*FONT-VARIANT: [^\s;]+;?"/gi,'"');J=J.replace(/\s*tab-stops:[^;"]*;?/gi,"");J=J.replace(/\s*tab-stops:[^"]*/gi,"");J=J.replace(/<\\?\?xml[^>]*>/gi,"");J=J.replace(/<(\w[^>]*) lang=([^ |>]*)([^>]*)/gi,"<$1$3");J=J.replace(/<(\w[^>]*) language=([^ |>]*)([^>]*)/gi,"<$1$3");J=J.replace(/<(\w[^>]*) onmouseover="([^\"]*)"([^>]*)/gi,"<$1$3");J=J.replace(/<(\w[^>]*) onmouseout="([^\"]*)"([^>]*)/gi,"<$1$3");return J;},filter_invalid_lists:function(J){J=J.replace(/<\/li>\n/gi,"");J=J.replace(/<\/li>
                    /gi,"
                    1. ");J=J.replace(/<\/ol>/gi,"
                  1. ");J=J.replace(/<\/ol><\/li>\n/gi,"
                  ");J=J.replace(/<\/li>
                    /gi,"
                    • ");J=J.replace(/<\/ul>/gi,"
                  • ");J=J.replace(/<\/ul><\/li>\n?/gi,"
                  ");J=J.replace(/<\/li>/gi,"");J=J.replace(/<\/ol>/gi,"
              ");J=J.replace(/
                /gi,"
                  ");J=J.replace(/
                    /gi,"
                      ");return J;},filter_safari:function(J){if(this.browser.webkit){J=J.replace(/([^>])<\/span>/gi,"    ");J=J.replace(/Apple-style-span/gi,"");J=J.replace(/style="line-height: normal;"/gi,"");J=J.replace(/yui-wk-div/gi,"");J=J.replace(/yui-wk-p/gi,"");J=J.replace(/
                    • <\/li>/gi,"");J=J.replace(/
                    • <\/li>/gi,"");J=J.replace(/
                    • <\/li>/gi,"");if(this.get("ptags")){J=J.replace(/]*)>/g,""); +J=J.replace(/<\/div>/gi,"

                      ");}else{J=J.replace(/]*)>([ tnr]*)<\/div>/gi,"
                      ");J=J.replace(/<\/div>/gi,"");}}return J;},filter_internals:function(J){J=J.replace(/\r/g,"");J=J.replace(/<\/?(body|head|html)[^>]*>/gi,"");J=J.replace(/<\/li>/gi,"
                    • ");J=J.replace(/yui-tag-span/gi,"");J=J.replace(/yui-tag/gi,"");J=J.replace(/yui-non/gi,"");J=J.replace(/yui-img/gi,"");J=J.replace(/ tag="span"/gi,"");J=J.replace(/ class=""/gi,"");J=J.replace(/ style=""/gi,"");J=J.replace(/ class=" "/gi,"");J=J.replace(/ class=" "/gi,"");J=J.replace(/ target=""/gi,"");J=J.replace(/ title=""/gi,"");if(this.browser.ie){J=J.replace(/ class= /gi,"");J=J.replace(/ class= >/gi,"");}return J;},filter_all_rgb:function(N){var M=new RegExp("rgb\\s*?\\(\\s*?([0-9]+).*?,\\s*?([0-9]+).*?,\\s*?([0-9]+).*?\\)","gi");var J=N.match(M);if(H.isArray(J)){for(var L=0;L/gi,"");K=K.replace(/
                      /gi,"");}K=K.replace(/
                      /gi,"");K=K.replace(/
                      /gi,"");K=K.replace(//gi,"");K=K.replace(/
                      /gi,"");K=K.replace(/
                      <\/div>/gi,"");K=K.replace(/

                      ( | )<\/p>/g,"");K=K.replace(/


                       <\/p>/gi,"");K=K.replace(/

                       <\/p>/gi,"");K=K.replace(/$/,"");K=K.replace(/<\/p>/g,"

                      ");if(this.browser.ie){K=K.replace(/    /g,"\t");}return K;},post_filter_linebreaks:function(K,J){if(J=="xhtml"){K=K.replace(//g,"
                      ");}else{K=K.replace(//g,"
                      ");}return K;},clearEditorDoc:function(){this._getDoc().body.innerHTML=" ";},openWindow:function(J){},moveWindow:function(){},_closeWindow:function(){},closeWindow:function(){this.toolbar.resetAllButtons();this.focus();},destructor:function(){if(this._nodeChangeDelayTimer){clearTimeout(this._nodeChangeDelayTimer);}this.hide();A.log("Destroying Editor","warn","SimpleEditor");if(this.resize){A.log("Destroying Resize","warn","SimpleEditor");this.resize.destroy();}if(this.dd){A.log("Unreg DragDrop Instance","warn","SimpleEditor");this.dd.unreg();}if(this.get("panel")){A.log("Destroying Editor Panel","warn","SimpleEditor");this.get("panel").destroy();}this.saveHTML();this.toolbar.destroy();A.log("Restoring TextArea","info","SimpleEditor");this.setStyle("visibility","visible");this.setStyle("position","static");this.setStyle("top","");this.setStyle("left","");var J=this.get("element");this.get("element_cont").get("parentNode").replaceChild(J,this.get("element_cont").get("element"));this.get("element_cont").get("element").innerHTML="";this.set("handleSubmit",false);return true;},toString:function(){var J="SimpleEditor";return J;}});C.namespace("Editor");C.Editor.Simple=E;C.Editor.Info={_instances:{},blankImage:"",window:{},panel:null,getEditorById:function(J){if(!H.isString(J)){J=J.id;}if(this._instances[J]){return this._instances[J];}return false;},saveAll:function(L){var K,M,J=C.Editor.Info._instances;if(L){C.each(J,function(N,O){if(O.get("element").form&&(O.get("element").form==L)){O.saveHTML();}});}else{C.each(J,function(N){N.saveHTML();});}},toString:function(){var J=0;C.each(this._instances,function(){J++;});return"Editor Info ("+J+" registered intance"+((J>1)?"s":"")+")";}};},"gallery-2009.11.02-20",{requires:["node","gallery-port","substitute"]}); \ No newline at end of file diff --git a/build/gallery-simple-editor/gallery-simple-editor.js b/build/gallery-simple-editor/gallery-simple-editor.js new file mode 100644 index 0000000000..e5af3801a4 --- /dev/null +++ b/build/gallery-simple-editor/gallery-simple-editor.js @@ -0,0 +1,7037 @@ +YUI.add('gallery-simple-editor', function(Y) { + + + /** + * @description

                      Creates a basic custom Toolbar Button. Primarily used with the Rich Text Editor's Toolbar

                      Provides a toolbar button based on the button and menu widgets, <select> elements are used in place of menu's.

                      + * @class ToolbarButton + * @namespace YAHOO.widget + * @requires yahoo, dom, element, event + * @extends YAHOO.util.Element + * + * + * @constructor + * @param {String/HTMLElement} el The element to turn into a button. + * @param {Object} attrs Object liternal containing configuration parameters. + */ + + var ToolbarButton = function() { + ToolbarButton.superclass.constructor.apply(this, arguments); + }; + + ToolbarButton.NAME = 'ToolbarButton'; + + ToolbarButton.ATTRS = { + node: { + setter: function(node) { + var n = Y.get(node); + if (!n) { + Y.error('Invalid Node Given: ' + node); + } else { + n = n.item(0); + } + return n; + } + }, + id: { + }, + /** + * @attribute value + * @description The value of the button + * @type String + */ + value: { + }, + /** + * @attribute menu + * @description The menu attribute, see YAHOO.widget.Button + * @type Object + */ + menu: { + value: false + }, + /** + * @attribute type + * @description The type of button to create: push, menu, color, select, spin + * @type String + */ + type: { + value: 'push', + writeOnce: true + }, + /** + * @attribute disabled + * @description Set the button into a disabled state + * @type String + */ + disabled: { + value: false, + setter: function(disabled) { + if (disabled) { + this.get('node').addClass('yui-button-disabled'); + this.get('node').addClass('yui-' + this.get('type') + '-button-disabled'); + } else { + this.get('node').removeClass('yui-button-disabled'); + this.get('node').removeClass('yui-' + this.get('type') + '-button-disabled'); + } + //console.log(this.get('node'), ': ', disabled); + if (this.get('type') == 'select') { + this.get('node').set('disabled', disabled); + } + } + }, + /** + * @attribute label + * @description The text label for the button + * @type String + */ + label: { + value: 'LABEL', + setter: function(label) { + if (this.get('type') == 'push') { + this.get('node').query('a').set('innerHTML', label); + } + } + }, + /** + * @attribute title + * @description The title of the button + * @type String + */ + title: { + value: 'TITLE', + setter: function(title) { + this.get('node').set('title', title); + } + }, + + /** + * @config container + * @description The container that the button is rendered to, handled by Toolbar + * @type String + */ + container: { + value: null, + writeOnce: true, + setter: function(node) { + var n = Y.get(node); + if (!n) { + Y.error('Invalid Node Given: ' + node); + } else { + n = n.item(0); + } + return n; + } + } + }; + + Y.extend(ToolbarButton, Y.Base, { + /** + * @property buttonType + * @private + * @description Tells if the Button is a Rich Button or a Simple Button + */ + buttonType: 'normal', + /** + * @method _handleMouseOver + * @private + * @description Adds classes to the button elements on mouseover (hover) + */ + _handleMouseOver: function() { + if (!this.get('disabled')) { + this.get('node').addClass('yui-button-hover'); + this.get('node').addClass('yui-' + this.get('type') + '-button-hover'); + } + }, + /** + * @method _handleMouseOut + * @private + * @description Removes classes from the button elements on mouseout (hover) + */ + _handleMouseOut: function() { + this.get('node').removeClass('yui-button-hover'); + this.get('node').removeClass('yui-' + this.get('type') + '-button-hover'); + }, + /** + * @method checkValue + * @param {String} value The value of the option that we want to mark as selected + * @description Select an option by value + */ + checkValue: function(value) { + if (this.get('type') == 'select') { + var opt = this.get('node').get('options'); + opt.each(function(n, k) { + if (n.get('value') == value) { + this.get('node').set('selectedIndex', k); + } + }, this); + } + }, + /** + * @method initializer + * @description The ToolbarButton class's initialization method + */ + initializer: function() { + var id, i, el, menu, opt, html; + switch (this.get('type')) { + case 'select': + case 'menu': + el = Y.Node.create(''); + menu = this.get('menu'); + + for (i = 0; i < menu.length; i++) { + opt = Y.Node.create(''); + el.appendChild(opt); + if (menu[i].checked) { + opt.set('selected', true); + } + } + el.on('change', Y.bind(this._handleSelect, this)); + id = Y.stamp(el); + el.set('id', id); + this.set('node', el); + break; + default: + html = ''; + this.set('node', Y.Node.create(html)); + this.get('node').on('mouseover', Y.bind(this._handleMouseOver, this)); + this.get('node').on('mouseout', Y.bind(this._handleMouseOut, this)); + break; + } + id = Y.stamp(this.get('node')); + this.set('id', id); + this.get('node').set('id', id); + this.set('title', this.get('title')); + this.set('label', this.get('label')); + this.set('disabled', this.get('disabled')); + this.get('container').appendChild(this.get('node')); + + this.get('node').on('click', Y.bind(function(e) { + e.halt(); + }, this)); + }, + /** + * @private + * @method _handleSelect + * @description The event fired when a change event gets fired on a select element + * @param {Event} ev The change event. + */ + _handleSelect: function(e) { + this.fire('change', {type: 'change', value: e.target.get('value'), target: this.get('node') }); + }, + /** + * @method getMenu + * @description A stub function to mimic YAHOO.widget.Button's getMenu method + */ + getMenu: function() { + return this.get('menu'); + }, + destructor: function() { + }, + /** + * @method toString + * @description Returns a string representing the toolbar. + * @return {String} + */ + toString: function() { + return 'Toolbar.Button (' + this.get('node').get('id') + ')'; + } + + }); + + Y.namespace('Toolbar'); + Y.Toolbar.Button = ToolbarButton; + + /* + var Lang = Y.Lang, + Dom = Y.DOM, + Event = { + on: Y.Event.nativeAdd, + getTarget: function(ev) { + return ev.target; + }, + stopEvent: function(ev) { + ev.halt(); + } + }, + YAHOO = { + log: console.log, + lang: Lang, + util: { + Dom: Dom, + Event: Event + } + }; + */ + + var getButton = function(id) { + var button = id; + if (Y.Lang.isString(id)) { + button = this.getButtonById(id); + } + if (Y.Lang.isNumber(id)) { + button = this.getButtonByIndex(id); + } + if (id instanceof Y.Toolbar.Button) { + button = id; + } else { + button = this.getButtonByValue(id); + } + return button; + }; + + /** + * Provides a rich toolbar widget based on the button and menu widgets + * @constructor + * @class Toolbar + * @extends YAHOO.util.Element + * @param {String/HTMLElement} el The element to turn into a toolbar. + * @param {Object} attrs Object liternal containing configuration parameters. + */ + var Toolbar = function() { + + this._configuredButtons = []; + + Toolbar.superclass.constructor.apply(this, arguments); + + }; + + Toolbar.NAME = 'Toolbar'; + + Toolbar.ATTRS = { + node: { + setter: function(node) { + var n = Y.get(node); + if (!n) { + Y.error('Invalid Node Given: ' + node); + } else { + n = n.item(0); + } + return n; + } + }, + /** + * @attribute buttonType + * @description The buttonType to use (advanced or basic) + * @type String + */ + buttonType: { + value: 'basic', + writeOnce: true, + validator: function(type) { + switch (type) { + case 'advanced': + case 'basic': + return type; + } + return 'basic'; + }, + setter: function(type) { + this.buttonType = Y.Toolbar.Button; + } + }, + /** + * @attribute buttons + * @description Object specifying the buttons to include in the toolbar + * Example: + *
                      +        * {
                      +        *   { id: 'b3', type: 'button', label: 'Underline', value: 'underline' },
                      +        *   { type: 'separator' },
                      +        *   { id: 'b4', type: 'menu', label: 'Align', value: 'align',
                      +        *       menu: [
                      +        *           { text: "Left", value: 'alignleft' },
                      +        *           { text: "Center", value: 'aligncenter' },
                      +        *           { text: "Right", value: 'alignright' }
                      +        *       ]
                      +        *   }
                      +        * }
                      +        * 
                      + * @type Array + */ + + buttons: { + value: [] + }, + /** + * @config cont + * @description The container for the toolbar. + * @type HTMLElement + */ + cont: { + value: null + }, + id: { + value: null + }, + /** + * @attribute grouplabels + * @description Boolean indicating if the toolbar should show the group label's text string. + * @default true + * @type Boolean + */ + grouplabels: { + value: true, + setter: function(grouplabels) { + if (grouplabels) { + this.get('cont').removeClass(this.CLASS_PREFIX + '-nogrouplabels'); + } else { + this.get('cont').addClass(this.CLASS_PREFIX + '-nogrouplabels'); + } + } + }, + + /** + * @attribute titlebar + * @description Boolean indicating if the toolbar should have a titlebar. If + * passed a string, it will use that as the titlebar text + * @default false + * @type Boolean or String + */ + titlebar: { + value: false + }, + + /** + * @attribute collapse + * @description Boolean indicating if the the titlebar should have a collapse button. + * The collapse button will not remove the toolbar, it will minimize it to the titlebar + * @default false + * @type Boolean + */ + collapse: { + value: false + } + + }; + + Y.extend(Toolbar, Y.Base, { + fireEvent: function(type, args) { + if (args.target) { + delete args.target; + } + this.fire(type, args); + }, + _handleCollapse: function() { + var collapse = this.get('collapse'); + + if (this._titlebar) { + var collapseEl = null; + var el = this._titlebar.query('span.collapse'); + if (collapse) { + if (el) { + //There is already a collapse button + return true; + } + collapseEl = Y.Node.create('X'); + this._titlebar.appendChild(collapseEl); + collapseEl.on('click', Y.bind(function() { + if (this.get('cont').get('parentNode').hasClass('yui-toolbar-container-collapsed')) { + this.collapse(false); //Expand Toolbar + } else { + this.collapse(); //Collapse Toolbar + } + }, this)); + } else { + collapseEl = this._titlebar.query('span.collapse'); + if (collapseEl) { + if (this.get('cont').get('parentNode').hasClass('yui-toolbar-container-collapsed')) { + //We are closed, reopen the titlebar.. + this.collapse(false); //Expand Toolbar + } + collapseEl.get('parentNode').removeChild(collapseEl); + } + } + } + }, + _renderTitle: function() { + var titlebar = this.get('titlebar'); + if (titlebar) { + if (this._titlebar && this._titlebar.get('parentNode')) { + this._titlebar.get('parentNode').removeChild(this._titlebar); + } + this._titlebar = Y.Node.create('
                      '); + this._titlebar.on('focus', Y.bind(this._handleFocus, this)); + + if (Y.Lang.isString(titlebar)) { + var h2 = Y.Node.create('

                      ' + titlebar + '

                      '); + this._titlebar.appendChild(h2); + h2.get('firstChild').on('click', function(ev) { + ev.halt(); + }); + /* + Event.on([h2, h2.firstChild], 'focus', function() { + this._handleFocus(); + }, this, true); + */ + } + if (this.get('node').get('firstChild')) { + this.get('node').insertBefore(this._titlebar, this.get('node').get('firstChild')); + } else { + this.get('node').appendChild(this._titlebar); + } + if (this.get('collapse')) { + this.set('collapse', true); + } + } else if (this._titlebar) { + if (this._titlebar && this._titlebar.get('parentNode')) { + this._titlebar.get('parentNode').removeChild(this._titlebar); + } + } + }, + /** + * @protected + * @property _configuredButtons + * @type Array + */ + _configuredButtons: null, + /** + * @method _addMenuClasses + * @private + * @description This method is called from Menu's renderEvent to add a few more classes to the menu items + * @param {String} ev The event that fired. + * @param {Array} na Array of event information. + * @param {Object} o Button config object. + */ + _addMenuClasses: function(ev, na, o) { + Dom.addClass(this.element, 'yui-toolbar-' + o.get('value') + '-menu'); + if (Dom.hasClass(o._button.parentNode.parentNode, 'yui-toolbar-select')) { + Dom.addClass(this.element, 'yui-toolbar-select-menu'); + } + var items = this.getItems(); + for (var i = 0; i < items.length; i++) { + Dom.addClass(items[i].element, 'yui-toolbar-' + o.get('value') + '-' + ((items[i].value) ? items[i].value.replace(/ /g, '-').toLowerCase() : items[i]._oText.nodeValue.replace(/ /g, '-').toLowerCase())); + Dom.addClass(items[i].element, 'yui-toolbar-' + o.get('value') + '-' + ((items[i].value) ? items[i].value.replace(/ /g, '-') : items[i]._oText.nodeValue.replace(/ /g, '-'))); + } + }, + /** + * @property buttonType + * @description The default button to use + * @type Object + */ + buttonType: Y.Toolbar.Button, + /** + * @property dd + * @description The DragDrop instance associated with the Toolbar + * @type Object + */ + dd: null, + /** + * @property _colorData + * @description Object reference containing colors hex and text values. + * @type Object + */ + _colorData: { +/* {{{ _colorData */ + '#111111': 'Obsidian', + '#2D2D2D': 'Dark Gray', + '#434343': 'Shale', + '#5B5B5B': 'Flint', + '#737373': 'Gray', + '#8B8B8B': 'Concrete', + '#A2A2A2': 'Gray', + '#B9B9B9': 'Titanium', + '#000000': 'Black', + '#D0D0D0': 'Light Gray', + '#E6E6E6': 'Silver', + '#FFFFFF': 'White', + '#BFBF00': 'Pumpkin', + '#FFFF00': 'Yellow', + '#FFFF40': 'Banana', + '#FFFF80': 'Pale Yellow', + '#FFFFBF': 'Butter', + '#525330': 'Raw Siena', + '#898A49': 'Mildew', + '#AEA945': 'Olive', + '#7F7F00': 'Paprika', + '#C3BE71': 'Earth', + '#E0DCAA': 'Khaki', + '#FCFAE1': 'Cream', + '#60BF00': 'Cactus', + '#80FF00': 'Chartreuse', + '#A0FF40': 'Green', + '#C0FF80': 'Pale Lime', + '#DFFFBF': 'Light Mint', + '#3B5738': 'Green', + '#668F5A': 'Lime Gray', + '#7F9757': 'Yellow', + '#407F00': 'Clover', + '#8A9B55': 'Pistachio', + '#B7C296': 'Light Jade', + '#E6EBD5': 'Breakwater', + '#00BF00': 'Spring Frost', + '#00FF80': 'Pastel Green', + '#40FFA0': 'Light Emerald', + '#80FFC0': 'Sea Foam', + '#BFFFDF': 'Sea Mist', + '#033D21': 'Dark Forrest', + '#438059': 'Moss', + '#7FA37C': 'Medium Green', + '#007F40': 'Pine', + '#8DAE94': 'Yellow Gray Green', + '#ACC6B5': 'Aqua Lung', + '#DDEBE2': 'Sea Vapor', + '#00BFBF': 'Fog', + '#00FFFF': 'Cyan', + '#40FFFF': 'Turquoise Blue', + '#80FFFF': 'Light Aqua', + '#BFFFFF': 'Pale Cyan', + '#033D3D': 'Dark Teal', + '#347D7E': 'Gray Turquoise', + '#609A9F': 'Green Blue', + '#007F7F': 'Seaweed', + '#96BDC4': 'Green Gray', + '#B5D1D7': 'Soapstone', + '#E2F1F4': 'Light Turquoise', + '#0060BF': 'Summer Sky', + '#0080FF': 'Sky Blue', + '#40A0FF': 'Electric Blue', + '#80C0FF': 'Light Azure', + '#BFDFFF': 'Ice Blue', + '#1B2C48': 'Navy', + '#385376': 'Biscay', + '#57708F': 'Dusty Blue', + '#00407F': 'Sea Blue', + '#7792AC': 'Sky Blue Gray', + '#A8BED1': 'Morning Sky', + '#DEEBF6': 'Vapor', + '#0000BF': 'Deep Blue', + '#0000FF': 'Blue', + '#4040FF': 'Cerulean Blue', + '#8080FF': 'Evening Blue', + '#BFBFFF': 'Light Blue', + '#212143': 'Deep Indigo', + '#373E68': 'Sea Blue', + '#444F75': 'Night Blue', + '#00007F': 'Indigo Blue', + '#585E82': 'Dockside', + '#8687A4': 'Blue Gray', + '#D2D1E1': 'Light Blue Gray', + '#6000BF': 'Neon Violet', + '#8000FF': 'Blue Violet', + '#A040FF': 'Violet Purple', + '#C080FF': 'Violet Dusk', + '#DFBFFF': 'Pale Lavender', + '#302449': 'Cool Shale', + '#54466F': 'Dark Indigo', + '#655A7F': 'Dark Violet', + '#40007F': 'Violet', + '#726284': 'Smoky Violet', + '#9E8FA9': 'Slate Gray', + '#DCD1DF': 'Violet White', + '#BF00BF': 'Royal Violet', + '#FF00FF': 'Fuchsia', + '#FF40FF': 'Magenta', + '#FF80FF': 'Orchid', + '#FFBFFF': 'Pale Magenta', + '#4A234A': 'Dark Purple', + '#794A72': 'Medium Purple', + '#936386': 'Cool Granite', + '#7F007F': 'Purple', + '#9D7292': 'Purple Moon', + '#C0A0B6': 'Pale Purple', + '#ECDAE5': 'Pink Cloud', + '#BF005F': 'Hot Pink', + '#FF007F': 'Deep Pink', + '#FF409F': 'Grape', + '#FF80BF': 'Electric Pink', + '#FFBFDF': 'Pink', + '#451528': 'Purple Red', + '#823857': 'Purple Dino', + '#A94A76': 'Purple Gray', + '#7F003F': 'Rose', + '#BC6F95': 'Antique Mauve', + '#D8A5BB': 'Cool Marble', + '#F7DDE9': 'Pink Granite', + '#C00000': 'Apple', + '#FF0000': 'Fire Truck', + '#FF4040': 'Pale Red', + '#FF8080': 'Salmon', + '#FFC0C0': 'Warm Pink', + '#441415': 'Sepia', + '#82393C': 'Rust', + '#AA4D4E': 'Brick', + '#800000': 'Brick Red', + '#BC6E6E': 'Mauve', + '#D8A3A4': 'Shrimp Pink', + '#F8DDDD': 'Shell Pink', + '#BF5F00': 'Dark Orange', + '#FF7F00': 'Orange', + '#FF9F40': 'Grapefruit', + '#FFBF80': 'Canteloupe', + '#FFDFBF': 'Wax', + '#482C1B': 'Dark Brick', + '#855A40': 'Dirt', + '#B27C51': 'Tan', + '#7F3F00': 'Nutmeg', + '#C49B71': 'Mustard', + '#E1C4A8': 'Pale Tan', + '#FDEEE0': 'Marble' +/* }}} */ + }, + /** + * @property _colorPicker + * @description The HTML Element containing the colorPicker + * @type HTMLElement + */ + _colorPicker: null, + /** + * @property STR_COLLAPSE + * @description String for Toolbar Collapse Button + * @type String + */ + STR_COLLAPSE: 'Collapse Toolbar', + /** + * @property STR_EXPAND + * @description String for Toolbar Collapse Button - Expand + * @type String + */ + STR_EXPAND: 'Expand Toolbar', + /** + * @property STR_SPIN_LABEL + * @description String for spinbutton dynamic label. Note the {VALUE} will be replaced with YAHOO.lang.substitute + * @type String + */ + STR_SPIN_LABEL: 'Spin Button with value {VALUE}. Use Control Shift Up Arrow and Control Shift Down arrow keys to increase or decrease the value.', + /** + * @property STR_SPIN_UP + * @description String for spinbutton up + * @type String + */ + STR_SPIN_UP: 'Click to increase the value of this input', + /** + * @property STR_SPIN_DOWN + * @description String for spinbutton down + * @type String + */ + STR_SPIN_DOWN: 'Click to decrease the value of this input', + /** + * @property _titlebar + * @description Object reference to the titlebar + * @type HTMLElement + */ + _titlebar: null, + /** + * @property browser + * @description Standard browser detection + * @type Object + */ + browser: Y.UA, + /** + * @protected + * @property _buttonList + * @description Internal property list of current buttons in the toolbar + * @type Array + */ + _buttonList: null, + /** + * @protected + * @property _buttonGroupList + * @description Internal property list of current button groups in the toolbar + * @type Array + */ + _buttonGroupList: null, + /** + * @protected + * @property _sep + * @description Internal reference to the separator HTML Element for cloning + * @type HTMLElement + */ + _sep: null, + /** + * @protected + * @property _sepCount + * @description Internal refernce for counting separators, so we can give them a useful class name for styling + * @type Number + */ + _sepCount: null, + /** + * @protected + * @property draghandle + * @type HTMLElement + */ + _dragHandle: null, + /** + * @protected + * @property _toolbarConfigs + * @type Object + */ + _toolbarConfigs: { + renderer: true + }, + /** + * @protected + * @property CLASS_CONTAINER + * @description Default CSS class to apply to the toolbar container element + * @type String + */ + CLASS_CONTAINER: 'yui-toolbar-container', + /** + * @protected + * @property CLASS_DRAGHANDLE + * @description Default CSS class to apply to the toolbar's drag handle element + * @type String + */ + CLASS_DRAGHANDLE: 'yui-toolbar-draghandle', + /** + * @protected + * @property CLASS_SEPARATOR + * @description Default CSS class to apply to all separators in the toolbar + * @type String + */ + CLASS_SEPARATOR: 'yui-toolbar-separator', + /** + * @protected + * @property CLASS_DISABLED + * @description Default CSS class to apply when the toolbar is disabled + * @type String + */ + CLASS_DISABLED: 'yui-toolbar-disabled', + /** + * @protected + * @property CLASS_PREFIX + * @description Default prefix for dynamically created class names + * @type String + */ + CLASS_PREFIX: 'yui-toolbar', + /** + * @method init + * @description The Toolbar class's initialization method + */ + initializer: function() { + var fs = Y.Node.create('
                      Toolbar
                      '); + this.set('id', this.get('node').get('id')); + this.get('node').appendChild(fs); + this.set('cont', fs.query('div.yui-toolbar-subcont')); + this.get('node').set('tabIndex', '-1'); + this.get('node').addClass(this.CLASS_CONTAINER); + + this._renderTitle(); + this._handleCollapse(); + this.on('titlebarChange', Y.bind(this._renderTitle, this)); + this.on('collapseChange', Y.bind(this._handleCollapse, this)); + + + this.processButtons(); + }, + processButtons: function() { + var button, buttons, len, b, data = this.get('buttons'); + Y.each(data, function(i, k) { + if (i.type == 'separator') { + this.addSeparator(); + } else if (i.group !== undefined) { + buttons = this.addButtonGroup(i); + if (buttons) { + len = buttons.length; + for(b = 0; b < len; b++) { + if (buttons[b]) { + this._configuredButtons[this._configuredButtons.length] = buttons[b].id; + } + } + } + this.set('buttons', data); + } else { + button = this.addButton(i); + if (button) { + this._configuredButtons[this._configuredButtons.length] = button.id; + } + } + }, this); + }, + /** + * @method initAttributes + * @description Initializes all of the configuration attributes used to create + * the toolbar. + * @param {Object} attr Object literal specifying a set of + * configuration attributes used to create the toolbar. + */ + initAttributes: function(attr) { + YAHOO.widget.Toolbar.superclass.initAttributes.call(this, attr); + + + /** + * @attribute disabled + * @description Boolean indicating if the toolbar should be disabled. It will also disable the draggable attribute if it is on. + * @default false + * @type Boolean + */ + this.setAttributeConfig('disabled', { + value: false, + method: function(disabled) { + if (this.get('disabled') === disabled) { + return false; + } + if (disabled) { + this.addClass(this.CLASS_DISABLED); + this.set('draggable', false); + this.disableAllButtons(); + } else { + this.removeClass(this.CLASS_DISABLED); + if (this._configs.draggable._initialConfig.value) { + //Draggable by default, set it back + this.set('draggable', true); + } + this.resetAllButtons(); + } + } + }); + + + /** + * @attribute draggable + * @description Boolean indicating if the toolbar should be draggable. + * @default false + * @type Boolean + */ + + this.setAttributeConfig('draggable', { + value: (attr.draggable || false), + method: function(draggable) { + if (draggable && !this.get('titlebar')) { + YAHOO.log('Dragging enabled', 'info', 'Toolbar'); + if (!this._dragHandle) { + this._dragHandle = document.createElement('SPAN'); + this._dragHandle.innerHTML = '|'; + this._dragHandle.setAttribute('title', 'Click to drag the toolbar'); + this._dragHandle.id = this.get('id') + '_draghandle'; + Dom.addClass(this._dragHandle, this.CLASS_DRAGHANDLE); + if (this.get('cont').hasChildNodes()) { + this.get('cont').insertBefore(this._dragHandle, this.get('cont').firstChild); + } else { + this.get('cont').appendChild(this._dragHandle); + } + this.dd = new YAHOO.util.DD(this.get('id')); + this.dd.setHandleElId(this._dragHandle.id); + + } + } else { + YAHOO.log('Dragging disabled', 'info', 'Toolbar'); + if (this._dragHandle) { + this._dragHandle.parentNode.removeChild(this._dragHandle); + this._dragHandle = null; + this.dd = null; + } + } + if (this._titlebar) { + if (draggable) { + this.dd = new YAHOO.util.DD(this.get('id')); + this.dd.setHandleElId(this._titlebar); + Dom.addClass(this._titlebar, 'draggable'); + } else { + Dom.removeClass(this._titlebar, 'draggable'); + if (this.dd) { + this.dd.unreg(); + this.dd = null; + } + } + } + }, + validator: function(value) { + var ret = true; + if (!YAHOO.util.DD) { + ret = false; + } + return ret; + } + }); + + }, + /** + * @method addButtonGroup + * @description Add a new button group to the toolbar. (uses addButton) + * @param {Object} oGroup Object literal reference to the Groups Config (contains an array of button configs as well as the group label) + */ + addButtonGroup: function(oGroup) { + + if (!this.get('node').hasClass(this.CLASS_PREFIX + '-grouped')) { + this.get('node').addClass(this.CLASS_PREFIX + '-grouped'); + } + var div = Y.Node.create('
                      '); + if (oGroup.label) { + var label = Y.Node.create('

                      ' + oGroup.label + '

                      '); + div.appendChild(label); + } + if (!this.get('grouplabels')) { + this.get('cont').addClass(this.CLASS_PREFIX, '-nogrouplabels'); + } + + this.get('cont').appendChild(div); + + //For accessibility, let's put all of the group buttons in an Unordered List + var ul = Y.Node.create('
                        '); + div.appendChild(ul); + + if (!this._buttonGroupList) { + this._buttonGroupList = {}; + } + + this._buttonGroupList[oGroup.group] = ul; + + + //An array of the button ids added to this group + //This is used for destruction later... + var addedButtons = [], + button; + + Y.each(oGroup.buttons, function(i) { + var li = Y.Node.create('
                      • '); + ul.appendChild(li); + if ((i.type !== undefined) && i.type == 'separator') { + this.addSeparator(li); + } else { + i.container = li; + button = this.addButton(i); + if (button) { + addedButtons[addedButtons.length] = button.id; + } + } + }, this); + + return addedButtons; + }, + /** + * @method addButtonToGroup + * @description Add a new button to a toolbar group. Buttons supported: + * push, split, menu, select, color, spin + * @param {Object} oButton Object literal reference to the Button's Config + * @param {String} group The Group identifier passed into the initial config + * @param {HTMLElement} after Optional HTML element to insert this button after in the DOM. + */ + addButtonToGroup: function(oButton, group, after) { + var groupCont = this._buttonGroupList[group], + li = document.createElement('li'); + + li.className = this.CLASS_PREFIX + '-groupitem'; + oButton.container = li; + this.addButton(oButton, after); + groupCont.appendChild(li); + }, + /** + * @method addButton + * @description Add a new button to the toolbar. Buttons supported: + * push, split, menu, select, color, spin + * @param {Object} oButton Object literal reference to the Button's Config + * @param {HTMLElement} after Optional HTML element to insert this button after in the DOM. + */ + addButton: function(oButton, after) { + if (!this._buttonList) { + this._buttonList = []; + } + if (!oButton.container) { + oButton.container = this.get('cont'); + } + /* + if ((oButton.type == 'menu') || (oButton.type == 'split') || (oButton.type == 'select')) { + if (Y.Lang.isArray(oButton.menu)) { + for (var i in oButton.menu) { + if (Lang.hasOwnProperty(oButton.menu, i)) { + var funcObject = { + fn: function(ev, x, oMenu) { + if (!oButton.menucmd) { + oButton.menucmd = oButton.value; + } + oButton.value = ((oMenu.value) ? oMenu.value : oMenu._oText.nodeValue); + }, + scope: this + }; + oButton.menu[i].onclick = funcObject; + } + } + } + } + */ + var _oButton = {}, skip = false; + Y.each(oButton, function(v, k) { + if (!this._toolbarConfigs[k]) { + _oButton[k] = v; + } + }, this); + if (oButton.type == 'select') { + _oButton.type = 'menu'; + } + if (oButton.type == 'spin') { + _oButton.type = 'push'; + } + if (_oButton.type == 'color') { + if (Y.Overlay) { + _oButton = this._makeColorButton(_oButton); + } else { + skip = true; + } + } + if (_oButton.menu) { + if ((Y.Overlay) && (oButton.menu instanceof Y.Overlay)) { + oButton.menu.on('visibleChange', function() { + this._button = _oButton; + }); + } else { + /* + for (var m = 0; m < _oButton.menu.length; m++) { + if (!_oButton.menu[m].value) { + _oButton.menu[m].value = _oButton.menu[m].text; + } + } + if (this.browser.webkit) { + _oButton.focusmenu = false; + } + */ + } + } + if (skip) { + oButton = false; + } else { + //Add to .get('buttons') manually + //this._configs.buttons.value[this._configs.buttons.value.length] = oButton; + var tmp = new this.buttonType(_oButton); + tmp.get('node').set('tabIndex', '-1'); + tmp.get('node').set('role', 'button'); + if (!_oButton.id || !oButton.id) { + oButton.id = tmp.get('id'); + _oButton.id = tmp.get('id'); + } + //tmp._selected = true; + + if (this.get('disabled')) { + //Toolbar is disabled, disable the new button too! + tmp.set('disabled', true); + } + /* + if (after) { + var el = tmp.get('element'); + var nextSib = null; + if (after.get) { + nextSib = after.get('element').nextSibling; + } else if (after.nextSibling) { + nextSib = after.nextSibling; + } + if (nextSib) { + nextSib.parentNode.insertBefore(el, nextSib); + } + } + */ + tmp.get('node').addClass(this.CLASS_PREFIX + '-' + tmp.get('value')); + + var icon = Y.Node.create(''); + tmp.get('node').insertBefore(icon, tmp.get('node').get('firstChild')); + /* + if (tmp._button.tagName.toLowerCase() == 'button') { + tmp.get('element').setAttribute('unselectable', 'on'); + //Replace the Button HTML Element with an a href if it exists + var a = document.createElement('a'); + a.innerHTML = tmp._button.innerHTML; + a.href = '#'; + a.tabIndex = '-1'; + Event.on(a, 'click', function(ev) { + Event.stopEvent(ev); + }); + tmp._button.parentNode.replaceChild(a, tmp._button); + tmp._button = a; + } + */ + /* + if (oButton.type == 'select') { + if (tmp._button.tagName.toLowerCase() == 'select') { + icon.parentNode.removeChild(icon); + var iel = tmp._button, + parEl = tmp.get('element'); + parEl.parentNode.replaceChild(iel, parEl); + //The 'element' value is currently the orphaned element + //In order for "destroy" to execute we need to get('element') to reference the correct node. + //I'm not sure if there is a direct approach to setting this value. + tmp._configs.element.value = iel; + } else { + //Don't put a class on it if it's a real select element + tmp.addClass(this.CLASS_PREFIX + '-select'); + } + } + */ + if (oButton.type == 'spin') { + if (!Y.Lang.isArray(oButton.range)) { + oButton.range = [ 10, 100 ]; + } + this._makeSpinButton(tmp, oButton); + } + tmp.get('node').set('title', tmp.get('label')); + if (oButton.type != 'spin') { + if ((Y.Overlay) && (_oButton.menu instanceof Y.Overlay)) { + var showPicker = function(ev) { + var exec = true; + if (ev.keyCode && (ev.keyCode == 9)) { + exec = false; + } + if (exec) { + if (this._colorPicker) { + this._colorPicker._button = oButton.value; + } + tmp.getMenu().set('visible', !tmp.getMenu().get('visible')); + } + ev.halt(); + }; + tmp.get('node').on('mousedown', Y.bind(showPicker, this)); + tmp.get('node').on('keydown', Y.bind(showPicker, this)); + + } else if ((oButton.type != 'menu') && (oButton.type != 'select')) { + tmp.get('node').on('keypress', Y.bind(this._buttonClick, this)); + tmp.get('node').on('mousedown', Y.bind(function(ev) { + ev.halt(); + this._buttonClick(ev, oButton); + }, this)); + tmp.get('node').on('click', function(ev) { + ev.halt(); + }); + } else { + + //Stop the mousedown event so we can trap the selection in the editor! + tmp.on('mousedown', function(ev) { + ev.halt(); + }); + tmp.on('click', function(ev) { + ev.halt(); + }); + tmp.on('change', function(ev) { + if (!oButton.menucmd) { + oButton.menucmd = oButton.value; + } + oButton.value = ev.value; + this._buttonClick(ev, oButton); + }, this, true); + } + /* + var self = this; + //Hijack the mousedown event in the menu and make it fire a button click.. + + tmp.on('appendTo', function() { + var tmp = this; + if (tmp.getMenu() && tmp.getMenu().mouseDownEvent) { + tmp.getMenu().mouseDownEvent.subscribe(function(ev, args) { + YAHOO.log('mouseDownEvent', 'warn', 'Toolbar'); + var oMenu = args[1]; + YAHOO.util.Event.stopEvent(args[0]); + tmp._onMenuClick(args[0], tmp); + if (!oButton.menucmd) { + oButton.menucmd = oButton.value; + } + oButton.value = ((oMenu.value) ? oMenu.value : oMenu._oText.nodeValue); + self._buttonClick.call(self, args[1], oButton); + tmp._hideMenu(); + return false; + }); + tmp.getMenu().clickEvent.subscribe(function(ev, args) { + YAHOO.log('clickEvent', 'warn', 'Toolbar'); + YAHOO.util.Event.stopEvent(args[0]); + }); + tmp.getMenu().mouseUpEvent.subscribe(function(ev, args) { + YAHOO.log('mouseUpEvent', 'warn', 'Toolbar'); + YAHOO.util.Event.stopEvent(args[0]); + }); + } + }); + */ + } else { + //Stop the mousedown event so we can trap the selection in the editor! + tmp.on('mousedown', function(ev) { + ev.halt(); + }); + tmp.on('click', function(ev) { + ev.halt(); + }); + } + if (this.browser.ie) { + /* + //Add a couple of new events for IE + tmp.DOM_EVENTS.focusin = true; + tmp.DOM_EVENTS.focusout = true; + + //Stop them so we don't loose focus in the Editor + tmp.on('focusin', function(ev) { + YAHOO.util.Event.stopEvent(ev); + }, oButton, this); + + tmp.on('focusout', function(ev) { + YAHOO.util.Event.stopEvent(ev); + }, oButton, this); + tmp.on('click', function(ev) { + YAHOO.util.Event.stopEvent(ev); + }, oButton, this); + */ + } + if (this.browser.webkit) { + //This will keep the document from gaining focus and the editor from loosing it.. + //Forcefully remove the focus calls in button! + //tmp.hasFocus = function() { + // return true; + //}; + } + this._buttonList[this._buttonList.length] = tmp; + /* + if ((oButton.type == 'menu') || (oButton.type == 'split') || (oButton.type == 'select')) { + if (Lang.isArray(oButton.menu)) { + YAHOO.log('Button type is (' + oButton.type + '), doing extra renderer work.', 'info', 'Toolbar'); + var menu = tmp.getMenu(); + if (menu && menu.renderEvent) { + menu.renderEvent.subscribe(this._addMenuClasses, tmp); + if (oButton.renderer) { + menu.renderEvent.subscribe(oButton.renderer, tmp); + } + } + } + } + */ + } + return oButton; + }, + /** + * @method addSeparator + * @description Add a new button separator to the toolbar. + * @param {HTMLElement} cont Optional HTML element to insert this button into. + * @param {HTMLElement} after Optional HTML element to insert this button after in the DOM. + */ + addSeparator: function(cont, after) { + var sepCont = ((cont) ? cont : this.get('cont')); + if (this._sepCount === null) { + this._sepCount = 0; + } + if (!this._sep) { + this._sep = Y.Node.create('|'); + } + var _sep = this._sep.cloneNode(true); + this._sepCount++; + + _sep.addClass(this.CLASS_SEPARATOR + '-' + this._sepCount); + if (after) { + var nextSib = null; + if (after.get) { + nextSib = after.get('element').nextSibling; + } else if (after.nextSibling) { + nextSib = after.nextSibling; + } else { + nextSib = after; + } + if (nextSib) { + if (nextSib == after) { + nextSib.parentNode.appendChild(_sep); + } else { + nextSib.parentNode.insertBefore(_sep, nextSib); + } + } + } else { + sepCont.appendChild(_sep); + } + return _sep; + }, + /** + * @method _createColorPicker + * @private + * @description Creates the core DOM reference to the color picker menu item. + * @param {String} id the id of the toolbar to prefix this DOM container with. + */ + _createColorPicker: function(id) { + var c = Y.get('#' + id + '_colors'); + if (c) { + c.get('parentNode').removeChild(c); + } + var picker = Y.Node.create(''); + Y.get('body').appendChild(picker); + + this._colorPicker = picker; + + var html = ''; + Y.each(this._colorData, function(v, k) { + html += '' + k.replace('#', '') + ''; + }, this); + html += 'X'; + picker.set('innerHTML', html); + + picker.on('mouseover', Y.bind(function(ev) { + var picker = this._colorPicker; + var em = picker.query('em'); + var strong = picker.query('strong'); + var tar = ev.target; + if (tar.get('tagName').toLowerCase() == 'a') { + em.setStyle('backgroundColor', tar.getStyle('backgroundColor')); + strong.set('innerHTML', this._colorData['#' + tar.get('innerHTML')] + '
                        ' + tar.get('innerHTML')); + } + }, this)); + picker.on('focus', function(ev) { + ev.halt(); + }); + picker.on('click', function(ev) { + ev.halt(); + }); + picker.on('mousedown', Y.bind(function(ev) { + ev.halt(); + var tar = ev.target, data; + if (tar.get('tagName').toLowerCase() == 'a') { + data = tar.get('innerHTML'); + var retVal = this.fire('colorPickerClicked', { type: 'colorPickerClicked', button: this._colorPicker._button, color: data, colorName: this._colorData['#' + data] } ); + var info = { + color: data, + colorName: this._colorData['#' + data], + value: this._colorPicker._button + }; + + this.fire('buttonClick', { type: 'buttonClick', button: info }); + this.getButtonByValue(this._colorPicker._button).getMenu().hide(); + this._colorPicker.setStyle('display', 'none'); + } + }, this)); + }, + /** + * @method _resetColorPicker + * @private + * @description Clears the currently selected color or mouseover color in the color picker. + */ + _resetColorPicker: function() { + var em = this._colorPicker.query('em'); + var strong = this._colorPicker.query('strong'); + em.setStyle('backgroundColor', 'transparent'); + strong.set('innerHTML', ''); + }, + /** + * @method _makeColorButton + * @private + * @description Called to turn a "color" button into a menu button with an Overlay for the menu. + * @param {Object} _oButton YAHOO.widget.ToolbarButton reference + */ + _makeColorButton: function(_oButton) { + if (!this._colorPicker) { + this._createColorPicker(this.get('id')); + } + _oButton.type = 'color'; + _oButton.menu = new Y.Overlay({ + contentBox: '#' + this.get('id') + '_' + _oButton.value + '_menu', + bodyContent: 'FOO', + visible: false, + zIndex: 999 + }); + _oButton.menu.render(this.get('cont')); + _oButton.menu.set('visible', false); + + _oButton.menu.get('contentBox').addClass('yui-button-menu'); + _oButton.menu.get('contentBox').addClass('yui-color-button-menu'); + + _oButton.menu.after('visibleChange', Y.bind(function() { + _oButton.menu.set('align', { + node: this.getButtonById(_oButton.id).get('node'), + points: ['tl', 'bl'] + }); + //Move the DOM reference of the color picker to the Overlay that we are about to show. + this._resetColorPicker(); + var _p = this._colorPicker; + if (_p.get('parentNode')) { + _p.get('parentNode').removeChild(_p); + } + _oButton.menu.get('contentBox').query('.yui-widget-bd').set('innerHTML', ''); + _oButton.menu.get('contentBox').query('.yui-widget-bd').appendChild(_p); + this._colorPicker.setStyle('display', 'block'); + }, this)); + + return _oButton; + }, + /** + * @private + * @method _makeSpinButton + * @description Create a button similar to an OS Spin button.. It has an up/down arrow combo to scroll through a range of int values. + * @param {Object} _button YAHOO.widget.ToolbarButton reference + * @param {Object} oButton Object literal containing the buttons initial config + */ + _makeSpinButton: function(_button, oButton) { + _button.get('node').addClass(this.CLASS_PREFIX + '-spinbutton'); + var self = this, + _par = _button.get('node'), //parentNode of Button Element for appending child + range = oButton.range, + _b1 = Y.Node.create('' + this.STR_SPIN_UP + ''), + _b2 = Y.Node.create('' + this.STR_SPIN_DOWN + ''); + + //Append them to the container + _par.appendChild(_b1); + _par.appendChild(_b2); + + + var label = Y.Lang.substitute(this.STR_SPIN_LABEL, { VALUE: _button.get('label') }); + _button.set('title', label); + + + var cleanVal = function(value) { + value = ((value < range[0]) ? range[0] : value); + value = ((value > range[1]) ? range[1] : value); + return value; + }; + + var br = this.browser; + var tbar = false; + var strLabel = this.STR_SPIN_LABEL; + if (this._titlebar && this._titlebar.get('firstChild')) { + tbar = this._titlebar.get('firstChild'); + } + + var _intUp = function(ev) { + ev.halt(); + if (!_button.get('disabled') && (ev.keyCode != 9)) { + var value = parseInt(_button.get('label'), 10); + value++; + value = cleanVal(value); + _button.set('label', ''+value); + var label = Y.Lang.substitute(strLabel, { VALUE: _button.get('label') }); + _button.set('title', label); + self._buttonClick(ev, oButton); + } + }; + + var _intDown = function(ev) { + ev.halt(); + if (!_button.get('disabled') && (ev.keyCode != 9)) { + var value = parseInt(_button.get('label'), 10); + value--; + value = cleanVal(value); + + _button.set('label', ''+value); + var label = Y.Lang.substitute(strLabel, { VALUE: _button.get('label') }); + _button.set('title', label); + self._buttonClick(ev, oButton); + } + }; + + var _intKeyUp = function(ev) { + if (ev.keyCode == 38) { + _intUp(ev); + } else if (ev.keyCode == 40) { + _intDown(ev); + } else if (ev.keyCode == 107 && ev.shiftKey) { //Plus Key + _intUp(ev); + } else if (ev.keyCode == 109 && ev.shiftKey) { //Minus Key + _intDown(ev); + } + }; + + //Handle arrow keys.. + _button.on('keydown', Y.bind(_intKeyUp, this)); + + //Listen for the click on the up button and act on it + //Listen for the click on the down button and act on it + _b1.on('mousedown',function(ev) { ev.halt(); }); + _b2.on('mousedown',function(ev) { ev.halt(); }); + _b1.on('click', Y.bind(_intUp, this)); + _b2.on('click', Y.bind(_intDown, this)); + }, + /** + * @protected + * @method _buttonClick + * @description Click handler for all buttons in the toolbar. + * @param {String} ev The event that was passed in. + * @param {Object} info Object literal of information about the button that was clicked. + */ + _buttonClick: function(ev, info) { + var doEvent = true; + + if (ev && ev.type == 'keypress') { + if (ev.keyCode == 9) { + doEvent = false; + } else if ((ev.keyCode === 13) || (ev.keyCode === 0) || (ev.keyCode === 32)) { + } else { + doEvent = false; + } + } + + if (doEvent) { + var fireNextEvent = true, + retValue = false; + + info.isSelected = this.isSelected(info.id); + + if (info.value) { + retValue = this.fire(info.value + 'Click', { type: info.value + 'Click', target: this.get('element'), button: info }); + if (retValue === false) { + fireNextEvent = false; + } + } + + if (info.menucmd && fireNextEvent) { + retValue = this.fire(info.menucmd + 'Click', { type: info.menucmd + 'Click', target: this.get('element'), button: info }); + if (retValue === false) { + fireNextEvent = false; + } + } + if (fireNextEvent) { + this.fire('buttonClick', { type: 'buttonClick', target: this.get('element'), button: info }); + } + + if (info.type == 'select') { + var button = this.getButtonById(info.id); + if (button.buttonType == 'rich') { + var txt = info.value; + for (var i = 0; i < info.menu.length; i++) { + if (info.menu[i].value == info.value) { + txt = info.menu[i].text; + break; + } + } + button.set('label', '' + txt + ''); + var _items = button.getMenu().getItems(); + for (var m = 0; m < _items.length; m++) { + if (_items[m].value.toLowerCase() == info.value.toLowerCase()) { + _items[m].cfg.setProperty('checked', true); + } else { + _items[m].cfg.setProperty('checked', false); + } + } + } + } + if (ev) { + ev.halt(); + } + } + }, + /** + * @private + * @property _keyNav + * @description Flag to determine if the arrow nav listeners have been attached + * @type Boolean + */ + _keyNav: null, + /** + * @private + * @property _navCounter + * @description Internal counter for walking the buttons in the toolbar with the arrow keys + * @type Number + */ + _navCounter: null, + /** + * @private + * @method _navigateButtons + * @description Handles the navigation/focus of toolbar buttons with the Arrow Keys + * @param {Event} ev The Key Event + */ + _navigateButtons: function(ev) { + switch (ev.keyCode) { + case 37: + case 39: + if (ev.keyCode == 37) { + this._navCounter--; + } else { + this._navCounter++; + } + if (this._navCounter > (this._buttonList.length - 1)) { + this._navCounter = 0; + } + if (this._navCounter < 0) { + this._navCounter = (this._buttonList.length - 1); + } + if (this._buttonList[this._navCounter]) { + var el = this._buttonList[this._navCounter].get('node'); + if (this.browser.ie) { + el = this._buttonList[this._navCounter].get('node').query('a'); + } + if (this._buttonList[this._navCounter].get('disabled')) { + this._navigateButtons(ev); + } else { + el.focus(); + } + } + break; + } + }, + /** + * @private + * @method _handleFocus + * @description Sets up the listeners for the arrow key navigation + */ + _handleFocus: function() { + if (!this._keyNav) { + var ev = 'keypress'; + if (this.browser.ie) { + ev = 'keydown'; + } + this.get('node').on(ev, Y.bind(this._navigateButtons, this)); + this._keyNav = true; + this._navCounter = -1; + } + }, + /** + * @method getButtonById + * @description Gets a button instance from the toolbar by is Dom id. + * @param {String} id The Dom id to query for. + * @return {YAHOO.widget.ToolbarButton} + */ + getButtonById: function(id) { + var len = this._buttonList.length; + for (var i = 0; i < len; i++) { + if (this._buttonList[i] && this._buttonList[i].get('id') == id) { + return this._buttonList[i]; + } + } + return false; + }, + /** + * @method getButtonByValue + * @description Gets a button instance or a menuitem instance from the toolbar by it's value. + * @param {String} value The button value to query for. + * @return {YAHOO.widget.ToolbarButton or YAHOO.widget.MenuItem} + */ + getButtonByValue: function(value) { + var _buttons = this.get('buttons'); + if (!_buttons) { + return false; + } + var len = _buttons.length; + for (var i = 0; i < len; i++) { + if (_buttons[i].group !== undefined) { + for (var m = 0; m < _buttons[i].buttons.length; m++) { + if ((_buttons[i].buttons[m].value == value) || (_buttons[i].buttons[m].menucmd == value)) { + return this.getButtonById(_buttons[i].buttons[m].id); + } + if (_buttons[i].buttons[m].menu) { //Menu Button, loop through the values + for (var s = 0; s < _buttons[i].buttons[m].menu.length; s++) { + if (_buttons[i].buttons[m].menu[s].value == value) { + return this.getButtonById(_buttons[i].buttons[m].id); + } + } + } + } + } else { + if ((_buttons[i].value == value) || (_buttons[i].menucmd == value)) { + return this.getButtonById(_buttons[i].id); + } + if (_buttons[i].menu) { //Menu Button, loop through the values + for (var j = 0; j < _buttons[i].menu.length; j++) { + if (_buttons[i].menu[j].value == value) { + return this.getButtonById(_buttons[i].id); + } + } + } + } + } + return false; + }, + /** + * @method getButtonByIndex + * @description Gets a button instance from the toolbar by is index in _buttonList. + * @param {Number} index The index of the button in _buttonList. + * @return {YAHOO.widget.ToolbarButton} + */ + getButtonByIndex: function(index) { + if (this._buttonList[index]) { + return this._buttonList[index]; + } else { + return false; + } + }, + /** + * @method getButtons + * @description Returns an array of buttons in the current toolbar + * @return {Array} + */ + getButtons: function() { + return this._buttonList; + }, + /** + * @method disableButton + * @description Disables a button in the toolbar. + * @param {String/Number} id Disable a button by it's id, index or value. + * @return {Boolean} + */ + disableButton: function(id) { + var button = getButton.call(this, id); + if (button) { + button.set('disabled', true); + } else { + return false; + } + }, + /** + * @method enableButton + * @description Enables a button in the toolbar. + * @param {String/Number} id Enable a button by it's id, index or value. + * @return {Boolean} + */ + enableButton: function(id) { + if (this.get('disabled')) { + return false; + } + var button = getButton.call(this, id); + if (button) { + if (button.get('disabled')) { + button.set('disabled', false); + } + } else { + return false; + } + }, + /** + * @method isSelected + * @description Tells if a button is selected or not. + * @param {String/Number} id A button by it's id, index or value. + * @return {Boolean} + */ + isSelected: function(id) { + var button = getButton.call(this, id); + if (button) { + return button._selected; + } + return false; + }, + /** + * @method selectButton + * @description Selects a button in the toolbar. + * @param {String/Number} id Select a button by it's id, index or value. + * @param {String} value If this is a Menu Button, check this item in the menu + * @return {Boolean} + */ + selectButton: function(id, value) { + var button = getButton.call(this, id); + if (button && (button.get('type') == 'push')) { + button.get('node').addClass('yui-button-selected'); + button.get('node').addClass('yui-button-' + button.get('value') + '-selected'); + button._selected = true; + if (value) { + if (button.buttonType == 'rich') { + var _items = button.getMenu().getItems(); + for (var m = 0; m < _items.length; m++) { + if (_items[m].value == value) { + _items[m].cfg.setProperty('checked', true); + button.set('label', '' + _items[m]._oText.nodeValue + ''); + } else { + _items[m].cfg.setProperty('checked', false); + } + } + } + } + } else { + return false; + } + }, + /** + * @method deselectButton + * @description Deselects a button in the toolbar. + * @param {String/Number} id Deselect a button by it's id, index or value. + * @return {Boolean} + */ + deselectButton: function(id) { + var button = getButton.call(this, id); + if (button) { + button.get('node').removeClass('yui-button-selected'); + button.get('node').removeClass('yui-button-' + button.get('value') + '-selected'); + button.get('node').removeClass('yui-button-hover'); + button._selected = false; + } else { + return false; + } + }, + /** + * @method deselectAllButtons + * @description Deselects all buttons in the toolbar. + * @return {Boolean} + */ + deselectAllButtons: function() { + var len = this._buttonList.length; + for (var i = 0; i < len; i++) { + this.deselectButton(this._buttonList[i]); + } + }, + /** + * @method disableAllButtons + * @description Disables all buttons in the toolbar. + * @return {Boolean} + */ + disableAllButtons: function() { + if (this.get('disabled')) { + return false; + } + var len = this._buttonList.length; + for (var i = 0; i < len; i++) { + this.disableButton(this._buttonList[i]); + } + }, + /** + * @method enableAllButtons + * @description Enables all buttons in the toolbar. + * @return {Boolean} + */ + enableAllButtons: function() { + if (this.get('disabled')) { + return false; + } + var len = this._buttonList.length; + for (var i = 0; i < len; i++) { + this.enableButton(this._buttonList[i]); + } + }, + /** + * @method resetAllButtons + * @description Resets all buttons to their initial state. + * @param {Object} _ex Except these buttons + * @return {Boolean} + */ + resetAllButtons: function(_ex) { + if (!Y.Lang.isObject(_ex)) { + _ex = {}; + } + if (this.get('disabled') || !this._buttonList) { + return false; + } + var len = this._buttonList.length; + for (var i = 0; i < len; i++) { + var _button = this._buttonList[i]; + if (_button) { + //var disabled = _button._configs.disabled._initialConfig.value; + var disabled = false; + if (_ex[_button.get('id')]) { + this.enableButton(_button); + this.selectButton(_button); + } else { + if (disabled) { + this.disableButton(_button); + } else { + this.enableButton(_button); + } + this.deselectButton(_button); + } + } + } + }, + /** + * @method destroyButton + * @description Destroy a button in the toolbar. + * @param {String/Number} id Destroy a button by it's id or index. + * @return {Boolean} + */ + destroyButton: function(id) { + var button = getButton.call(this, id); + if (button) { + var thisID = button.get('id'), + new_list = [], i = 0, + len = this._buttonList.length; + + button.destroy(); + + for (i = 0; i < len; i++) { + if (this._buttonList[i].get('id') != thisID) { + new_list[new_list.length]= this._buttonList[i]; + } + } + + this._buttonList = new_list; + } else { + return false; + } + }, + /** + * @method destroy + * @description Destroys the toolbar, all of it's elements and objects. + * @return {Boolean} + */ + destructor: function() { + var len = this._configuredButtons.length, j, i, b; + for(b = 0; b < len; b++) { + this.destroyButton(this._configuredButtons[b]); + } + + this._configuredButtons = null; + + this.get('element').innerHTML = ''; + this.get('element').className = ''; + //Brutal Object Destroy + for (i in this) { + if (Lang.hasOwnProperty(this, i)) { + this[i] = null; + } + } + return true; + }, + /** + * @method collapse + * @description Programatically collapse the toolbar. + * @param {Boolean} collapse True to collapse, false to expand. + */ + collapse: function(collapse) { + var el = this._titlebar.query('span.collapse'); + if (collapse === false) { + this.get('cont').get('parentNode').removeClass('yui-toolbar-container-collapsed'); + if (el) { + el.removeClass('collapsed'); + el.set('title', this.STR_COLLAPSE); + } + this.fire('toolbarExpanded', { type: 'toolbarExpanded' }); + } else { + if (el) { + el.addClass('collapsed'); + el.set('title', this.STR_EXPAND); + } + this.get('cont').get('parentNode').addClass('yui-toolbar-container-collapsed'); + this.fire('toolbarCollapsed', { type: 'toolbarCollapsed' }); + } + }, + /** + * @method toString + * @description Returns a string representing the toolbar. + * @return {String} + */ + toString: function() { + return 'Toolbar'; + } + }); + + Y.namespace('Y.Toolbar'); + Y.Toolbar.Bar = Toolbar; + + var YAHOO = Y.Port(), + Lang = YAHOO.lang, + Dom = YAHOO.util.Dom, + Event = YAHOO.util.Event; + + var SimpleEditor = function(o) { + + var node = Y.get(o.node), + id = node.get('id'); + if (!id) { + id = Y.stamp(node); + node.set('id', id); + } + + var element_cont = Y.Node.create('
                        '); + //Hack + Y.get('body').appendChild(element_cont); + o.element_cont = element_cont; + o.editor_wrapper = element_cont.query('div.wrapper'); + o.toolbar_cont = element_cont.query('div.toolbar_cont'); + //Hack + Y.get('body').removeChild(element_cont); + + if (!o.html) { + o.html = '{TITLE}{CONTENT}'; + } + if (!o.css) { + o.css = this._defaultCSS; + } + //console.log(o); + + + SimpleEditor.superclass.constructor.apply(this, arguments); + }; + + SimpleEditor.NAME = 'simpleeditor'; + + SimpleEditor.ATTRS = { + node: { + setter: function(node) { + var n = Y.get(node); + if (!n) { + Y.error('Invalid Node Given: ' + node); + } else { + n = n.item(0); + } + return n; + } + }, + /** + * @config container + * @description Used when dynamically creating the Editor from Javascript with no default textarea. + * We will create one and place it in this container. If no container is passed we will append to document.body. + * @default false + * @type HTMLElement + */ + 'container': { + writeOnce: true, + value: false + }, + /** + * @private + * @config iframe + * @description Internal config for holding the iframe element. + * @default null + * @type HTMLElement + */ + 'iframe': { + value: null + }, + /** + * @private + * @depreciated - No longer used, should use this.get('element') + * @config textarea + * @description Internal config for holding the textarea element (replaced with element). + * @default null + * @type HTMLElement + */ + 'textarea': { + value: null, + writeOnce: true + }, + /** + * @config element_cont + * @description Internal config for the editors container + * @default false + * @type HTMLElement + */ + 'element_cont': { + value: null + }, + /** + * @private + * @config editor_wrapper + * @description The outter wrapper for the entire editor. + * @default null + * @type HTMLElement + */ + 'editor_wrapper': { + value: null + }, + /** + * @config toolbar_cont + * @description Internal config for the toolbars container + * @default false + * @type Boolean + */ + 'toolbar_cont': { + value: null, + writeOnce: true + }, + /** + * @attribute toolbar + * @description The default toolbar config. + * @type Object + */ + 'toolbar': { + value: null + }, + /** + * @attribute height + * @description The height of the editor iframe container, not including the toolbar.. + * @default Best guessed size of the textarea, for best results use CSS to style the height of the textarea or pass it in as an argument + * @type String + */ + 'height': { + value: null, + setter: function(height) { + if (this._rendered) { + this.get('iframe').get('parentNode').setStyle('height', height); + } + } + }, + /** + * @attribute width + * @description The width of the editor container. + * @default Best guessed size of the textarea, for best results use CSS to style the width of the textarea or pass it in as an argument + * @type String + */ + 'width': { + value: null, + setter: function(width) { + if (this._rendered) { + //We have been rendered, change the width + this.get('element_cont').setStyle('width', width); + } + } + }, + /** + * @attribute blankimage + * @description The URL for the image placeholder to put in when inserting an image. + * @default The yahooapis.com address for the current release + 'assets/blankimage.png' + * @type String + */ + 'blankimage': { + value: null + }, + /** + * @attribute css + * @description The Base CSS used to format the content of the editor + * @default
                        html {
                        +            height: 95%;
                        +        }
                        +        body {
                        +            height: 100%;
                        +            padding: 7px; background-color: #fff; font:13px/1.22 arial,helvetica,clean,sans-serif;*font-size:small;*font:x-small;
                        +        }
                        +        a {
                        +            color: blue;
                        +            text-decoration: underline;
                        +            cursor: pointer;
                        +        }
                        +        .warning-localfile {
                        +            border-bottom: 1px dashed red !important;
                        +        }
                        +        .yui-busy {
                        +            cursor: wait !important;
                        +        }
                        +        img.selected { //Safari image selection
                        +            border: 2px dotted #808080;
                        +        }
                        +        img {
                        +            cursor: pointer !important;
                        +            border: none;
                        +        }
                        +        
                        + * @type String + */ + 'css': { + value: null, + writeOnce: true + }, + /** + * @attribute html + * @description The default HTML to be written to the iframe document before the contents are loaded (Note that the DOCTYPE attr will be added at render item) + * @default This HTML requires a few things if you are to override: +

                        {TITLE}, {CSS}, {HIDDEN_CSS}, {EXTRA_CSS} and {CONTENT} need to be there, they are passed to YAHOO.lang.substitute to be replace with other strings.

                        +

                        onload="document.body._rteLoaded = true;" : the onload statement must be there or the editor will not finish loading.

                        + +
                        +            <html>
                        +                <head>
                        +                    <title>{TITLE}</title>
                        +                    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
                        +                    <style>
                        +                    {CSS}
                        +                    </style>
                        +                    <style>
                        +                    {HIDDEN_CSS}
                        +                    </style>
                        +                    <style>
                        +                    {EXTRA_CSS}
                        +                    </style>
                        +                </head>
                        +            <body onload="document.body._rteLoaded = true;">
                        +            {CONTENT}
                        +            </body>
                        +            </html>
                        +            
                        +
                        + * @type String + */ + 'html': { + value: null, + writeOnce: true + }, + /** + * @attribute extracss + * @description Extra user defined css to load after the default SimpleEditor CSS + * @default '' + * @type String + */ + 'extracss': { + value: '', + writeOnce: true + }, + /** + * @config setDesignMode + * @description Should the Editor set designMode on the document. Default: true. + * @default true + * @type Boolean + */ + setDesignMode: { + value: true + }, + /** + * @config nodeChangeDelay + * @description Do we wrap the nodeChange method in a timeout for performance. Default: true. + * @default true + * @type Boolean + */ + nodeChangeDelay: { + value: true + }, + /** + * @attribute dompath + * @description Toggle the display of the current Dom path below the editor + * @default false + * @type Boolean + */ + dompath: { + value: false + }, + /** + * @attribute markup + * @description Should we try to adjust the markup for the following types: semantic, css, default or xhtml + * @default "semantic" + * @type String + */ + markup: { + value: 'semantic', + validator: function(markup) { + switch (markup.toLowerCase()) { + case 'semantic': + case 'css': + case 'default': + case 'xhtml': + return true; + } + return false; + } + }, + /** + * @config maxUndo + * @description The max number of undo levels to store. + * @default 30 + * @type Number + */ + maxUndo: { + writeOnce: true, + value: 30 + }, + + /** + * @config ptags + * @description If true, the editor uses <P> tags instead of <br> tags. (Use Shift + Enter to get a <br>) + * @default false + * @type Boolean + */ + ptags: { + writeOnce: true, + value: false + }, + /** + * @config plainText + * @description Process the inital textarea data as if it was plain text. Accounting for spaces, tabs and line feeds. + * @default false + * @type Boolean + */ + plainText: { + writeOnce: true, + value: false + }, + /** + * @private + * @config disabled_iframe + * @description Internal config for holding the iframe element used when disabling the Editor. + * @default null + * @type HTMLElement + */ + disabled_iframe: { + value: null + }, + /** + * @config nodeChangeThreshold + * @description The number of seconds that need to be in between nodeChange processing + * @default 3 + * @type Number + */ + nodeChangeThreshold: { + value: 3, + validator: Y.Lang.isNumber + }, + /** + * @config allowNoEdit + * @description Should the editor check for non-edit fields. It should be noted that this technique is not perfect. If the user does the right things, they will still be able to make changes. + * Such as highlighting an element below and above the content and hitting a toolbar button or a shortcut key. + * @default false + * @type Boolean + */ + allowNoEdit: { + value: false, + validator: Y.Lang.isBoolean + }, + /** + * @config limitCommands + * @description Should the Editor limit the allowed execCommands to the ones available in the toolbar. If true, then execCommand and keyboard shortcuts will fail if they are not defined in the toolbar. + * @default false + * @type Boolean + */ + limitCommands: { + value: false, + validator: Y.Lang.isBoolean + }, + /** + * @attribute removeLineBreaks + * @description Should we remove linebreaks and extra spaces on cleanup + * @default false + * @type Boolean + */ + removeLineBreaks: { + value: false, + validator: Y.Lang.isBoolean + }, + /** + * @config filterWord + * @description Attempt to filter out MS Word HTML from the Editor's output. + * @type Boolean + */ + filterWord: { + value: false, + validator: Y.Lang.isBoolean + }, + /** + * @config insert + * @description If true, selection is not required for: fontname, fontsize, forecolor, backcolor. + * @default false + * @type Boolean + */ + insert: { + value: false + }, + /** + * @config saveEl + * @description When save HTML is called, this element will be updated as well as the source of data. + * @default element + * @type HTMLElement + */ + saveEl: { + value: null, + setter: function(node) { + var n = Y.get(node); + if (!n) { + return false; + } else { + n = n.item(0); + } + return n; + } + }, + /** + * @attribute disabled + * @description This will toggle the editor's disabled state. When the editor is disabled, designMode is turned off and a mask is placed over the iframe so no interaction can take place. + All Toolbar buttons are also disabled so they cannot be used. + * @default false + * @type Boolean + */ + disabled: { + value: false, + setter: function(disabled) { + if (this._rendered) { + this._disableEditor(disabled); + } + } + }, + /** + * @attribute focusAtStart + * @description Should we focus the window when the content is ready? + * @default false + * @type Boolean + */ + focusAtStart: { + value: false, + writeOnce: true + }, + /** + * @attribute handleSubmit + * @description Config handles if the editor will attach itself to the textareas parent form's submit handler. + If it is set to true, the editor will attempt to attach a submit listener to the textareas parent form. + Then it will trigger the editors save handler and place the new content back into the text area before the form is submitted. + * @default false + * @type Boolean + */ + handleSubmit: { + value: false + } + }; + + Y.extend(SimpleEditor, Y.PortBase, { + _handleSubmit: function() { + var exec = this.get('handleSubmit'); + if (this.get('node').get('form')) { + if (!this._formButtons) { + this._formButtons = []; + } + if (exec) { + this.get('node').get('form').on('submit', Y.bind(this._handleFormSubmit, this)); + + var i = this.get('node').get('form').query('input'); + if (i) { + i.each(function(v, k) { + var type = v.get('type'); + if (type && (type.toLowerCase() == 'submit')) { + v.on('click', Y.bind(this._handleFormButtonClick, this)); + this._formButtons.push(v); + } + }, this); + } + } else { + //Event.removeListener(this.get('element').form, 'submit', this._handleFormSubmit); + //if (this._formButtons) { + // Event.removeListener(this._formButtons, 'click', this._handleFormButtonClick); + //} + } + } + }, + _handleInsert: function() { + if (this.get('insert')) { + var buttons = { + fontname: true, + fontsize: true, + forecolor: true, + backcolor: true + }; + var tmp = this._defaultToolbar.buttons; + for (var i = 0; i < tmp.length; i++) { + if (tmp[i].buttons) { + for (var a = 0; a < tmp[i].buttons.length; a++) { + if (tmp[i].buttons[a].value) { + if (buttons[tmp[i].buttons[a].value]) { + delete tmp[i].buttons[a].disabled; + } + } + } + } + } + } + }, + _handleDOMPath: function() { + var dompath = this.get('dompath'); + if (dompath && !this.dompath) { + this.dompath = Y.Node.create('
                        '); + this.get('element_cont').get('firstChild').appendChild(this.dompath); + if (this.get('iframe')) { + this._writeDomPath(); + } + } else if (!dompath && this.dompath) { + this.dompath.get('parentNode').removeChild(this.dompath); + this.dompath = null; + } + }, + /** + * @private + * @property _lastCommand + * @description A cache of the last execCommand (used for Undo/Redo so they don't mark an undo level) + * @type String + */ + _lastCommand: null, + _undoNodeChange: function() {}, + _storeUndo: function() {}, + /** + * @private + * @method _checkKey + * @description Checks a keyMap entry against a key event + * @param {Object} k The _keyMap object + * @param {Event} e The Mouse Event + * @return {Boolean} + */ + _checkKey: function(k, e) { + var ret = false; + if ((e.keyCode === k.key)) { + if (k.mods && (k.mods.length > 0)) { + var val = 0; + for (var i = 0; i < k.mods.length; i++) { + if (this.browser.mac) { + if (k.mods[i] == 'ctrl') { + k.mods[i] = 'meta'; + } + } + if (e[k.mods[i] + 'Key'] === true) { + val++; + } + } + if (val === k.mods.length) { + ret = true; + } + } else { + ret = true; + } + } + //YAHOO.log('Shortcut Key Check: (' + k.key + ') return: ' + ret, 'info', 'SimpleEditor'); + return ret; + }, + /** + * @private + * @property _keyMap + * @description Named key maps for various actions in the Editor. Example: CLOSE_WINDOW: { key: 87, mods: ['shift', 'ctrl'] }. + * This entry shows that when key 87 (W) is found with the modifiers of shift and control, the window will close. You can customize this object to tweak keyboard shortcuts. + * @type {Object/Mixed} + */ + _keyMap: { + SELECT_ALL: { + key: 65, //A key + mods: ['ctrl'] + }, + CLOSE_WINDOW: { + key: 87, //W key + mods: ['shift', 'ctrl'] + }, + FOCUS_TOOLBAR: { + key: 27, + mods: ['shift'] + }, + FOCUS_AFTER: { + key: 27 + }, + FONT_SIZE_UP: { + key: 38, + mods: ['shift', 'ctrl'] + }, + FONT_SIZE_DOWN: { + key: 40, + mods: ['shift', 'ctrl'] + }, + CREATE_LINK: { + key: 76, + mods: ['shift', 'ctrl'] + }, + BOLD: { + key: 66, + mods: ['shift', 'ctrl'] + }, + ITALIC: { + key: 73, + mods: ['shift', 'ctrl'] + }, + UNDERLINE: { + key: 85, + mods: ['shift', 'ctrl'] + }, + UNDO: { + key: 90, + mods: ['ctrl'] + }, + REDO: { + key: 90, + mods: ['shift', 'ctrl'] + }, + JUSTIFY_LEFT: { + key: 219, + mods: ['shift', 'ctrl'] + }, + JUSTIFY_CENTER: { + key: 220, + mods: ['shift', 'ctrl'] + }, + JUSTIFY_RIGHT: { + key: 221, + mods: ['shift', 'ctrl'] + } + }, + /** + * @private + * @method _cleanClassName + * @description Makes a useable classname from dynamic data, by dropping it to lowercase and replacing spaces with -'s. + * @param {String} str The classname to clean up + * @return {String} + */ + _cleanClassName: function(str) { + return str.replace(/ /g, '-').toLowerCase(); + }, + /** + * @property _textarea + * @description Flag to determine if we are using a textarea or an HTML Node. + * @type Boolean + */ + _textarea: null, + /** + * @property _docType + * @description The DOCTYPE to use in the editable container. + * @type String + */ + _docType: '', + /** + * @property editorDirty + * @description This flag will be set when certain things in the Editor happen. It is to be used by the developer to check to see if content has changed. + * @type Boolean + */ + editorDirty: null, + /** + * @property _defaultCSS + * @description The default CSS used in the config for 'css'. This way you can add to the config like this: { css: YAHOO.widget.SimpleEditor.prototype._defaultCSS + 'ADD MYY CSS HERE' } + * @type String + */ + _defaultCSS: '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; } .warning-localfile { border-bottom: 1px dashed red !important; } .yui-busy { cursor: wait !important; } img.selected { border: 2px dotted #808080; } img { cursor: pointer !important; border: none; } body.ptags.webkit div.yui-wk-p { margin: 11px 0; } body.ptags.webkit div.yui-wk-div { margin: 0; }', + /** + * @property _defaultToolbar + * @private + * @description Default toolbar config. + * @type Object + */ + _defaultToolbar: null, + /** + * @property _lastButton + * @private + * @description The last button pressed, so we don't disable it. + * @type Object + */ + _lastButton: null, + /** + * @property _baseHREF + * @private + * @description The base location of the editable page (this page) so that relative paths for image work. + * @type String + */ + _baseHREF: function() { + var href = document.location.href; + if (href.indexOf('?') !== -1) { //Remove the query string + href = href.substring(0, href.indexOf('?')); + } + href = href.substring(0, href.lastIndexOf('/')) + '/'; + return href; + }(), + /** + * @property _lastImage + * @private + * @description Safari reference for the last image selected (for styling as selected). + * @type HTMLElement + */ + _lastImage: null, + /** + * @property _blankImageLoaded + * @private + * @description Don't load the blank image more than once.. + * @type Boolean + */ + _blankImageLoaded: null, + /** + * @property _fixNodesTimer + * @private + * @description Holder for the fixNodes timer + * @type Date + */ + _fixNodesTimer: null, + /** + * @property _nodeChangeTimer + * @private + * @description Holds a reference to the nodeChange setTimeout call + * @type Number + */ + _nodeChangeTimer: null, + /** + * @property _nodeChangeDelayTimer + * @private + * @description Holds a reference to the nodeChangeDelay setTimeout call + * @type Number + */ + _nodeChangeDelayTimer: null, + /** + * @property _lastNodeChangeEvent + * @private + * @description Flag to determine the last event that fired a node change + * @type Event + */ + _lastNodeChangeEvent: null, + /** + * @property _lastNodeChange + * @private + * @description Flag to determine when the last node change was fired + * @type Date + */ + _lastNodeChange: 0, + /** + * @property _rendered + * @private + * @description Flag to determine if editor has been rendered or not + * @type Boolean + */ + _rendered: null, + /** + * @property DOMReady + * @private + * @description Flag to determine if DOM is ready or not + * @type Boolean + */ + DOMReady: null, + /** + * @property _selection + * @private + * @description Holder for caching iframe selections + * @type Object + */ + _selection: null, + /** + * @property _mask + * @private + * @description DOM Element holder for the editor Mask when disabled + * @type Object + */ + _mask: null, + /** + * @property _showingHiddenElements + * @private + * @description Status of the hidden elements button + * @type Boolean + */ + _showingHiddenElements: null, + /** + * @property currentWindow + * @description A reference to the currently open EditorWindow + * @type Object + */ + currentWindow: null, + /** + * @property currentEvent + * @description A reference to the current editor event + * @type Event + */ + currentEvent: null, + /** + * @property operaEvent + * @private + * @description setTimeout holder for Opera and Image DoubleClick event.. + * @type Object + */ + operaEvent: null, + /** + * @property currentFont + * @description A reference to the last font selected from the Toolbar + * @type HTMLElement + */ + currentFont: null, + /** + * @property currentElement + * @description A reference to the current working element in the editor + * @type Array + */ + currentElement: null, + /** + * @property dompath + * @description A reference to the dompath container for writing the current working dom path to. + * @type HTMLElement + */ + dompath: null, + /** + * @property beforeElement + * @description A reference to the H2 placed before the editor for Accessibilty. + * @type HTMLElement + */ + beforeElement: null, + /** + * @property afterElement + * @description A reference to the H2 placed after the editor for Accessibilty. + * @type HTMLElement + */ + afterElement: null, + /** + * @property invalidHTML + * @description Contains a list of HTML elements that are invalid inside the editor. They will be removed when they are found. If you set the value of a key to "{ keepContents: true }", then the element will be replaced with a yui-non span to be filtered out when cleanHTML is called. The only tag that is ignored here is the span tag as it will force the Editor into a loop and freeze the browser. However.. all of these tags will be removed in the cleanHTML routine. + * @type Object + */ + invalidHTML: { + form: true, + input: true, + button: true, + select: true, + link: true, + html: true, + body: true, + iframe: true, + script: true, + style: true, + textarea: true + }, + /** + * @property toolbar + * @description Local property containing the YAHOO.widget.Toolbar instance + * @type YAHOO.widget.Toolbar + */ + toolbar: null, + /** + * @private + * @property _contentTimer + * @description setTimeout holder for documentReady check + */ + _contentTimer: null, + /** + * @private + * @property _contentTimerMax + * @description The number of times the loaded content should be checked before giving up. Default: 500 + */ + _contentTimerMax: 500, + /** + * @private + * @property _contentTimerCounter + * @description Counter to check the number of times the body is polled for before giving up + * @type Number + */ + _contentTimerCounter: 0, + /** + * @private + * @property _disabled + * @description The Toolbar items that should be disabled if there is no selection present in the editor. + * @type Array + */ + _disabled: [ 'createlink', 'fontname', 'fontsize', 'forecolor', 'backcolor' ], + /** + * @private + * @property _alwaysDisabled + * @description The Toolbar items that should ALWAYS be disabled event if there is a selection present in the editor. + * @type Object + */ + _alwaysDisabled: { undo: true, redo: true }, + /** + * @private + * @property _alwaysEnabled + * @description The Toolbar items that should ALWAYS be enabled event if there isn't a selection present in the editor. + * @type Object + */ + _alwaysEnabled: { }, + /** + * @private + * @property _semantic + * @description The Toolbar commands that we should attempt to make tags out of instead of using styles. + * @type Object + */ + _semantic: { 'bold': true, 'italic' : true, 'underline' : true }, + /** + * @private + * @property _tag2cmd + * @description A tag map of HTML tags to convert to the different types of commands so we can select the proper toolbar button. + * @type Object + */ + _tag2cmd: { + 'b': 'bold', + 'strong': 'bold', + 'i': 'italic', + 'em': 'italic', + 'u': 'underline', + 'sup': 'superscript', + 'sub': 'subscript', + 'img': 'insertimage', + 'a' : 'createlink', + 'ul' : 'insertunorderedlist', + 'ol' : 'insertorderedlist' + }, + + /** + * @private _createIframe + * @description Creates the DOM and YUI Element for the iFrame editor area. + * @param {String} id The string ID to prefix the iframe with + * @return {Object} iFrame object + */ + _createIframe: function() { + var ifrmDom = document.createElement('iframe'); + ifrmDom.id = this.get('node').get('id') + '_editor'; + var config = { + border: '0', + frameBorder: '0', + marginWidth: '0', + marginHeight: '0', + leftMargin: '0', + topMargin: '0', + allowTransparency: 'true', + width: '100%' + }; + if (this.get('autoHeight')) { + config.scrolling = 'no'; + } + Y.each(config, function(v, k) { + ifrmDom.setAttribute(k, v); + }); + var isrc = 'javascript:;'; + if (this.browser.ie) { + //isrc = 'about:blank'; + //TODO - Check this, I have changed it before.. + isrc = 'javascript:false;'; + } + ifrmDom.setAttribute('src', isrc); + //var ifrm = new YAHOO.util.Element(ifrmDom); + var ifrm = Y.get(ifrmDom); + ifrm.setStyle('visibility', 'hidden'); + return ifrm; + }, + /** + * @private _isElement + * @description Checks to see if an Element reference is a valid one and has a certain tag type + * @param {HTMLElement} el The element to check + * @param {String} tag The tag that the element needs to be + * @return {Boolean} + */ + _isElement: function(el, tag) { + if (el && el.tagName && (el.tagName.toLowerCase() == tag)) { + return true; + } + if (el && el.getAttribute && (el.getAttribute('tag') == tag)) { + return true; + } + return false; + }, + /** + * @private _hasParent + * @description Checks to see if an Element reference or one of it's parents is a valid one and has a certain tag type + * @param {HTMLElement} el The element to check + * @param {String} tag The tag that the element needs to be + * @return HTMLElement + */ + _hasParent: function(el, tag) { + if (!el || !el.parentNode) { + return false; + } + + while (el.parentNode) { + if (this._isElement(el, tag)) { + return el; + } + if (el.parentNode) { + el = el.parentNode; + } else { + return false; + } + } + return false; + }, + /** + * @private + * @method _getDoc + * @description Get the Document of the IFRAME + * @return {Object} + */ + _getDoc: function() { + var value = false; + try { + var f = document.getElementById(this.get('iframe').get('id')); + if (f && f.contentWindow.document) { + value = f.contentWindow.document; + return value; + } + } catch (e) { + return false; + } + }, + /** + * @private + * @method _getWindow + * @description Get the Window of the IFRAME + * @return {Object} + */ + _getWindow: function() { + var f = document.getElementById(this.get('iframe').get('id')); + return f.contentWindow; + }, + /** + * @method focus + * @description Attempt to set the focus of the iframes window. + */ + focus: function() { + this._getWindow().focus(); + }, + /** + * @private + * @depreciated - This should not be used, moved to this.focus(); + * @method _focusWindow + * @description Attempt to set the focus of the iframes window. + */ + _focusWindow: function() { + YAHOO.log('_focusWindow: depreciated in favor of this.focus()', 'warn', 'Editor'); + this.focus(); + }, + /** + * @private + * @method _hasSelection + * @description Determines if there is a selection in the editor document. + * @return {Boolean} + */ + _hasSelection: function() { + var sel = this._getSelection(); + var range = this._getRange(); + var hasSel = false; + + if (!sel || !range) { + return hasSel; + } + + //Internet Explorer + if (this.browser.ie || this.browser.opera) { + if (range.text) { + hasSel = true; + } + if (range.html) { + hasSel = true; + } + } else { + if (this.browser.webkit) { + if (sel+'' !== '') { + hasSel = true; + } + } else { + if (sel && (sel.toString() !== '') && (sel !== undefined)) { + hasSel = true; + } + } + } + return hasSel; + }, + /** + * @private + * @method _getSelection + * @description Handles the different selection objects across the A-Grade list. + * @return {Object} Selection Object + */ + _getSelection: function() { + var _sel = null; + if (this._getDoc() && this._getWindow()) { + if (this._getDoc().selection) { + _sel = this._getDoc().selection; + } else { + _sel = this._getWindow().getSelection(); + } + //Handle Safari's lack of Selection Object + if (this.browser.webkit) { + if (_sel.baseNode) { + this._selection = {}; + this._selection.baseNode = _sel.baseNode; + this._selection.baseOffset = _sel.baseOffset; + this._selection.extentNode = _sel.extentNode; + this._selection.extentOffset = _sel.extentOffset; + } else if (this._selection !== null) { + _sel = this._getWindow().getSelection(); + _sel.setBaseAndExtent( + this._selection.baseNode, + this._selection.baseOffset, + this._selection.extentNode, + this._selection.extentOffset); + this._selection = null; + } + } + } + return _sel; + }, + /** + * @private + * @method _selectNode + * @description Places the highlight around a given node + * @param {HTMLElement} node The node to select + */ + _selectNode: function(node, collapse) { + if (!node) { + return false; + } + var sel = this._getSelection(), + range = null; + + if (this.browser.ie) { + try { //IE freaks out here sometimes.. + range = this._getDoc().body.createTextRange(); + range.moveToElementText(node); + range.select(); + } catch (e) { + YAHOO.log('IE failed to select element: ' + node.tagName, 'warn', 'SimpleEditor'); + } + } else if (this.browser.webkit) { + if (collapse) { + sel.setBaseAndExtent(node, 1, node, node.innerText.length); + } else { + sel.setBaseAndExtent(node, 0, node, node.innerText.length); + } + } else if (this.browser.opera) { + sel = this._getWindow().getSelection(); + range = this._getDoc().createRange(); + range.selectNode(node); + sel.removeAllRanges(); + sel.addRange(range); + } else { + range = this._getDoc().createRange(); + range.selectNodeContents(node); + sel.removeAllRanges(); + sel.addRange(range); + } + //TODO - Check Performance + this.nodeChange(); + }, + /** + * @private + * @method _getRange + * @description Handles the different range objects across the A-Grade list. + * @return {Object} Range Object + */ + _getRange: function() { + var sel = this._getSelection(); + + if (sel === null) { + return null; + } + + if (this.browser.webkit && !sel.getRangeAt) { + var _range = this._getDoc().createRange(); + try { + _range.setStart(sel.anchorNode, sel.anchorOffset); + _range.setEnd(sel.focusNode, sel.focusOffset); + } catch (e) { + _range = this._getWindow().getSelection()+''; + } + return _range; + } + + if (this.browser.ie || this.browser.opera) { + try { + return sel.createRange(); + } catch (e2) { + return null; + } + } + + if (sel.rangeCount > 0) { + return sel.getRangeAt(0); + } + return null; + }, + /** + * @private + * @method _setDesignMode + * @description Sets the designMode property of the iFrame document's body. + * @param {String} state This should be either on or off + */ + _setDesignMode: function(state) { + if (this.get('setDesignMode')) { + try { + this._getDoc().designMode = ((state.toLowerCase() == 'off') ? 'off' : 'on'); + } catch(e) { } + } + }, + /** + * @private + * @method _toggleDesignMode + * @description Toggles the designMode property of the iFrame document on and off. + * @return {String} The state that it was set to. + */ + _toggleDesignMode: function() { + YAHOO.log('It is not recommended to use this method and it will be depreciated.', 'warn', 'SimpleEditor'); + var _dMode = this._getDoc().designMode, + _state = ((_dMode.toLowerCase() == 'on') ? 'off' : 'on'); + this._setDesignMode(_state); + return _state; + }, + /** + * @private + * @property _focused + * @description Holder for trapping focus/blur state and prevent double events + * @type Boolean + */ + _focused: null, + /** + * @private + * @method _handleFocus + * @description Handles the focus of the iframe. Note, this is window focus event, not an Editor focus event. + * @param {Event} e The DOM Event + */ + _handleFocus: function(e) { + if (!this._focused) { + YAHOO.log('Editor Window Focused', 'info', 'SimpleEditor'); + this._focused = true; + this.fireEvent('editorWindowFocus', { type: 'editorWindowFocus', target: this }); + } + }, + /** + * @private + * @method _handleBlur + * @description Handles the blur of the iframe. Note, this is window blur event, not an Editor blur event. + * @param {Event} e The DOM Event + */ + _handleBlur: function(e) { + if (this._focused) { + YAHOO.log('Editor Window Blurred', 'info', 'SimpleEditor'); + this._focused = false; + this.fireEvent('editorWindowBlur', { type: 'editorWindowBlur', target: this }); + } + }, + /** + * @private + * @method _initEditorEvents + * @description This method sets up the listeners on the Editors document. + */ + _initEditorEvents: function() { + //Setup Listeners on iFrame + var doc = this._getDoc(), + win = this._getWindow(); + + Y.Event.nativeAdd(doc, 'mouseup', Y.bind(this._handleMouseUp, this)); + Y.Event.nativeAdd(doc, 'mousedown', Y.bind(this._handleMouseDown, this)); + Y.Event.nativeAdd(doc, 'click', Y.bind(this._handleClick, this)); + Y.Event.nativeAdd(doc, 'dblclick', Y.bind(this._handleDoubleClick, this)); + Y.Event.nativeAdd(doc, 'keypress', Y.bind(this._handleKeyPress, this)); + Y.Event.nativeAdd(doc, 'keyup', Y.bind(this._handleKeyUp, this)); + Y.Event.nativeAdd(doc, 'keydown', Y.bind(this._handleKeyDown, this)); + + //Focus and blur.. + Y.Event.nativeAdd(win, 'focus', Y.bind(this._handleFocus, this)); + Y.Event.nativeAdd(win, 'blur', Y.bind(this._handleBlur, this)); + }, + /** + * @private + * @method _removeEditorEvents + * @description This method removes the listeners on the Editors document (for disabling). + */ + _removeEditorEvents: function() { + //Remove Listeners on iFrame + var doc = this._getDoc(), + win = this._getWindow(); + + Y.Event.nativeRremove(doc, 'mouseup', Y.bind(this._handleMouseUp, this)); + Y.Event.nativeRremove(doc, 'mousedown', Y.bind(this._handleMouseDown, this)); + Y.Event.nativeRremove(doc, 'click', Y.bind(this._handleClick, this)); + Y.Event.nativeRremove(doc, 'dblclick', Y.bind(this._handleDoubleClick, this)); + Y.Event.nativeRremove(doc, 'keypress', Y.bind(this._handleKeyPress, this)); + Y.Event.nativeRremove(doc, 'keyup', Y.bind(this._handleKeyUp, this)); + Y.Event.nativeRremove(doc, 'keydown', Y.bind(this._handleKeyDown, this)); + + //Focus and blur.. + Y.Event.nativeRremove(win, 'focus', Y.bind(this._handleFocus, this)); + Y.Event.nativeRremove(win, 'blur', Y.bind(this._handleBlur, this)); + }, + _fixWebkitDivs: function() { + if (this.browser.webkit) { + var divs = this._getDoc().body.getElementsByTagName('div'); + Dom.addClass(divs, 'yui-wk-div'); + } + }, + /** + * @private + * @method _initEditor + * @param {Boolean} raw Don't add events. + * @description This method is fired from _checkLoaded when the document is ready. It turns on designMode and set's up the listeners. + */ + _initEditor: function(raw) { + if (this._editorInit) { + return; + } + this._editorInit = true; + if (this.browser.ie) { + this._getDoc().body.style.margin = '0'; + } + if (!this.get('disabled')) { + this._setDesignMode('on'); + this._contentTimerCounter = 0; + } + if (!this._getDoc().body) { + YAHOO.log('Body is null, check again', 'error', 'SimpleEditor'); + this._contentTimerCounter = 0; + this._editorInit = false; + this._checkLoaded(); + return false; + } + + YAHOO.log('editorLoaded', 'info', 'SimpleEditor'); + if (!raw) { + this.toolbar.on('buttonClick', Y.bind(this._handleToolbarClick, this)); + } + if (!this.get('disabled')) { + this._initEditorEvents(); + this.toolbar.set('disabled', false); + } + + if (raw) { + this.fire('editorContentReloaded', { type: 'editorreloaded'}); + } else { + this.fire('editorContentLoaded', { type: 'editorLoaded' }); + } + this._fixWebkitDivs(); + if (this.get('dompath')) { + YAHOO.log('Delayed DomPath write', 'info', 'SimpleEditor'); + Y.later(150, this, this._writeDomPath); + } + var br = []; + Y.each(this.browser, function(v, k) { + if (v) { + br.push(k); + } + }, this); + if (this.get('ptags')) { + br.push('ptags'); + } + Y.DOM.addClass(this._getDoc().body, br.join(' ')); + this.nodeChange(true); + }, + /** + * @private + * @method _checkLoaded + * @param {Boolean} raw Don't add events. + * @description Called from a setTimeout loop to check if the iframes body.onload event has fired, then it will init the editor. + */ + _checkLoaded: function(raw) { + this._editorInit = false; + this._contentTimerCounter++; + if (this._contentTimer) { + clearTimeout(this._contentTimer); + } + if (this._contentTimerCounter > this._contentTimerMax) { + YAHOO.log('ERROR: Body Did Not load', 'error', 'SimpleEditor'); + return false; + } + var init = false; + try { + if (this._getDoc() && this._getDoc().body) { + if (this.browser.ie) { + if (this._getDoc().body.readyState == 'complete') { + init = true; + } + } else { + if (this._getDoc().body._rteLoaded === true) { + init = true; + } + } + } + } catch (e) { + init = false; + YAHOO.log('checking body (e)' + e, 'error', 'SimpleEditor'); + } + + if (init === true) { + //The onload event has fired, clean up after ourselves and fire the _initEditor method + YAHOO.log('Firing _initEditor', 'info', 'SimpleEditor'); + this._initEditor(raw); + } else { + var self = this; + this._contentTimer = setTimeout(function() { + self._checkLoaded.call(self, raw); + }, 20); + } + }, + /** + * @private + * @method _setInitialContent + * @param {Boolean} raw Don't add events. + * @description This method will open the iframes content document and write the textareas value into it, then start the body.onload checking. + */ + _setInitialContent: function(raw) { + YAHOO.log('Populating editor body with contents of the text area', 'info', 'SimpleEditor'); + + var value = ((this._textarea) ? this.get('node').get('value') : this.get('node').get('innerHTML')), + doc = null; + + if (value === '') { + value = '
                        '; + } + + var html = Y.Lang.substitute(this.get('html'), { + TITLE: this.STR_TITLE, + CONTENT: this._cleanIncomingHTML(value), + CSS: this.get('css'), + HIDDEN_CSS: ((this.get('hiddencss')) ? this.get('hiddencss') : '/* No Hidden CSS */'), + EXTRA_CSS: ((this.get('extracss')) ? this.get('extracss') : '/* No Extra CSS */') + }), + check = true; + + html = html.replace(/RIGHT_BRACKET/gi, '{'); + html = html.replace(/LEFT_BRACKET/gi, '}'); + + if (document.compatMode != 'BackCompat') { + YAHOO.log('Adding Doctype to editable area', 'info', 'SimpleEditor'); + html = this._docType + "\n" + html; + } else { + YAHOO.log('DocType skipped because we are in BackCompat Mode.', 'warn', 'SimpleEditor'); + } + + if (this.browser.ie || this.browser.webkit || this.browser.opera || (navigator.userAgent.indexOf('Firefox/1.5') != -1)) { + //Firefox 1.5 doesn't like setting designMode on an document created with a data url + try { + //Adobe AIR Code + if (this.browser.air) { + doc = this._getDoc().implementation.createHTMLDocument(); + var origDoc = this._getDoc(); + origDoc.open(); + origDoc.close(); + doc.open(); + doc.write(html); + doc.close(); + var node = origDoc.importNode(doc.getElementsByTagName("html")[0], true); + origDoc.replaceChild(node, origDoc.getElementsByTagName("html")[0]); + origDoc.body._rteLoaded = true; + } else { + doc = this._getDoc(); + doc.open(); + doc.write(html); + doc.close(); + } + } catch (e) { + YAHOO.log('Setting doc failed.. (_setInitialContent)', 'error', 'SimpleEditor'); + //Safari will only be here if we are hidden + check = false; + } + } else { + //This keeps Firefox 2 from writing the iframe to history preserving the back buttons functionality + this.get('iframe').set('src', 'data:text/html;charset=utf-8,' + encodeURIComponent(html)); + } + this.get('iframe').setStyle('visibility', ''); + if (check) { + this._checkLoaded(raw); + } + }, + /** + * @private + * @method _setMarkupType + * @param {String} action The action to take. Possible values are: css, default or semantic + * @description This method will turn on/off the useCSS execCommand. + */ + _setMarkupType: function(action) { + switch (this.get('markup')) { + case 'css': + this._setEditorStyle(true); + break; + case 'default': + this._setEditorStyle(false); + break; + case 'semantic': + case 'xhtml': + if (this._semantic[action]) { + this._setEditorStyle(false); + } else { + this._setEditorStyle(true); + } + break; + } + }, + /** + * Set the editor to use CSS instead of HTML + * @param {Booleen} stat True/False + */ + _setEditorStyle: function(stat) { + try { + this._getDoc().execCommand('useCSS', false, !stat); + } catch (ex) { + } + }, + /** + * @private + * @method _getSelectedElement + * @description This method will attempt to locate the element that was last interacted with, either via selection, location or event. + * @return {HTMLElement} The currently selected element. + */ + _getSelectedElement: function() { + var doc = this._getDoc(), + range = null, + sel = null, + elm = null, + check = true; + + if (this.browser.ie) { + this.currentEvent = this._getWindow().event; //Event utility assumes window.event, so we need to reset it to this._getWindow().event; + range = this._getRange(); + if (range) { + elm = range.item ? range.item(0) : range.parentElement(); + if (this._hasSelection()) { + //TODO + //WTF.. Why can't I get an element reference here?!??! + } + if (elm === doc.body) { + elm = null; + } + } + if ((this.currentEvent !== null) && (this.currentEvent.keyCode === 0)) { + elm = Event.getTarget(this.currentEvent); + } + } else { + sel = this._getSelection(); + range = this._getRange(); + + if (!sel || !range) { + return null; + } + //TODO + if (!this._hasSelection() && this.browser.webkit3) { + //check = false; + } + if (this.browser.gecko) { + //Added in 2.6.0 + if (range.startContainer) { + if (range.startContainer.nodeType === 3) { + elm = range.startContainer.parentNode; + } else if (range.startContainer.nodeType === 1) { + elm = range.startContainer; + } + //Added in 2.7.0 + if (this.currentEvent) { + var tar = Event.getTarget(this.currentEvent); + if (!this._isElement(tar, 'html')) { + if (elm !== tar) { + elm = tar; + } + } + } + } + } + + if (check) { + if (sel.anchorNode && (sel.anchorNode.nodeType == 3)) { + if (sel.anchorNode.parentNode) { //next check parentNode + elm = sel.anchorNode.parentNode; + } + if (sel.anchorNode.nextSibling != sel.focusNode.nextSibling) { + elm = sel.anchorNode.nextSibling; + } + } + if (this._isElement(elm, 'br')) { + elm = null; + } + if (!elm) { + elm = range.commonAncestorContainer; + if (!range.collapsed) { + if (range.startContainer == range.endContainer) { + if (range.startOffset - range.endOffset < 2) { + if (range.startContainer.hasChildNodes()) { + elm = range.startContainer.childNodes[range.startOffset]; + } + } + } + } + } + } + } + + if (this.currentEvent !== null) { + try { + switch (this.currentEvent.type) { + case 'click': + case 'mousedown': + case 'mouseup': + if (this.browser.webkit) { + elm = Event.getTarget(this.currentEvent); + } + break; + default: + //Do nothing + break; + } + } catch (e) { + YAHOO.log('Firefox 1.5 errors here: ' + e, 'error', 'SimpleEditor'); + } + } else if ((this.currentElement && this.currentElement[0]) && (!this.browser.ie)) { + //TODO is this still needed? + //elm = this.currentElement[0]; + } + + + if (this.browser.opera || this.browser.webkit) { + if (this.currentEvent && !elm) { + elm = Event.getTarget(this.currentEvent); + } + } + if (!elm || !elm.tagName) { + elm = doc.body; + } + if (this._isElement(elm, 'html')) { + //Safari sometimes gives us the HTML node back.. + elm = doc.body; + } + if (this._isElement(elm, 'body')) { + //make sure that body means this body not the parent.. + elm = doc.body; + } + if (elm && !elm.parentNode) { //Not in document + elm = doc.body; + } + if (elm === undefined) { + elm = null; + } + return elm; + }, + /** + * @private + * @method _getSelectedNode + * @description Calls _getSelectedElement and passes it through Y.get to get a Node reference. + * @return {Y.Node} The currently selected element. + */ + _getSelectedNode: function() { + return Y.get(this._getSelectedElement()); + }, + /** + * @private + * @method _getDomPath + * @description This method will attempt to build the DOM path from the currently selected element. + * @param HTMLElement el The element to start with, if not provided _getSelectedElement is used + * @return {Array} An array of node references that will create the DOM Path. + */ + _getDomPath: function(el) { + if (!el) { + el = this._getSelectedElement(); + } + var domPath = []; + while (el !== null) { + if (el.ownerDocument != this._getDoc()) { + el = null; + break; + } + //Check to see if we get el.nodeName and nodeType + if (el.nodeName && el.nodeType && (el.nodeType == 1)) { + domPath[domPath.length] = el; + } + + if (this._isElement(el, 'body')) { + break; + } + + el = el.parentNode; + } + if (domPath.length === 0) { + if (this._getDoc() && this._getDoc().body) { + domPath[0] = this._getDoc().body; + } + } + return domPath.reverse(); + }, + /** + * @private + * @method _writeDomPath + * @description Write the current DOM path out to the dompath container below the editor. + */ + _writeDomPath: function() { + var path = this._getDomPath(), + pathArr = [], + classPath = '', + pathStr = ''; + + for (var i = 0; i < path.length; i++) { + var tag = path[i].tagName.toLowerCase(); + if ((tag == 'ol') && (path[i].type)) { + tag += ':' + path[i].type; + } + if (Dom.hasClass(path[i], 'yui-tag')) { + tag = path[i].getAttribute('tag'); + } + if ((this.get('markup') == 'semantic') || (this.get('markup') == 'xhtml')) { + switch (tag) { + case 'b': tag = 'strong'; break; + case 'i': tag = 'em'; break; + } + } + if (!Dom.hasClass(path[i], 'yui-non')) { + if (Dom.hasClass(path[i], 'yui-tag')) { + pathStr = tag; + } else { + classPath = ((path[i].className !== '') ? '.' + path[i].className.replace(/ /g, '.') : ''); + if ((classPath.indexOf('yui') != -1) || (classPath.toLowerCase().indexOf('apple-style-span') != -1)) { + classPath = ''; + } + pathStr = tag + ((path[i].id) ? '#' + path[i].id : '') + classPath; + } + switch (tag) { + case 'body': + pathStr = 'body'; + break; + case 'a': + if (path[i].getAttribute('href', 2)) { + pathStr += ':' + path[i].getAttribute('href', 2).replace('mailto:', '').replace('http:/'+'/', '').replace('https:/'+'/', ''); //May need to add others here ftp + } + break; + case 'img': + var h = path[i].height; + var w = path[i].width; + if (path[i].style.height) { + h = parseInt(path[i].style.height, 10); + } + if (path[i].style.width) { + w = parseInt(path[i].style.width, 10); + } + pathStr += '(' + w + 'x' + h + ')'; + break; + } + + if (pathStr.length > 10) { + pathStr = '' + pathStr.substring(0, 10) + '...' + ''; + } else { + pathStr = '' + pathStr + ''; + } + pathArr[pathArr.length] = pathStr; + } + } + var str = pathArr.join(' ' + this.SEP_DOMPATH + ' '); + //Prevent flickering + if (this.dompath.get('innerHTML') != str) { + this.dompath.set('innerHTML', str); + } + }, + /** + * @private + * @method _fixNodes + * @description Fix href and imgs as well as remove invalid HTML. + */ + _fixNodes: function() { + try { + var doc = this._getDoc(), + els = []; + + Y.each(this.invalidHTML, function(v, k) { + if (k.toLowerCase() != 'span') { + var tags = doc.body.getElementsByTagName(k); + if (tags.length) { + for (var i = 0; i < tags.length; i++) { + els.push(tags[i]); + } + } + } + }); + for (var h = 0; h < els.length; h++) { + if (els[h].parentNode) { + if (Lang.isObject(this.invalidHTML[els[h].tagName.toLowerCase()]) && this.invalidHTML[els[h].tagName.toLowerCase()].keepContents) { + this._swapEl(els[h], 'span', function(el) { + el.className = 'yui-non'; + }); + } else { + els[h].parentNode.removeChild(els[h]); + } + } + } + var imgs = this._getDoc().getElementsByTagName('img'); + Dom.addClass(imgs, 'yui-img'); + } catch(e) {} + }, + /** + * @private + * @method _isNonEditable + * @param Event ev The Dom event being checked + * @description Method is called at the beginning of all event handlers to check if this element or a parent element has the class yui-noedit (this.CLASS_NOEDIT) applied. + * If it does, then this method will stop the event and return true. The event handlers will then return false and stop the nodeChange from occuring. This method will also + * disable and enable the Editor's toolbar based on the noedit state. + * @return Boolean + */ + _isNonEditable: function(ev) { + if (this.get('allowNoEdit')) { + var el = Event.getTarget(ev); + if (this._isElement(el, 'html')) { + el = null; + } + var path = this._getDomPath(el); + for (var i = (path.length - 1); i > -1; i--) { + if (Dom.hasClass(path[i], this.CLASS_NOEDIT)) { + //if (this.toolbar.get('disabled') === false) { + // this.toolbar.set('disabled', true); + //} + try { + this._getDoc().execCommand('enableObjectResizing', false, 'false'); + } catch (e) {} + this.nodeChange(); + Event.stopEvent(ev); + YAHOO.log('CLASS_NOEDIT found in DOM Path, stopping event', 'info', 'SimpleEditor'); + return true; + } + } + //if (this.toolbar.get('disabled') === true) { + //Should only happen once.. + //this.toolbar.set('disabled', false); + try { + this._getDoc().execCommand('enableObjectResizing', false, 'true'); + } catch (e2) {} + //} + } + return false; + }, + /** + * @private + * @method _setCurrentEvent + * @param {Event} ev The event to cache + * @description Sets the current event property + */ + _setCurrentEvent: function(ev) { + this.currentEvent = ev; + }, + /** + * @private + * @method _handleClick + * @param {Event} ev The event we are working on. + * @description Handles all click events inside the iFrame document. + */ + _handleClick: function(ev) { + var ret = this.fireEvent('beforeEditorClick', { type: 'beforeEditorClick', target: this, ev: ev }); + if (ret === false) { + return false; + } + if (this._isNonEditable(ev)) { + return false; + } + this._setCurrentEvent(ev); + if (this.currentWindow) { + this.closeWindow(); + } + if (this.currentWindow) { + this.closeWindow(); + } + if (this.browser.webkit) { + var tar =Event.getTarget(ev); + if (this._isElement(tar, 'a') || this._isElement(tar.parentNode, 'a')) { + Event.stopEvent(ev); + this.nodeChange(); + } + } else { + this.nodeChange(); + } + this.fireEvent('editorClick', { type: 'editorClick', target: this, ev: ev }); + }, + /** + * @private + * @method _handleMouseUp + * @param {Event} ev The event we are working on. + * @description Handles all mouseup events inside the iFrame document. + */ + _handleMouseUp: function(ev) { + var ret = this.fireEvent('beforeEditorMouseUp', { type: 'beforeEditorMouseUp', target: this, ev: ev }); + if (ret === false) { + return false; + } + if (this._isNonEditable(ev)) { + return false; + } + //Don't set current event for mouseup. + //It get's fired after a menu is closed and gives up a bogus event to work with + //this._setCurrentEvent(ev); + var self = this; + if (this.browser.opera) { + /* + * @knownissue Opera appears to stop the MouseDown, Click and DoubleClick events on an image inside of a document with designMode on.. + * @browser Opera + * @description This work around traps the MouseUp event and sets a timer to check if another MouseUp event fires in so many seconds. If another event is fired, they we internally fire the DoubleClick event. + */ + var sel = Event.getTarget(ev); + if (this._isElement(sel, 'img')) { + this.nodeChange(); + if (this.operaEvent) { + clearTimeout(this.operaEvent); + this.operaEvent = null; + this._handleDoubleClick(ev); + } else { + this.operaEvent = window.setTimeout(function() { + self.operaEvent = false; + }, 700); + } + } + } + //This will stop Safari from selecting the entire document if you select all the text in the editor + if (this.browser.webkit || this.browser.opera) { + if (this.browser.webkit) { + Event.stopEvent(ev); + } + } + this.nodeChange(); + this.fireEvent('editorMouseUp', { type: 'editorMouseUp', target: this, ev: ev }); + }, + /** + * @private + * @method _handleMouseDown + * @param {Event} ev The event we are working on. + * @description Handles all mousedown events inside the iFrame document. + */ + _handleMouseDown: function(ev) { + var ret = this.fireEvent('beforeEditorMouseDown', { type: 'beforeEditorMouseDown', target: this, ev: ev }); + if (ret === false) { + return false; + } + if (this._isNonEditable(ev)) { + return false; + } + this._setCurrentEvent(ev); + var sel = Event.getTarget(ev); + if (this.browser.webkit && this._hasSelection()) { + var _sel = this._getSelection(); + if (!this.browser.webkit3) { + _sel.collapse(true); + } else { + _sel.collapseToStart(); + } + } + if (this.browser.webkit && this._lastImage) { + Dom.removeClass(this._lastImage, 'selected'); + this._lastImage = null; + } + if (this._isElement(sel, 'img') || this._isElement(sel, 'a')) { + if (this.browser.webkit) { + Event.stopEvent(ev); + if (this._isElement(sel, 'img')) { + Dom.addClass(sel, 'selected'); + this._lastImage = sel; + } + } + if (this.currentWindow) { + this.closeWindow(); + } + this.nodeChange(); + } + this.fireEvent('editorMouseDown', { type: 'editorMouseDown', target: this, ev: ev }); + }, + /** + * @private + * @method _handleDoubleClick + * @param {Event} ev The event we are working on. + * @description Handles all doubleclick events inside the iFrame document. + */ + _handleDoubleClick: function(ev) { + var ret = this.fireEvent('beforeEditorDoubleClick', { type: 'beforeEditorDoubleClick', target: this, ev: ev }); + if (ret === false) { + return false; + } + if (this._isNonEditable(ev)) { + return false; + } + this._setCurrentEvent(ev); + var sel = Event.getTarget(ev); + if (this._isElement(sel, 'img')) { + this.currentElement[0] = sel; + this.toolbar.fireEvent('insertimageClick', { type: 'insertimageClick', target: this.toolbar }); + this.fireEvent('afterExecCommand', { type: 'afterExecCommand', target: this }); + } else if (this._hasParent(sel, 'a')) { //Handle elements inside an a + this.currentElement[0] = this._hasParent(sel, 'a'); + this.toolbar.fireEvent('createlinkClick', { type: 'createlinkClick', target: this.toolbar }); + this.fireEvent('afterExecCommand', { type: 'afterExecCommand', target: this }); + } + this.nodeChange(); + this.fireEvent('editorDoubleClick', { type: 'editorDoubleClick', target: this, ev: ev }); + }, + /** + * @private + * @method _handleKeyUp + * @param {Event} ev The event we are working on. + * @description Handles all keyup events inside the iFrame document. + */ + _handleKeyUp: function(ev) { + var ret = this.fireEvent('beforeEditorKeyUp', { type: 'beforeEditorKeyUp', target: this, ev: ev }); + if (ret === false) { + return false; + } + if (this._isNonEditable(ev)) { + return false; + } + this._setCurrentEvent(ev); + switch (ev.keyCode) { + case this._keyMap.SELECT_ALL.key: + if (this._checkKey(this._keyMap.SELECT_ALL, ev)) { + this.nodeChange(); + } + break; + case 32: //Space Bar + case 35: //End + case 36: //Home + case 37: //Left Arrow + case 38: //Up Arrow + case 39: //Right Arrow + case 40: //Down Arrow + case 46: //Forward Delete + case 8: //Delete + case this._keyMap.CLOSE_WINDOW.key: //W key if window is open + if ((ev.keyCode == this._keyMap.CLOSE_WINDOW.key) && this.currentWindow) { + if (this._checkKey(this._keyMap.CLOSE_WINDOW, ev)) { + this.closeWindow(); + } + } else { + if (!this.browser.ie) { + if (this._nodeChangeTimer) { + clearTimeout(this._nodeChangeTimer); + } + var self = this; + this._nodeChangeTimer = setTimeout(function() { + self._nodeChangeTimer = null; + self.nodeChange.call(self); + }, 100); + } else { + this.nodeChange(); + } + this.editorDirty = true; + } + break; + } + this.fireEvent('editorKeyUp', { type: 'editorKeyUp', target: this, ev: ev }); + this._storeUndo(); + }, + /** + * @private + * @method _handleKeyPress + * @param {Event} ev The event we are working on. + * @description Handles all keypress events inside the iFrame document. + */ + _handleKeyPress: function(ev) { + var ret = this.fireEvent('beforeEditorKeyPress', { type: 'beforeEditorKeyPress', target: this, ev: ev }); + if (ret === false) { + return false; + } + + if (this.get('allowNoEdit')) { + //if (ev && ev.keyCode && ((ev.keyCode == 46) || ev.keyCode == 63272)) { + if (ev && ev.keyCode && (ev.keyCode == 63272)) { + //Forward delete key + YAHOO.log('allowNoEdit is set, forward delete key has been disabled', 'warn', 'SimpleEditor'); + Event.stopEvent(ev); + } + } + if (this._isNonEditable(ev)) { + return false; + } + this._setCurrentEvent(ev); + if (this.browser.opera) { + if (ev.keyCode === 13) { + var tar = this._getSelectedElement(); + if (!this._isElement(tar, 'li')) { + this.execCommand('inserthtml', '
                        '); + Event.stopEvent(ev); + } + } + } + if (this.browser.webkit) { + if (!this.browser.webkit3) { + if (ev.keyCode && (ev.keyCode == 122) && (ev.metaKey)) { + //This is CMD + z (for undo) + if (this._hasParent(this._getSelectedElement(), 'li')) { + YAHOO.log('We are in an LI and we found CMD + z, stopping the event', 'warn', 'SimpleEditor'); + Event.stopEvent(ev); + } + } + } + this._listFix(ev); + } + this._fixListDupIds(); + this.fireEvent('editorKeyPress', { type: 'editorKeyPress', target: this, ev: ev }); + }, + /** + * @private + * @method _handleKeyDown + * @param {Event} ev The event we are working on. + * @description Handles all keydown events inside the iFrame document. + */ + _handleKeyDown: function(ev) { + var ret = this.fireEvent('beforeEditorKeyDown', { type: 'beforeEditorKeyDown', target: this, ev: ev }); + if (ret === false) { + return false; + } + var tar = null, _range = null; + if (this._isNonEditable(ev)) { + return false; + } + this._setCurrentEvent(ev); + if (this.currentWindow) { + this.closeWindow(); + } + if (this.currentWindow) { + this.closeWindow(); + } + var doExec = false, + action = null, + value = null, + exec = false; + + //YAHOO.log('keyCode: ' + ev.keyCode, 'info', 'SimpleEditor'); + + switch (ev.keyCode) { + case this._keyMap.FOCUS_TOOLBAR.key: + if (this._checkKey(this._keyMap.FOCUS_TOOLBAR, ev)) { + var h = this.toolbar.getElementsByTagName('h2')[0]; + if (h && h.firstChild) { + h.firstChild.focus(); + } + } else if (this._checkKey(this._keyMap.FOCUS_AFTER, ev)) { + //Focus After Element - Esc + this.afterElement.focus(); + } + Event.stopEvent(ev); + doExec = false; + break; + //case 76: //L + case this._keyMap.CREATE_LINK.key: //L + if (this._hasSelection()) { + if (this._checkKey(this._keyMap.CREATE_LINK, ev)) { + var makeLink = true; + if (this.get('limitCommands')) { + if (!this.toolbar.getButtonByValue('createlink')) { + YAHOO.log('Toolbar Button for (createlink) was not found, skipping exec.', 'info', 'SimpleEditor'); + makeLink = false; + } + } + if (makeLink) { + this.execCommand('createlink', ''); + this.toolbar.fireEvent('createlinkClick', { type: 'createlinkClick', target: this.toolbar }); + this.fireEvent('afterExecCommand', { type: 'afterExecCommand', target: this }); + doExec = false; + } + } + } + break; + //case 90: //Z + case this._keyMap.UNDO.key: + case this._keyMap.REDO.key: + if (this._checkKey(this._keyMap.REDO, ev)) { + action = 'redo'; + doExec = true; + } else if (this._checkKey(this._keyMap.UNDO, ev)) { + action = 'undo'; + doExec = true; + } + break; + //case 66: //B + case this._keyMap.BOLD.key: + if (this._checkKey(this._keyMap.BOLD, ev)) { + action = 'bold'; + doExec = true; + } + break; + case this._keyMap.FONT_SIZE_UP.key: + case this._keyMap.FONT_SIZE_DOWN.key: + var uk = false, dk = false; + if (this._checkKey(this._keyMap.FONT_SIZE_UP, ev)) { + uk = true; + } + if (this._checkKey(this._keyMap.FONT_SIZE_DOWN, ev)) { + dk = true; + } + if (uk || dk) { + var fs_button = this.toolbar.getButtonByValue('fontsize'), + label = parseInt(fs_button.get('label'), 10), + newValue = (label + 1); + + if (dk) { + newValue = (label - 1); + } + + action = 'fontsize'; + value = newValue + 'px'; + doExec = true; + } + break; + //case 73: //I + case this._keyMap.ITALIC.key: + if (this._checkKey(this._keyMap.ITALIC, ev)) { + action = 'italic'; + doExec = true; + } + break; + //case 85: //U + case this._keyMap.UNDERLINE.key: + if (this._checkKey(this._keyMap.UNDERLINE, ev)) { + action = 'underline'; + doExec = true; + } + break; + case 9: + if (this.browser.ie) { + //Insert a tab in Internet Explorer + _range = this._getRange(); + tar = this._getSelectedElement(); + if (!this._isElement(tar, 'li')) { + if (_range) { + _range.pasteHTML('    '); + _range.collapse(false); + _range.select(); + } + Event.stopEvent(ev); + } + } + //Firefox 3 code + if (this.browser.gecko > 1.8) { + tar = this._getSelectedElement(); + if (this._isElement(tar, 'li')) { + if (ev.shiftKey) { + this._getDoc().execCommand('outdent', null, ''); + } else { + this._getDoc().execCommand('indent', null, ''); + } + + } else if (!this._hasSelection()) { + this.execCommand('inserthtml', '    '); + } + Event.stopEvent(ev); + } + break; + case 13: + var p = null, i = 0; + if (this.get('ptags') && !ev.shiftKey) { + if (this.browser.gecko) { + tar = this._getSelectedElement(); + if (!this._hasParent(tar, 'li')) { + if (this._hasParent(tar, 'p')) { + p = this._getDoc().createElement('p'); + p.innerHTML = ' '; + Dom.insertAfter(p, tar); + this._selectNode(p.firstChild); + } else if (this._isElement(tar, 'body')) { + this.execCommand('insertparagraph', null); + var ps = this._getDoc().body.getElementsByTagName('p'); + for (i = 0; i < ps.length; i++) { + if (ps[i].getAttribute('_moz_dirty') !== null) { + p = this._getDoc().createElement('p'); + p.innerHTML = ' '; + Dom.insertAfter(p, ps[i]); + this._selectNode(p.firstChild); + ps[i].removeAttribute('_moz_dirty'); + } + } + } else { + YAHOO.log('Something went wrong with paragraphs, please file a bug!!', 'error', 'SimpleEditor'); + doExec = true; + action = 'insertparagraph'; + } + Event.stopEvent(ev); + } + } + if (this.browser.webkit) { + tar = this._getSelectedElement(); + if (!this._hasParent(tar, 'li')) { + this.execCommand('insertparagraph', null); + var divs = this._getDoc().body.getElementsByTagName('div'); + for (i = 0; i < divs.length; i++) { + if (!Dom.hasClass(divs[i], 'yui-wk-div')) { + Dom.addClass(divs[i], 'yui-wk-p'); + } + } + Event.stopEvent(ev); + } + } + } else { + if (this.browser.webkit) { + tar = this._getSelectedElement(); + if (!this._hasParent(tar, 'li')) { + if (this.browser.webkit4) { + this.execCommand('insertlinebreak'); + } else { + this.execCommand('inserthtml', ''); + var holder = this._getDoc().getElementById('yui-br'), + br = this._getDoc().createElement('br'), + caret = this._getDoc().createElement('span'); + + holder.parentNode.replaceChild(br, holder); + caret.className = 'yui-non'; + caret.innerHTML = ' '; + Dom.insertAfter(caret, br); + this._selectNode(caret); + } + Event.stopEvent(ev); + } + } + if (this.browser.ie) { + YAHOO.log('Stopping P tags', 'info', 'SimpleEditor'); + //Insert a
                        instead of a

                        in Internet Explorer + _range = this._getRange(); + tar = this._getSelectedElement(); + if (!this._isElement(tar, 'li')) { + if (_range) { + _range.pasteHTML('
                        '); + _range.collapse(false); + _range.select(); + } + Event.stopEvent(ev); + } + } + } + break; + } + if (this.browser.ie) { + this._listFix(ev); + } + if (doExec && action) { + this.execCommand(action, value); + Event.stopEvent(ev); + this.nodeChange(); + } + this.fireEvent('editorKeyDown', { type: 'editorKeyDown', target: this, ev: ev }); + }, + /** + * @private + * @property _fixListRunning + * @type Boolean + * @description Keeps more than one _fixListDupIds from running at the same time. + */ + _fixListRunning: null, + /** + * @private + * @method _fixListDupIds + * @description Some browsers will duplicate the id of an LI when created in designMode. + * This method will fix the duplicate id issue. However it will only preserve the first element + * in the document list with the unique id. + */ + _fixListDupIds: function() { + if (this._fixListRunning) { + return false; + } + if (this._getDoc()) { + this._fixListRunning = true; + var lis = this._getDoc().body.getElementsByTagName('li'), + i = 0, ids = {}; + for (i = 0; i < lis.length; i++) { + if (lis[i].id) { + if (ids[lis[i].id]) { + lis[i].id = ''; + } + ids[lis[i].id] = true; + } + } + this._fixListRunning = false; + } + }, + /** + * @private + * @method _listFix + * @param {Event} ev The event we are working on. + * @description Handles the Enter key, Tab Key and Shift + Tab keys for List Items. + */ + _listFix: function(ev) { + //YAHOO.log('Lists Fix (' + ev.keyCode + ')', 'info', 'SimpleEditor'); + var testLi = null, par = null, preContent = false, range = null; + //Enter Key + if (this.browser.webkit) { + if (ev.keyCode && (ev.keyCode == 13)) { + if (this._hasParent(this._getSelectedElement(), 'li')) { + var tar = this._hasParent(this._getSelectedElement(), 'li'); + if (tar.previousSibling) { + if (tar.firstChild && (tar.firstChild.length == 1)) { + this._selectNode(tar); + } + } + } + } + } + //Shift + Tab Key + if (ev.keyCode && ((!this.browser.webkit3 && (ev.keyCode == 25)) || ((this.browser.webkit3 || !this.browser.webkit) && ((ev.keyCode == 9) && ev.shiftKey)))) { + testLi = this._getSelectedElement(); + if (this._hasParent(testLi, 'li')) { + testLi = this._hasParent(testLi, 'li'); + YAHOO.log('We have a SHIFT tab in an LI, reverse it..', 'info', 'SimpleEditor'); + if (this._hasParent(testLi, 'ul') || this._hasParent(testLi, 'ol')) { + YAHOO.log('We have a double parent, move up a level', 'info', 'SimpleEditor'); + par = this._hasParent(testLi, 'ul'); + if (!par) { + par = this._hasParent(testLi, 'ol'); + } + //YAHOO.log(par.previousSibling + ' :: ' + par.previousSibling.innerHTML); + if (this._isElement(par.previousSibling, 'li')) { + par.removeChild(testLi); + par.parentNode.insertBefore(testLi, par.nextSibling); + if (this.browser.ie) { + range = this._getDoc().body.createTextRange(); + range.moveToElementText(testLi); + range.collapse(false); + range.select(); + } + if (this.browser.webkit) { + this._selectNode(testLi.firstChild); + } + Event.stopEvent(ev); + } + } + } + } + //Tab Key + if (ev.keyCode && ((ev.keyCode == 9) && (!ev.shiftKey))) { + YAHOO.log('List Fix - Tab', 'info', 'SimpleEditor'); + var preLi = this._getSelectedElement(); + if (this._hasParent(preLi, 'li')) { + preContent = this._hasParent(preLi, 'li').innerHTML; + } + //YAHOO.log('preLI: ' + preLi.tagName + ' :: ' + preLi.innerHTML); + if (this.browser.webkit) { + this._getDoc().execCommand('inserttext', false, '\t'); + } + testLi = this._getSelectedElement(); + if (this._hasParent(testLi, 'li')) { + YAHOO.log('We have a tab in an LI', 'info', 'SimpleEditor'); + par = this._hasParent(testLi, 'li'); + YAHOO.log('parLI: ' + par.tagName + ' :: ' + par.innerHTML); + var newUl = this._getDoc().createElement(par.parentNode.tagName.toLowerCase()); + if (this.browser.webkit) { + var span = Dom.getElementsByClassName('Apple-tab-span', 'span', par); + //Remove the span element that Safari puts in + if (span[0]) { + par.removeChild(span[0]); + par.innerHTML = Lang.trim(par.innerHTML); + //Put the HTML from the LI into this new LI + if (preContent) { + par.innerHTML = '' + preContent + ' '; + } else { + par.innerHTML = '  '; + } + } + } else { + if (preContent) { + par.innerHTML = preContent + ' '; + } else { + par.innerHTML = ' '; + } + } + + par.parentNode.replaceChild(newUl, par); + newUl.appendChild(par); + if (this.browser.webkit) { + this._getSelection().setBaseAndExtent(par.firstChild, 1, par.firstChild, par.firstChild.innerText.length); + if (!this.browser.webkit3) { + par.parentNode.parentNode.style.display = 'list-item'; + setTimeout(function() { + par.parentNode.parentNode.style.display = 'block'; + }, 1); + } + } else if (this.browser.ie) { + range = this._getDoc().body.createTextRange(); + range.moveToElementText(par); + range.collapse(false); + range.select(); + } else { + this._selectNode(par); + } + Event.stopEvent(ev); + } + if (this.browser.webkit) { + Event.stopEvent(ev); + } + this.nodeChange(); + } + }, + /** + * @method nodeChange + * @param {Boolean} force Optional paramenter to skip the threshold counter + * @description Handles setting up the toolbar buttons, getting the Dom path, fixing nodes. + */ + nodeChange: function(force) { + var NCself = this; + this._storeUndo(); + if (this.get('nodeChangeDelay')) { + this._nodeChangeDelayTimer = window.setTimeout(function() { + NCself._nodeChangeDelayTimer = null; + NCself._nodeChange.apply(NCself, arguments); + }, 0); + } else { + this._nodeChange(); + } + }, + /** + * @private + * @method _nodeChange + * @param {Boolean} force Optional paramenter to skip the threshold counter + * @description Fired from nodeChange in a setTimeout. + */ + _nodeChange: function(force) { + var threshold = parseInt(this.get('nodeChangeThreshold'), 10), + thisNodeChange = Math.round(new Date().getTime() / 1000), + self = this; + + if (force === true) { + this._lastNodeChange = 0; + } + + if ((this._lastNodeChange + threshold) < thisNodeChange) { + if (this._fixNodesTimer === null) { + this._fixNodesTimer = window.setTimeout(function() { + self._fixNodes.call(self); + self._fixNodesTimer = null; + }, 0); + } + } + this._lastNodeChange = thisNodeChange; + if (this.currentEvent) { + try { + this._lastNodeChangeEvent = this.currentEvent.type; + } catch (e) {} + } + + var beforeNodeChange = this.fire('beforeNodeChange', { type: 'beforeNodeChange' }); + if (beforeNodeChange === false) { + return false; + } + if (this.get('dompath')) { + Y.later(0, this, this._writeDomPath); + } + //Check to see if we are disabled before continuing + if (!this.get('disabled')) { + if (this.STOP_NODE_CHANGE) { + //Reset this var for next action + this.STOP_NODE_CHANGE = false; + return false; + } else { + var sel = this._getSelection(), + range = this._getRange(), + el = this._getSelectedElement(), + fn_button = this.toolbar.getButtonByValue('fontname'), + fs_button = this.toolbar.getButtonByValue('fontsize'), + undo_button = this.toolbar.getButtonByValue('undo'), + redo_button = this.toolbar.getButtonByValue('redo'); + + //Handle updating the toolbar with active buttons + var _ex = {}; + if (this._lastButton) { + _ex[this._lastButton.id] = true; + //this._lastButton = null; + } + if (!this._isElement(el, 'body')) { + if (fn_button) { + _ex[fn_button.get('id')] = true; + } + if (fs_button) { + _ex[fs_button.get('id')] = true; + } + } + if (redo_button) { + delete _ex[redo_button.get('id')]; + } + this.toolbar.resetAllButtons(_ex); + + //Handle disabled buttons + for (var d = 0; d < this._disabled.length; d++) { + var _button = this.toolbar.getButtonByValue(this._disabled[d]); + if (_button && _button.get) { + if (this._lastButton && (_button.get('id') === this._lastButton.id)) { + //Skip + } else { + if (!this._hasSelection() && !this.get('insert')) { + switch (this._disabled[d]) { + case 'fontname': + case 'fontsize': + break; + default: + //No Selection - disable + this.toolbar.disableButton(_button); + } + } else { + if (!this._alwaysDisabled[this._disabled[d]]) { + this.toolbar.enableButton(_button); + } + } + if (!this._alwaysEnabled[this._disabled[d]]) { + this.toolbar.deselectButton(_button); + } + } + } + } + var path = this._getDomPath(); + var tag = null, cmd = null; + for (var i = 0; i < path.length; i++) { + tag = path[i].tagName.toLowerCase(); + if (path[i].getAttribute('tag')) { + tag = path[i].getAttribute('tag').toLowerCase(); + } + cmd = this._tag2cmd[tag]; + if (cmd === undefined) { + cmd = []; + } + if (!Lang.isArray(cmd)) { + cmd = [cmd]; + } + + //Bold and Italic styles + if (path[i].style.fontWeight.toLowerCase() == 'bold') { + cmd[cmd.length] = 'bold'; + } + if (path[i].style.fontStyle.toLowerCase() == 'italic') { + cmd[cmd.length] = 'italic'; + } + if (path[i].style.textDecoration.toLowerCase() == 'underline') { + cmd[cmd.length] = 'underline'; + } + if (path[i].style.textDecoration.toLowerCase() == 'line-through') { + cmd[cmd.length] = 'strikethrough'; + } + if (cmd.length > 0) { + for (var j = 0; j < cmd.length; j++) { + this.toolbar.selectButton(cmd[j]); + this.toolbar.enableButton(cmd[j]); + } + } + //Handle Alignment + switch (path[i].style.textAlign.toLowerCase()) { + case 'left': + case 'right': + case 'center': + case 'justify': + var alignType = path[i].style.textAlign.toLowerCase(); + if (path[i].style.textAlign.toLowerCase() == 'justify') { + alignType = 'full'; + } + this.toolbar.selectButton('justify' + alignType); + this.toolbar.enableButton('justify' + alignType); + break; + } + } + //After for loop + + //Reset Font Family and Size to the inital configs + if (fn_button) { + var family = fn_button._conf.data.initValue.label; + fn_button.set('label', '' + family + ''); + this._updateMenuChecked('fontname', family); + } + + if (fs_button) { + fs_button.set('label', fs_button._conf.data.initValue.label); + } + + var hd_button = this.toolbar.getButtonByValue('heading'); + if (hd_button) { + hd_button.set('label', hd_button._conf.data.initValue.label); + this._updateMenuChecked('heading', 'none'); + } + var img_button = this.toolbar.getButtonByValue('insertimage'); + if (img_button && this.currentWindow && (this.currentWindow.name == 'insertimage')) { + this.toolbar.disableButton(img_button); + } + if (this._lastButton && this._lastButton.isSelected) { + this.toolbar.deselectButton(this._lastButton.id); + } + this._undoNodeChange(); + } + } + + this.fire('afterNodeChange', { type: 'afterNodeChange' }); + }, + /** + * @private + * @method _updateMenuChecked + * @param {Object} button The command identifier of the button you want to check + * @param {String} value The value of the menu item you want to check + * @param {YAHOO.widget.Toolbar} The Toolbar instance the button belongs to (defaults to this.toolbar) + * @description Gets the menu from a button instance, if the menu is not rendered it will render it. It will then search the menu for the specified value, unchecking all other items and checking the specified on. + */ + _updateMenuChecked: function(button, value, tbar) { + if (!tbar) { + tbar = this.toolbar; + } + var _button = tbar.getButtonByValue(button); + _button.checkValue(value); + }, + /** + * @private + * @method _handleToolbarClick + * @param {Event} ev The event that triggered the button click + * @description This is an event handler attached to the Toolbar's buttonClick event. It will fire execCommand with the command identifier from the Toolbar Button. + */ + _handleToolbarClick: function(ev) { + var value = ''; + var str = ''; + var cmd = ev.button.value; + if (ev.button.menucmd) { + value = cmd; + cmd = ev.button.menucmd; + } + this._lastButton = ev.button; + if (this.STOP_EXEC_COMMAND) { + YAHOO.log('execCommand skipped because we found the STOP_EXEC_COMMAND flag set to true', 'warn', 'SimpleEditor'); + YAHOO.log('NOEXEC::execCommand::(' + cmd + '), (' + value + ')', 'warn', 'SimpleEditor'); + this.STOP_EXEC_COMMAND = false; + return false; + } else { + this.execCommand(cmd, value); + if (!this.browser.webkit) { + var Fself = this; + setTimeout(function() { + Fself.focus.call(Fself); + }, 5); + } + } + Event.stopEvent(ev); + }, + /** + * @private + * @method _setupAfterElement + * @description Creates the accessibility h2 header and places it after the iframe in the Dom for navigation. + */ + _setupAfterElement: function() { + if (!this.beforeElement) { + this.beforeElement = Y.Node.create('

                        '); + this.beforeElement.set('innerHTML', this.STR_BEFORE_EDITOR); + this.get('element_cont').query('.first-child').insertBefore(this.beforeElement, this.toolbar.get('node').get('nextSibling')); + } + if (!this.afterElement) { + this.afterElement = Y.Node.create('

                        ' + this.STR_LEAVE_EDITOR + '

                        '); + this.get('element_cont').get('firstChild').appendChild(this.afterElement); + } + }, + /** + * @private + * @method _disableEditor + * @param {Boolean} disabled Pass true to disable, false to enable + * @description Creates a mask to place over the Editor. + */ + _disableEditor: function(disabled) { + var iframe, par, html, height; + if (!this.get('disabled_iframe') && disabled) { + iframe = this._createIframe(); + iframe.set('id', 'disabled_' + this.get('iframe').get('id')); + iframe.setStyle('height', '100%'); + iframe.setStyle('display', 'none'); + iframe.setStyle('visibility', 'visible'); + this.set('disabled_iframe', iframe); + par = this.get('iframe').get('parentNode'); + par.appendChild(iframe); + } + if (!iframe) { + iframe = this.get('disabled_iframe'); + } + if (disabled) { + this._orgIframe = this.get('iframe'); + + if (this.toolbar) { + this.toolbar.set('disabled', true); + } + + html = this.getEditorHTML(); + height = this.get('iframe').get('offsetHeight'); + iframe.setStyle('visibility', ''); + iframe.setStyle('position', ''); + iframe.setStyle('top', ''); + iframe.setStyle('left', ''); + this._orgIframe.setStyle('visibility', 'hidden'); + this._orgIframe.setStyle('position', 'absolute'); + this._orgIframe.setStyle('top', '-99999px'); + this._orgIframe.setStyle('left', '-99999px'); + this.set('iframe', iframe); + this._setInitialContent(true); + + if (!this._mask) { + this._mask = Y.Node.create('
                        '); + if (this.browser.ie) { + this._mask.setStyle('height', height + 'px'); + } + this.get('iframe').get('parentNode').appendChild(this._mask); + } + this.on('editorContentReloaded', function() { + this._getDoc().body._rteLoaded = false; + this.setEditorHTML(html); + iframe.setStyle('display', 'block'); + }); + } else { + if (this._mask) { + this._mask.get('parentNode').removeChild(this._mask); + this._mask = null; + if (this.toolbar) { + this.toolbar.set('disabled', false); + } + iframe.setStyle('visibility', 'hidden'); + iframe.setStyle('position', 'absolute'); + iframe.setStyle('top', '-99999px'); + iframe.setStyle('left', '-99999px'); + this._orgIframe.setStyle('visibility', ''); + this._orgIframe.setStyle('position', ''); + this._orgIframe.setStyle('top', ''); + this._orgIframe.setStyle('left', ''); + this.set('iframe', this._orgIframe); + + this.focus(); + Y.later(100, this, this.nodeChange); + } + } + }, + /** + * @property SEP_DOMPATH + * @description The value to place in between the Dom path items + * @type String + */ + SEP_DOMPATH: '<', + /** + * @property STR_LEAVE_EDITOR + * @description The accessibility string for the element after the iFrame + * @type String + */ + STR_LEAVE_EDITOR: 'You have left the Rich Text Editor.', + /** + * @property STR_BEFORE_EDITOR + * @description The accessibility string for the element before the iFrame + * @type String + */ + STR_BEFORE_EDITOR: 'This text field can contain stylized text and graphics. To cycle through all formatting options, use the keyboard shortcut Shift + Escape to place focus on the toolbar and navigate between options with your arrow keys. To exit this text editor use the Escape key and continue tabbing.

                        Common formatting keyboard shortcuts:

                        • Control Shift B sets text to bold
                        • Control Shift I sets text to italic
                        • Control Shift U underlines text
                        • Control Shift L adds an HTML link
                        ', + /** + * @property STR_TITLE + * @description The Title of the HTML document that is created in the iFrame + * @type String + */ + STR_TITLE: 'Rich Text Area.', + /** + * @property STR_IMAGE_HERE + * @description The text to place in the URL textbox when using the blankimage. + * @type String + */ + STR_IMAGE_HERE: 'Image URL Here', + /** + * @property STR_IMAGE_URL + * @description The label string for Image URL + * @type String + */ + STR_IMAGE_URL: 'Image URL', + /** + * @property STR_LINK_URL + * @description The label string for the Link URL. + * @type String + */ + STR_LINK_URL: 'Link URL', + /** + * @protected + * @property STOP_EXEC_COMMAND + * @description Set to true when you want the default execCommand function to not process anything + * @type Boolean + */ + STOP_EXEC_COMMAND: false, + /** + * @protected + * @property STOP_NODE_CHANGE + * @description Set to true when you want the default nodeChange function to not process anything + * @type Boolean + */ + STOP_NODE_CHANGE: false, + /** + * @protected + * @property CLASS_NOEDIT + * @description CSS class applied to elements that are not editable. + * @type String + */ + CLASS_NOEDIT: 'yui-noedit', + /** + * @protected + * @property CLASS_CONTAINER + * @description Default CSS class to apply to the editors container element + * @type String + */ + CLASS_CONTAINER: 'yui-editor-container', + /** + * @protected + * @property CLASS_EDITABLE + * @description Default CSS class to apply to the editors iframe element + * @type String + */ + CLASS_EDITABLE: 'yui-editor-editable', + /** + * @protected + * @property CLASS_EDITABLE_CONT + * @description Default CSS class to apply to the editors iframe's parent element + * @type String + */ + CLASS_EDITABLE_CONT: 'yui-editor-editable-container', + /** + * @protected + * @property CLASS_PREFIX + * @description Default prefix for dynamically created class names + * @type String + */ + CLASS_PREFIX: 'yui-editor', + /** + * @property browser + * @description Standard browser detection + * @type Object + */ + browser: function() { + var br = Y.UA; + //Check for webkit3 + if (br.webkit >= 420) { + br.webkit3 = br.webkit; + } else { + br.webkit3 = 0; + } + if (br.webkit >= 530) { + br.webkit4 = br.webkit; + } else { + br.webkit4 = 0; + } + br.mac = false; + //Check for Mac + if (navigator.userAgent.indexOf('Macintosh') !== -1) { + br.mac = true; + } + + return br; + }(), + /** + * @method init + * @description The Editor class' initialization method + */ + initializer: function() { + + if (!this._defaultToolbar) { + this._defaultToolbar = { + collapse: true, + titlebar: 'Text Editing Tools', + draggable: false, + buttons: [ + { group: 'fontstyle', label: 'Font Name and Size', + buttons: [ + { type: 'select', label: 'Arial', value: 'fontname', disabled: true, + menu: [ + { text: 'Arial', checked: true }, + { text: 'Arial Black' }, + { text: 'Comic Sans MS' }, + { text: 'Courier New' }, + { text: 'Lucida Console' }, + { text: 'Tahoma' }, + { text: 'Times New Roman' }, + { text: 'Trebuchet MS' }, + { text: 'Verdana' } + ] + }, + { type: 'spin', label: '13', value: 'fontsize', range: [ 9, 75 ], disabled: true } + ] + }, + { type: 'separator' }, + { group: 'textstyle', label: 'Font Style', + buttons: [ + { type: 'push', label: 'Bold CTRL + SHIFT + B', value: 'bold' }, + { type: 'push', label: 'Italic CTRL + SHIFT + I', value: 'italic' }, + { type: 'push', label: 'Underline CTRL + SHIFT + U', value: 'underline' }, + { type: 'push', label: 'Strike Through', value: 'strikethrough' }, + { type: 'separator' }, + { type: 'color', label: 'Font Color', value: 'forecolor', disabled: true }, + { type: 'color', label: 'Background Color', value: 'backcolor', disabled: true } + + ] + }, + { type: 'separator' }, + { group: 'indentlist', label: 'Lists', + buttons: [ + { type: 'push', label: 'Create an Unordered List', value: 'insertunorderedlist' }, + { type: 'push', label: 'Create an Ordered List', value: 'insertorderedlist' } + ] + }, + { type: 'separator' }, + { group: 'insertitem', label: 'Insert Item', + buttons: [ + { type: 'push', label: 'HTML Link CTRL + SHIFT + L', value: 'createlink', disabled: true }, + { type: 'push', label: 'Insert Image', value: 'insertimage' } + ] + } + ] + }; + } + + Y.Editor.Info._instances[this.get('node').get('id')] = this; + + this.currentElement = []; + + this._handleDOMPath(); + this.on('dompathChange', Y.bind(this._handleDOMPath, this)); + this.set('blankimage', this._getBlankImage()); + this._handleInsert(); + this.set('saveEl', this.get('node')); + this._handleSubmit(); + this.on('handleSubmitChange', Y.bind(this._handleSubmit, this)); + if (this.get('focusAtStart')) { + this.on('editorContentLoaded', function() { + Y.later(400, this, function() { + this.focus(); + this.editorDirty = false; + }); + }); + } + }, + /** + * @private + * @method _getBlankImage + * @description Retrieves the full url of the image to use as the blank image. + * @return {String} The URL to the blank image + */ + _getBlankImage: function() { + var img = ''; + if (!this._blankImageLoaded) { + if (Y.Editor.Info.blankImage) { + this.set('blankimage', Y.Editor.Info.blankImage); + this._blankImageLoaded = true; + } else { + var div = document.createElement('div'); + div.style.position = 'absolute'; + div.style.top = '-9999px'; + div.style.left = '-9999px'; + div.className = this.CLASS_PREFIX + '-blankimage'; + document.body.appendChild(div); + img = Dom.getStyle(div, 'backgroundImage'); + img = img.replace('url(', '').replace(')', '').replace(/"/g, ''); + //Adobe AIR Code + img = img.replace('app:/', ''); + this.set('blankimage', img); + this._blankImageLoaded = true; + div.parentNode.removeChild(div); + Y.Editor.Info.blankImage = img; + } + } else { + img = this.get('blankimage'); + } + return img; + }, + /** + * @private + * @method _handleAutoHeight + * @description Handles resizing the editor's height based on the content + */ + _handleAutoHeight: function() { + var doc = this._getDoc(), + body = doc.body, + docEl = doc.documentElement; + + var height = parseInt(Dom.getStyle(this.get('editor_wrapper'), 'height'), 10); + var newHeight = body.scrollHeight; + if (this.browser.webkit) { + newHeight = docEl.scrollHeight; + } + if (newHeight < parseInt(this.get('height'), 10)) { + newHeight = parseInt(this.get('height'), 10); + } + if ((height != newHeight) && (newHeight >= parseInt(this.get('height'), 10))) { + var anim = this.get('animate'); + this.set('animate', false); + this.set('height', newHeight + 'px'); + this.set('animate', anim); + if (this.browser.ie) { + //Internet Explorer needs this + this.get('iframe').setStyle('height', '99%'); + this.get('iframe').setStyle('zoom', '1'); + var self = this; + window.setTimeout(function() { + self.get('iframe').setStyle('height', '100%'); + }, 1); + } + } + }, + /** + * @private + * @property _formButtons + * @description Array of buttons that are in the Editor's parent form (for handleSubmit) + * @type Array + */ + _formButtons: null, + /** + * @private + * @property _formButtonClicked + * @description The form button that was clicked to submit the form. + * @type HTMLElement + */ + _formButtonClicked: null, + /** + * @private + * @method _handleFormButtonClick + * @description The click listener assigned to each submit button in the Editor's parent form. + * @param {Event} ev The click event + */ + _handleFormButtonClick: function(ev) { + var tar = Event.getTarget(ev); + this._formButtonClicked = tar; + }, + /** + * @private + * @method _handleFormSubmit + * @description Handles the form submission. + * @param {Object} ev The Form Submit Event + */ + _handleFormSubmit: function(ev) { + this.saveHTML(); + + var form = this.get('node').get('form'), + tar = this._formButtonClicked || false; + + form.detach('submit', Y.bind(this._handleFormSubmit, this)); + //console.log(form); + /* + if (this.browser.ie) { + if (tar && !tar.disabled) { + tar.click(); + } + } else { // Gecko, Opera, and Safari + if (tar && !tar.disabled) { + tar.click(); + } + var oEvent = document.createEvent("HTMLEvents"); + oEvent.initEvent("submit", true, true); + form.dispatchEvent(oEvent); + if (YAHOO.env.ua.webkit) { + if (YAHOO.lang.isFunction(form.submit)) { + form.submit(); + } + } + } + */ + //2.6.0 + //Removed this, not need since removing Safari 2.x + //Event.stopEvent(ev); + }, + /** + * @private + * @method _handleFontSize + * @description Handles the font size button in the toolbar. + * @param {Object} o Object returned from Toolbar's buttonClick Event + */ + _handleFontSize: function(o) { + var button = this.toolbar.getButtonById(o.button.id); + var value = button.get('label') + 'px'; + this.execCommand('fontsize', value); + return false; + }, + /** + * @private + * @description Handles the colorpicker buttons in the toolbar. + * @param {Object} o Object returned from Toolbar's buttonClick Event + */ + _handleColorPicker: function(o) { + var cmd = o.button; + var value = '#' + o.color; + if ((cmd == 'forecolor') || (cmd == 'backcolor')) { + this.execCommand(cmd, value); + } + }, + /** + * @private + * @method _handleAlign + * @description Handles the alignment buttons in the toolbar. + * @param {Object} o Object returned from Toolbar's buttonClick Event + */ + _handleAlign: function(o) { + var cmd = null; + for (var i = 0; i < o.button.menu.length; i++) { + if (o.button.menu[i].value == o.button.value) { + cmd = o.button.menu[i].value; + } + } + var value = this._getSelection(); + + this.execCommand(cmd, value); + return false; + }, + /** + * @private + * @method _handleAfterNodeChange + * @description Fires after a nodeChange happens to setup the things that where reset on the node change (button state). + */ + _handleAfterNodeChange: function() { + var path = this._getDomPath(), + elm = null, + family = null, + fontsize = null, + validFont = false, + fn_button = this.toolbar.getButtonByValue('fontname'), + fs_button = this.toolbar.getButtonByValue('fontsize'), + hd_button = this.toolbar.getButtonByValue('heading'); + + for (var i = 0; i < path.length; i++) { + elm = path[i]; + + var tag = elm.tagName.toLowerCase(); + + + if (elm.getAttribute('tag')) { + tag = elm.getAttribute('tag'); + } + + family = elm.getAttribute('face'); + if (Dom.getStyle(elm, 'fontFamily')) { + family = Dom.getStyle(elm, 'fontFamily'); + //Adobe AIR Code + family = family.replace(/'/g, ''); + } + + if (tag.substring(0, 1) == 'h') { + if (hd_button) { + /* + for (var h = 0; h < hd_button._conf.data.initValue.menu.length; h++) { + if (hd_button._conf.data.initValue.menu.value[h].value.toLowerCase() == tag) { + hd_button.set('label', hd_button._configs.menu.value[h].text); + } + } + this._updateMenuChecked('heading', tag); + */ + } + } + } + + if (fn_button) { + /* + for (var b = 0; b < fn_button._conf.data.initValue.menu.value.length; b++) { + if (family && fn_button._configs.menu.value[b].text.toLowerCase() == family.toLowerCase()) { + validFont = true; + family = fn_button._configs.menu.value[b].text; //Put the proper menu name in the button + } + } + */ + if (!validFont) { + family = fn_button._conf.data.initValue.label; + } + var familyLabel = '' + family + ''; + if (fn_button.get('label') != familyLabel) { + fn_button.set('label', familyLabel); + this._updateMenuChecked('fontname', family); + } + } + + if (fs_button) { + fontsize = parseInt(Dom.getStyle(elm, 'fontSize'), 10); + if ((fontsize === null) || isNaN(fontsize)) { + fontsize = fs_button._conf.data.initValue.label; + } + fs_button.set('label', ''+fontsize); + } + + if (!this._isElement(elm, 'body') && !this._isElement(elm, 'img')) { + this.toolbar.enableButton(fn_button); + this.toolbar.enableButton(fs_button); + this.toolbar.enableButton('forecolor'); + this.toolbar.enableButton('backcolor'); + } + if (this._isElement(elm, 'img')) { + if (Y.Overlay) { + this.toolbar.enableButton('createlink'); + } + } + if (this._hasParent(elm, 'blockquote')) { + this.toolbar.selectButton('indent'); + this.toolbar.disableButton('indent'); + this.toolbar.enableButton('outdent'); + } + if (this._hasParent(elm, 'ol') || this._hasParent(elm, 'ul')) { + this.toolbar.disableButton('indent'); + } + this._lastButton = null; + + }, + /** + * @private + * @method _handleInsertImageClick + * @description Opens the Image Properties Window when the insert Image button is clicked or an Image is Double Clicked. + */ + _handleInsertImageClick: function() { + if (this.get('limitCommands')) { + if (!this.toolbar.getButtonByValue('insertimage')) { + YAHOO.log('Toolbar Button for (insertimage) was not found, skipping exec.', 'info', 'SimpleEditor'); + return false; + } + } + + this.toolbar.set('disabled', true); //Disable the toolbar when the prompt is showing + var _handleAEC = function() { + var el = this.currentElement[0], + src = 'http://'; + if (!el) { + el = this._getSelectedElement(); + } + if (el) { + if (el.getAttribute('src')) { + src = el.getAttribute('src', 2); + if (src.indexOf(this.get('blankimage')) != -1) { + src = this.STR_IMAGE_HERE; + } + } + } + var str = prompt(this.STR_IMAGE_URL + ': ', src); + if ((str !== '') && (str !== null)) { + el.setAttribute('src', str); + } else if (str === '') { + el.parentNode.removeChild(el); + this.currentElement = []; + this.nodeChange(); + } else if ((str === null)) { + src = el.getAttribute('src', 2); + if (src.indexOf(this.get('blankimage')) != -1) { + el.parentNode.removeChild(el); + this.currentElement = []; + this.nodeChange(); + } + } + this.closeWindow(); + this.toolbar.set('disabled', false); + this.unsubscribe('afterExecCommand', _handleAEC, this, true); + }; + this.on('afterExecCommand', _handleAEC, this, true); + }, + /** + * @private + * @method _handleInsertImageWindowClose + * @description Handles the closing of the Image Properties Window. + */ + _handleInsertImageWindowClose: function() { + this.nodeChange(); + }, + /** + * @private + * @method _isLocalFile + * @param {String} url THe url/string to check + * @description Checks to see if a string (href or img src) is possibly a local file reference.. + */ + _isLocalFile: function(url) { + if ((url) && (url !== '') && ((url.indexOf('file:/') != -1) || (url.indexOf(':\\') != -1))) { + return true; + } + return false; + }, + /** + * @private + * @method _handleCreateLinkClick + * @description Handles the opening of the Link Properties Window when the Create Link button is clicked or an href is doubleclicked. + */ + _handleCreateLinkClick: function() { + if (this.get('limitCommands')) { + if (!this.toolbar.getButtonByValue('createlink')) { + YAHOO.log('Toolbar Button for (createlink) was not found, skipping exec.', 'info', 'SimpleEditor'); + return false; + } + } + + this.toolbar.set('disabled', true); //Disable the toolbar when the prompt is showing + + var _handleAEC = function() { + var el = this.currentElement[0], + url = ''; + + if (el) { + if (el.getAttribute('href', 2) !== null) { + url = el.getAttribute('href', 2); + } + } + var str = prompt(this.STR_LINK_URL + ': ', url); + if ((str !== '') && (str !== null)) { + var urlValue = str; + if ((urlValue.indexOf(':/'+'/') == -1) && (urlValue.substring(0,1) != '/') && (urlValue.substring(0, 6).toLowerCase() != 'mailto')) { + if ((urlValue.indexOf('@') != -1) && (urlValue.substring(0, 6).toLowerCase() != 'mailto')) { + //Found an @ sign, prefix with mailto: + urlValue = 'mailto:' + urlValue; + } else { + /* :// not found adding */ + if (urlValue.substring(0, 1) != '#') { + //urlValue = 'http:/'+'/' + urlValue; + } + } + } + el.setAttribute('href', urlValue); + } else if (str !== null) { + var _span = this._getDoc().createElement('span'); + _span.innerHTML = el.innerHTML; + Dom.addClass(_span, 'yui-non'); + el.parentNode.replaceChild(_span, el); + } + this.closeWindow(); + this.toolbar.set('disabled', false); + this.unsubscribe('afterExecCommand', _handleAEC, this, true); + }; + this.on('afterExecCommand', _handleAEC, this); + + }, + /** + * @private + * @method _handleCreateLinkWindowClose + * @description Handles the closing of the Link Properties Window. + */ + _handleCreateLinkWindowClose: function() { + this.nodeChange(); + this.currentElement = []; + }, + /** + * @method render + * @description Calls the private method _render in a setTimeout to allow for other things on the page to continue to load. + */ + render: function() { + if (this._rendered) { + return false; + } + YAHOO.log('Render', 'info', 'SimpleEditor'); + + if (this.get('node')) { + this._textarea = true; + if (this.get('node').get('tagName').toLowerCase() !== 'textarea') { + this._textarea = false; + } + } else { + YAHOO.log('No Element', 'error', 'SimpleEditor'); + return false; + } + this._rendered = true; + Y.later(4, this, this._render); + }, + /** + * @private + * @method _render + * @description Causes the toolbar and the editor to render and replace the textarea. + */ + _render: function() { + YAHOO.log('_render'); + this.set('textarea', this.get('node')); + + this.get('element_cont').setStyle('display', 'none'); + this.get('element_cont').addClass(this.CLASS_CONTAINER); + + this.set('iframe', this._createIframe()); + + Y.later(10, this, this._setInitialContent, false); + + this.get('editor_wrapper').appendChild(this.get('iframe')); + + if (this.get('disabled')) { + this._disableEditor(true); + } + + var tbarConf = this.get('toolbar'); + if (!tbarConf) { + tbarConf = this._defaultToolbar; + } + //Create Toolbar instance + if (tbarConf instanceof Y.Toolbar.Bar) { + this.toolbar = tbarConf; + //Set the toolbar to disabled until content is loaded + this.toolbar.set('disabled', true); + } else { + tbarConf.node = this.get('toolbar_cont'); + //Set the toolbar to disabled until content is loaded + tbarConf.disabled = true; + this.toolbar = new Y.Toolbar.Bar(tbarConf); + } + + YAHOO.log('fireEvent::toolbarLoaded', 'info', 'SimpleEditor'); + this.fire('toolbarLoaded', { type: 'toolbarLoaded' }); + + + this.toolbar.on('toolbarCollapsed', function() { + if (this.currentWindow) { + this.moveWindow(); + } + }, this, true); + this.toolbar.on('toolbarExpanded', function() { + if (this.currentWindow) { + this.moveWindow(); + } + }, this, true); + this.toolbar.on('fontsizeClick', this._handleFontSize, this, true); + + this.toolbar.on('colorPickerClicked', function(o) { + this._handleColorPicker(o); + this.STOP_EXEC_COMMAND = true; + }, this, true); + + this.toolbar.on('alignClick', this._handleAlign, this, true); + this.on('afterNodeChange', this._handleAfterNodeChange, this, true); + this.toolbar.on('insertimageClick', this._handleInsertImageClick, this, true); + this.on('windowinsertimageClose', this._handleInsertImageWindowClose, this, true); + this.toolbar.on('createlinkClick', this._handleCreateLinkClick, this, true); + this.on('windowcreatelinkClose', this._handleCreateLinkWindowClose, this, true); + + + //Replace Textarea with editable area + this.get('node').get('parentNode').replaceChild(this.get('element_cont'), this.get('node')); + + this.get('node').setStyle('visibility', 'hidden'); + this.get('node').setStyle('position', 'absolute'); + this.get('node').setStyle('top', '-9999px'); + this.get('node').setStyle('left', '-9999px'); + this.get('element_cont').appendChild(this.get('node')); + this.get('element_cont').setStyle('display', 'block'); + + + this.get('iframe').get('parentNode').addClass(this.CLASS_EDITABLE_CONT); + this.get('iframe').addClass(this.CLASS_EDITABLE); + + //Set height and width of editor container + this.get('element_cont').setStyle('width', this.get('width')); + this.get('iframe').get('parentNode').setStyle('height', this.get('height')); + + this.get('iframe').setStyle('width', '100%'); //WIDTH + this.get('iframe').setStyle('height', '100%'); + + Y.later(0, this, this._setupAfterElement); + this.fire('afterRender', { type: 'afterRender' }); + }, + /** + * @method execCommand + * @param {String} action The "execCommand" action to try to execute (Example: bold, insertimage, inserthtml) + * @param {String} value (optional) The value for a given action such as action: fontname value: 'Verdana' + * @description This method attempts to try and level the differences in the various browsers and their support for execCommand actions + */ + execCommand: function(action, value) { + var beforeExec = this.fireEvent('beforeExecCommand', { type: 'beforeExecCommand', target: this, args: arguments }); + if ((beforeExec === false) || (this.STOP_EXEC_COMMAND)) { + this.STOP_EXEC_COMMAND = false; + return false; + } + this._lastCommand = action; + this._setMarkupType(action); + if (this.browser.ie) { + this._getWindow().focus(); + } + var exec = true; + + if (this.get('limitCommands')) { + if (!this.toolbar.getButtonByValue(action)) { + YAHOO.log('Toolbar Button for (' + action + ') was not found, skipping exec.', 'info', 'SimpleEditor'); + exec = false; + } + } + + this.editorDirty = true; + + if ((typeof this['cmd_' + action.toLowerCase()] == 'function') && exec) { + YAHOO.log('Found execCommand override method: (cmd_' + action.toLowerCase() + ')', 'info', 'SimpleEditor'); + var retValue = this['cmd_' + action.toLowerCase()](value); + exec = retValue[0]; + if (retValue[1]) { + action = retValue[1]; + } + if (retValue[2]) { + value = retValue[2]; + } + } + if (exec) { + YAHOO.log('execCommand::(' + action + '), (' + value + ')', 'info', 'SimpleEditor'); + try { + this._getDoc().execCommand(action, false, value); + } catch(e) { + YAHOO.log('execCommand Failed', 'error', 'SimpleEditor'); + } + } else { + YAHOO.log('OVERRIDE::execCommand::(' + action + '),(' + value + ') skipped', 'warn', 'SimpleEditor'); + } + this.on('afterExecCommand', function() { + this.unsubscribeAll('afterExecCommand'); + this.nodeChange(); + }, this, true); + this.fireEvent('afterExecCommand', { type: 'afterExecCommand', target: this }); + + }, + /* {{{ Command Overrides */ + + /** + * @method cmd_bold + * @param value Value passed from the execCommand method + * @description This is an execCommand override method. It is called from execCommand when the execCommand('bold') is used. + */ + cmd_bold: function(value) { + if (!this.browser.webkit) { + var el = this._getSelectedElement(); + if (el && this._isElement(el, 'span') && this._hasSelection()) { + if (el.style.fontWeight == 'bold') { + el.style.fontWeight = ''; + var b = this._getDoc().createElement('b'), + par = el.parentNode; + par.replaceChild(b, el); + b.appendChild(el); + } + } + } + return [true]; + }, + /** + * @method cmd_italic + * @param value Value passed from the execCommand method + * @description This is an execCommand override method. It is called from execCommand when the execCommand('italic') is used. + */ + + cmd_italic: function(value) { + if (!this.browser.webkit) { + var el = this._getSelectedElement(); + if (el && this._isElement(el, 'span') && this._hasSelection()) { + if (el.style.fontStyle == 'italic') { + el.style.fontStyle = ''; + var i = this._getDoc().createElement('i'), + par = el.parentNode; + par.replaceChild(i, el); + i.appendChild(el); + } + } + } + return [true]; + }, + + + /** + * @method cmd_underline + * @param value Value passed from the execCommand method + * @description This is an execCommand override method. It is called from execCommand when the execCommand('underline') is used. + */ + cmd_underline: function(value) { + if (!this.browser.webkit) { + var el = this._getSelectedElement(); + if (el && this._isElement(el, 'span')) { + if (el.style.textDecoration == 'underline') { + el.style.textDecoration = 'none'; + } else { + el.style.textDecoration = 'underline'; + } + return [false]; + } + } + return [true]; + }, + /** + * @method cmd_backcolor + * @param value Value passed from the execCommand method + * @description This is an execCommand override method. It is called from execCommand when the execCommand('backcolor') is used. + */ + cmd_backcolor: function(value) { + var exec = true, + el = this._getSelectedElement(), + action = 'backcolor'; + + if (this.browser.gecko || this.browser.opera) { + this._setEditorStyle(true); + action = 'hilitecolor'; + } + + if (!this._isElement(el, 'body') && !this._hasSelection()) { + el.style.backgroundColor = value; + this._selectNode(el); + exec = false; + } else { + if (this.get('insert')) { + el = this._createInsertElement({ backgroundColor: value }); + } else { + this._createCurrentElement('span', { backgroundColor: value, color: el.style.color, fontSize: el.style.fontSize, fontFamily: el.style.fontFamily }); + this._selectNode(this.currentElement[0]); + } + exec = false; + } + + return [exec, action]; + }, + /** + * @method cmd_forecolor + * @param value Value passed from the execCommand method + * @description This is an execCommand override method. It is called from execCommand when the execCommand('forecolor') is used. + */ + cmd_forecolor: function(value) { + var exec = true, + el = this._getSelectedElement(); + + if (!this._isElement(el, 'body') && !this._hasSelection()) { + Dom.setStyle(el, 'color', value); + this._selectNode(el); + exec = false; + } else { + if (this.get('insert')) { + el = this._createInsertElement({ color: value }); + } else { + this._createCurrentElement('span', { color: value, fontSize: el.style.fontSize, fontFamily: el.style.fontFamily, backgroundColor: el.style.backgroundColor }); + this._selectNode(this.currentElement[0]); + } + exec = false; + } + return [exec]; + }, + /** + * @method cmd_unlink + * @param value Value passed from the execCommand method + * @description This is an execCommand override method. It is called from execCommand when the execCommand('unlink') is used. + */ + cmd_unlink: function(value) { + this._swapEl(this.currentElement[0], 'span', function(el) { + el.className = 'yui-non'; + }); + return [false]; + }, + /** + * @method cmd_createlink + * @param value Value passed from the execCommand method + * @description This is an execCommand override method. It is called from execCommand when the execCommand('createlink') is used. + */ + cmd_createlink: function(value) { + var el = this._getSelectedElement(), _a = null; + if (this._hasParent(el, 'a')) { + this.currentElement[0] = this._hasParent(el, 'a'); + } else if (this._isElement(el, 'li')) { + _a = this._getDoc().createElement('a'); + _a.innerHTML = el.innerHTML; + el.innerHTML = ''; + el.appendChild(_a); + this.currentElement[0] = _a; + } else if (!this._isElement(el, 'a')) { + this._createCurrentElement('a'); + _a = this._swapEl(this.currentElement[0], 'a'); + this.currentElement[0] = _a; + } else { + this.currentElement[0] = el; + } + return [false]; + }, + /** + * @method cmd_insertimage + * @param value Value passed from the execCommand method + * @description This is an execCommand override method. It is called from execCommand when the execCommand('insertimage') is used. + */ + cmd_insertimage: function(value) { + var exec = true, _img = null, action = 'insertimage', + el = this._getSelectedElement(); + + if (value === '') { + value = this.get('blankimage'); + } + + /* + * @knownissue Safari Cursor Position + * @browser Safari 2.x + * @description The issue here is that we have no way of knowing where the cursor position is + * inside of the iframe, so we have to place the newly inserted data in the best place that we can. + */ + + YAHOO.log('InsertImage: ' + el.tagName, 'info', 'SimpleEditor'); + if (this._isElement(el, 'img')) { + this.currentElement[0] = el; + exec = false; + } else { + if (this._getDoc().queryCommandEnabled(action)) { + this._getDoc().execCommand(action, false, value); + var imgs = this._getDoc().getElementsByTagName('img'); + for (var i = 0; i < imgs.length; i++) { + if (!Dom.hasClass(imgs[i], 'yui-img')) { + Dom.addClass(imgs[i], 'yui-img'); + this.currentElement[0] = imgs[i]; + } + } + exec = false; + } else { + if (el == this._getDoc().body) { + _img = this._getDoc().createElement('img'); + _img.setAttribute('src', value); + Dom.addClass(_img, 'yui-img'); + this._getDoc().body.appendChild(_img); + } else { + this._createCurrentElement('img'); + _img = this._getDoc().createElement('img'); + _img.setAttribute('src', value); + Dom.addClass(_img, 'yui-img'); + this.currentElement[0].parentNode.replaceChild(_img, this.currentElement[0]); + } + this.currentElement[0] = _img; + exec = false; + } + } + return [exec]; + }, + /** + * @method cmd_inserthtml + * @param value Value passed from the execCommand method + * @description This is an execCommand override method. It is called from execCommand when the execCommand('inserthtml') is used. + */ + cmd_inserthtml: function(value) { + var exec = true, action = 'inserthtml', _span = null, _range = null; + /* + * @knownissue Safari cursor position + * @browser Safari 2.x + * @description The issue here is that we have no way of knowing where the cursor position is + * inside of the iframe, so we have to place the newly inserted data in the best place that we can. + */ + if (this.browser.webkit && !this._getDoc().queryCommandEnabled(action)) { + YAHOO.log('More Safari DOM tricks (inserthtml)', 'info', 'EditorSafari'); + this._createCurrentElement('img'); + _span = this._getDoc().createElement('span'); + _span.innerHTML = value; + this.currentElement[0].parentNode.replaceChild(_span, this.currentElement[0]); + exec = false; + } else if (this.browser.ie) { + _range = this._getRange(); + if (_range.item) { + _range.item(0).outerHTML = value; + } else { + _range.pasteHTML(value); + } + exec = false; + } + return [exec]; + }, + /** + * @method cmd_list + * @param tag The tag of the list you want to create (eg, ul or ol) + * @description This is a combined execCommand override method. It is called from the cmd_insertorderedlist and cmd_insertunorderedlist methods. + */ + cmd_list: function(tag) { + var exec = true, list = null, li = 0, el = null, str = '', + selEl = this._getSelectedElement(), action = 'insertorderedlist'; + if (tag == 'ul') { + action = 'insertunorderedlist'; + } + /* + * @knownissue Safari 2.+ doesn't support ordered and unordered lists + * @browser Safari 2.x + * The issue with this workaround is that when applied to a set of text + * that has BR's in it, Safari may or may not pick up the individual items as + * list items. This is fixed in WebKit (Safari 3) + * 2.6.0: Seems there are still some issues with List Creation and Safari 3, reverting to previously working Safari 2.x code + */ + //if ((this.browser.webkit && !this._getDoc().queryCommandEnabled(action))) { + if (this.browser.webkit && !this.browser.webkit4) { + if (this._isElement(selEl, 'li') && this._isElement(selEl.parentNode, tag)) { + YAHOO.log('We already have a list, undo it', 'info', 'SimpleEditor'); + el = selEl.parentNode; + list = this._getDoc().createElement('span'); + Dom.addClass(list, 'yui-non'); + str = ''; + var lis = el.getElementsByTagName('li'); + for (li = 0; li < lis.length; li++) { + str += '
                        ' + lis[li].innerHTML + '
                        '; + } + list.innerHTML = str; + this.currentElement[0] = el; + this.currentElement[0].parentNode.replaceChild(list, this.currentElement[0]); + } else { + YAHOO.log('Create list item', 'info', 'SimpleEditor'); + this._createCurrentElement(tag.toLowerCase()); + list = this._getDoc().createElement(tag); + for (li = 0; li < this.currentElement.length; li++) { + var newli = this._getDoc().createElement('li'); + newli.innerHTML = this.currentElement[li].innerHTML + '  '; + list.appendChild(newli); + if (li > 0) { + this.currentElement[li].parentNode.removeChild(this.currentElement[li]); + } + } + + var items = list.firstChild.innerHTML.split('
                        '); + if (items.length > 0) { + list.innerHTML = ''; + for (var i = 0; i < items.length; i++) { + var item = this._getDoc().createElement('li'); + item.innerHTML = items[i]; + list.appendChild(item); + } + } + + this.currentElement[0].parentNode.replaceChild(list, this.currentElement[0]); + this.currentElement[0] = list; + var _h = this.currentElement[0].firstChild; + _h = Dom.getElementsByClassName('yui-non', 'span', _h)[0]; + this._getSelection().setBaseAndExtent(_h, 1, _h, _h.innerText.length); + } + exec = false; + } else { + el = this._getSelectedElement(); + YAHOO.log(el.tagName); + if (this._isElement(el, 'li') && this._isElement(el.parentNode, tag) || (this.browser.ie && this._isElement(this._getRange().parentElement, 'li')) || (this.browser.ie && this._isElement(el, 'ul')) || (this.browser.ie && this._isElement(el, 'ol'))) { //we are in a list.. + YAHOO.log('We already have a list, undo it', 'info', 'SimpleEditor'); + if (this.browser.ie) { + if ((this.browser.ie && this._isElement(el, 'ul')) || (this.browser.ie && this._isElement(el, 'ol'))) { + el = el.getElementsByTagName('li')[0]; + } + YAHOO.log('Undo IE', 'info', 'SimpleEditor'); + str = ''; + var lis2 = el.parentNode.getElementsByTagName('li'); + for (var j = 0; j < lis2.length; j++) { + str += lis2[j].innerHTML + '
                        '; + } + var newEl = this._getDoc().createElement('span'); + newEl.innerHTML = str; + el.parentNode.parentNode.replaceChild(newEl, el.parentNode); + } else { + this.nodeChange(); + this._getDoc().execCommand(action, '', el.parentNode); + this.nodeChange(); + } + exec = false; + } + if (this.browser.opera) { + var self = this; + window.setTimeout(function() { + var liso = self._getDoc().getElementsByTagName('li'); + for (var i = 0; i < liso.length; i++) { + if (liso[i].innerHTML.toLowerCase() == '
                        ') { + liso[i].parentNode.parentNode.removeChild(liso[i].parentNode); + } + } + },30); + } + if (this.browser.ie && exec) { + var html = ''; + if (this._getRange().html) { + html = '
                      • ' + this._getRange().html+ '
                      • '; + } else { + var t = this._getRange().text.split('\n'); + if (t.length > 1) { + html = ''; + for (var ie = 0; ie < t.length; ie++) { + html += '
                      • ' + t[ie] + '
                      • '; + } + } else { + var txt = this._getRange().text; + if (txt === '') { + html = '
                      • ' + txt + '
                      • '; + } else { + html = '
                      • ' + txt + '
                      • '; + } + } + } + this._getRange().pasteHTML('<' + tag + '>' + html + ''); + var new_item = this._getDoc().getElementById('new_list_item'); + if (new_item) { + var range = this._getDoc().body.createTextRange(); + range.moveToElementText(new_item); + range.collapse(false); + range.select(); + new_item.id = ''; + } + exec = false; + } + } + return exec; + }, + /** + * @method cmd_insertorderedlist + * @param value Value passed from the execCommand method + * @description This is an execCommand override method. It is called from execCommand when the execCommand('insertorderedlist ') is used. + */ + cmd_insertorderedlist: function(value) { + return [this.cmd_list('ol')]; + }, + /** + * @method cmd_insertunorderedlist + * @param value Value passed from the execCommand method + * @description This is an execCommand override method. It is called from execCommand when the execCommand('insertunorderedlist') is used. + */ + cmd_insertunorderedlist: function(value) { + return [this.cmd_list('ul')]; + }, + /** + * @method cmd_fontname + * @param value Value passed from the execCommand method + * @description This is an execCommand override method. It is called from execCommand when the execCommand('fontname') is used. + */ + cmd_fontname: function(value) { + var exec = true, + selEl = this._getSelectedElement(); + + this.currentFont = value; + if (selEl && selEl.tagName && !this._hasSelection() && !this._isElement(selEl, 'body') && !this.get('insert')) { + Dom.setStyle(selEl, 'fontFamily', value); + exec = false; + } else if (this.get('insert') && !this._hasSelection()) { + YAHOO.log('No selection and no selected element and we are in insert mode', 'info', 'SimpleEditor'); + var el = this._createInsertElement({ fontFamily: value }); + exec = false; + } + return [exec]; + }, + /** + * @method cmd_fontsize + * @param value Value passed from the execCommand method + * @description This is an execCommand override method. It is called from execCommand when the execCommand('fontsize') is used. + */ + cmd_fontsize: function(value) { + var el = null, go = true; + el = this._getSelectedElement(); + if (this.browser.webkit) { + if (this.currentElement[0]) { + if (el == this.currentElement[0]) { + go = false; + Dom.setStyle(el, 'fontSize', value); + this._selectNode(el); + this.currentElement[0] = el; + } + } + } + if (go) { + if (!this._isElement(this._getSelectedElement(), 'body') && (!this._hasSelection())) { + el = this._getSelectedElement(); + Dom.setStyle(el, 'fontSize', value); + if (this.get('insert') && this.browser.ie) { + var r = this._getRange(); + r.collapse(false); + r.select(); + } else { + this._selectNode(el); + } + } else if (this.currentElement && (this.currentElement.length > 0) && (!this._hasSelection()) && (!this.get('insert'))) { + Dom.setStyle(this.currentElement, 'fontSize', value); + } else { + if (this.get('insert') && !this._hasSelection()) { + el = this._createInsertElement({ fontSize: value }); + this.currentElement[0] = el; + this._selectNode(this.currentElement[0]); + } else { + this._createCurrentElement('span', {'fontSize': value, fontFamily: el.style.fontFamily, color: el.style.color, backgroundColor: el.style.backgroundColor }); + this._selectNode(this.currentElement[0]); + } + } + } + return [false]; + }, + /* }}} */ + /** + * @private + * @method _swapEl + * @param {HTMLElement} el The element to swap with + * @param {String} tagName The tagname of the element that you wish to create + * @param {Function} callback (optional) A function to run on the element after it is created, but before it is replaced. An element reference is passed to this function. + * @description This function will create a new element in the DOM and populate it with the contents of another element. Then it will assume it's place. + */ + _swapEl: function(el, tagName, callback) { + var _el = this._getDoc().createElement(tagName); + if (el) { + _el.innerHTML = el.innerHTML; + } + if (typeof callback == 'function') { + callback.call(this, _el); + } + if (el) { + el.parentNode.replaceChild(_el, el); + } + return _el; + }, + /** + * @private + * @method _createInsertElement + * @description Creates a new "currentElement" then adds some text (and other things) to make it selectable and stylable. Then the user can continue typing. + * @param {Object} css (optional) Object literal containing styles to apply to the new element. + * @return {HTMLElement} + */ + _createInsertElement: function(css) { + this._createCurrentElement('span', css); + var el = this.currentElement[0]; + if (this.browser.webkit) { + //Little Safari Hackery here.. + el.innerHTML = ' '; + el = el.firstChild; + this._getSelection().setBaseAndExtent(el, 1, el, el.innerText.length); + } else if (this.browser.ie || this.browser.opera) { + el.innerHTML = ' '; + } + this.focus(); + this._selectNode(el, true); + return el; + }, + /** + * @private + * @method _createCurrentElement + * @param {String} tagName (optional defaults to a) The tagname of the element that you wish to create + * @param {Object} tagStyle (optional) Object literal containing styles to apply to the new element. + * @description This is a work around for the various browser issues with execCommand. This method will run execCommand('fontname', false, 'yui-tmp') on the given selection. + * It will then search the document for an element with the font-family set to yui-tmp and replace that with another span that has other information in it, then assign the new span to the + * this.currentElement array, so we now have element references to the elements that were just modified. At this point we can use standard DOM manipulation to change them as we see fit. + */ + _createCurrentElement: function(tagName, tagStyle) { + tagName = ((tagName) ? tagName : 'a'); + var tar = null, + el = [], + _doc = this._getDoc(); + + if (this.currentFont) { + if (!tagStyle) { + tagStyle = {}; + } + tagStyle.fontFamily = this.currentFont; + this.currentFont = null; + } + this.currentElement = []; + + var _elCreate = function(tagName, tagStyle) { + var el = null; + tagName = ((tagName) ? tagName : 'span'); + tagName = tagName.toLowerCase(); + switch (tagName) { + case 'h1': + case 'h2': + case 'h3': + case 'h4': + case 'h5': + case 'h6': + el = _doc.createElement(tagName); + break; + default: + el = _doc.createElement(tagName); + if (tagName === 'span') { + Dom.addClass(el, 'yui-tag-' + tagName); + Dom.addClass(el, 'yui-tag'); + el.setAttribute('tag', tagName); + } + //console.log(tagStyle); + if (tagStyle) { + Y.each(tagStyle, function(v, k) { + el.style[k] = v; + }); + } + break; + } + return el; + }; + + if (!this._hasSelection()) { + if (this._getDoc().queryCommandEnabled('insertimage')) { + this._getDoc().execCommand('insertimage', false, 'yui-tmp-img'); + var imgs = this._getDoc().getElementsByTagName('img'); + for (var j = 0; j < imgs.length; j++) { + if (imgs[j].getAttribute('src', 2) == 'yui-tmp-img') { + el = _elCreate(tagName, tagStyle); + imgs[j].parentNode.replaceChild(el, imgs[j]); + this.currentElement[this.currentElement.length] = el; + } + } + } else { + if (this.currentEvent) { + tar = Event.getTarget(this.currentEvent); + } else { + //For Safari.. + tar = this._getDoc().body; + } + } + if (tar) { + /* + * @knownissue Safari Cursor Position + * @browser Safari 2.x + * @description The issue here is that we have no way of knowing where the cursor position is + * inside of the iframe, so we have to place the newly inserted data in the best place that we can. + */ + el = _elCreate(tagName, tagStyle); + if (this._isElement(tar, 'body') || this._isElement(tar, 'html')) { + if (this._isElement(tar, 'html')) { + tar = this._getDoc().body; + } + tar.appendChild(el); + } else if (tar.nextSibling) { + tar.parentNode.insertBefore(el, tar.nextSibling); + } else { + tar.parentNode.appendChild(el); + } + //this.currentElement = el; + this.currentElement[this.currentElement.length] = el; + this.currentEvent = null; + if (this.browser.webkit) { + //Force Safari to focus the new element + this._getSelection().setBaseAndExtent(el, 0, el, 0); + if (this.browser.webkit3) { + this._getSelection().collapseToStart(); + } else { + this._getSelection().collapse(true); + } + } + } + } else { + //Force CSS Styling for this action... + this._setEditorStyle(true); + this._getDoc().execCommand('fontname', false, 'yui-tmp'); + var _tmp = [], __tmp, __els = ['font', 'span', 'i', 'b', 'u', 'li']; + + if (!this._isElement(this._getSelectedElement(), 'body')) { + __els[__els.length] = this._getDoc().getElementsByTagName(this._getSelectedElement().tagName); + __els[__els.length] = this._getDoc().getElementsByTagName(this._getSelectedElement().parentNode.tagName); + } + for (var _els = 0; _els < __els.length; _els++) { + var _tmp1 = this._getDoc().getElementsByTagName(__els[_els]); + for (var e = 0; e < _tmp1.length; e++) { + _tmp[_tmp.length] = _tmp1[e]; + } + } + + + for (var i = 0; i < _tmp.length; i++) { + if ((Dom.getStyle(_tmp[i], 'fontFamily') == 'yui-tmp') || (_tmp[i].face && (_tmp[i].face == 'yui-tmp'))) { + if (tagName !== 'span') { + el = _elCreate(tagName, tagStyle); + } else { + el = _elCreate(_tmp[i].tagName, tagStyle); + } + el.innerHTML = _tmp[i].innerHTML; + if (this._isElement(_tmp[i], 'ol') || (this._isElement(_tmp[i], 'ul'))) { + var fc = _tmp[i].getElementsByTagName('li')[0]; + _tmp[i].style.fontFamily = 'inherit'; + fc.style.fontFamily = 'inherit'; + el.innerHTML = fc.innerHTML; + fc.innerHTML = ''; + fc.appendChild(el); + this.currentElement[this.currentElement.length] = el; + } else if (this._isElement(_tmp[i], 'li')) { + _tmp[i].innerHTML = ''; + _tmp[i].appendChild(el); + _tmp[i].style.fontFamily = 'inherit'; + this.currentElement[this.currentElement.length] = el; + } else { + if (_tmp[i].parentNode) { + _tmp[i].parentNode.replaceChild(el, _tmp[i]); + this.currentElement[this.currentElement.length] = el; + this.currentEvent = null; + if (this.browser.webkit) { + //Force Safari to focus the new element + this._getSelection().setBaseAndExtent(el, 0, el, 0); + if (this.browser.webkit3) { + this._getSelection().collapseToStart(); + } else { + this._getSelection().collapse(true); + } + } + if (this.browser.ie && tagStyle && tagStyle.fontSize) { + this._getSelection().empty(); + } + if (this.browser.gecko) { + this._getSelection().collapseToStart(); + } + } + } + } + } + var len = this.currentElement.length; + for (var o = 0; o < len; o++) { + if ((o + 1) != len) { //Skip the last one in the list + if (this.currentElement[o] && this.currentElement[o].nextSibling) { + if (this._isElement(this.currentElement[o], 'br')) { + this.currentElement[this.currentElement.length] = this.currentElement[o].nextSibling; + } + } + } + } + } + }, + /** + * @method saveHTML + * @description Cleans the HTML with the cleanHTML method then places that string back into the textarea. + * @return String + */ + saveHTML: function() { + var html = this.cleanHTML(); + if (this._textarea) { + this.get('node').set('value', html); + } else { + this.get('node').set('innerHTML', html); + } + if (this.get('saveEl') !== this.get('node')) { + var out = this.get('saveEl'); + if (out) { + if (out.get('tagName').toLowerCase() === 'textarea') { + out.set('value', html); + } else { + out.set('innerHTML', html); + } + } + } + return html; + }, + /** + * @method setEditorHTML + * @param {String} incomingHTML The html content to load into the editor + * @description Loads HTML into the editors body + */ + setEditorHTML: function(incomingHTML) { + var html = this._cleanIncomingHTML(incomingHTML); + html = html.replace(/RIGHT_BRACKET/gi, '{'); + html = html.replace(/LEFT_BRACKET/gi, '}'); + this._getDoc().body.innerHTML = html; + this.nodeChange(); + }, + /** + * @method getEditorHTML + * @description Gets the unprocessed/unfiltered HTML from the editor + */ + getEditorHTML: function() { + try { + var b = this._getDoc().body; + if (b === null) { + YAHOO.log('Body is null, returning null.', 'error', 'SimpleEditor'); + return null; + } + return this._getDoc().body.innerHTML; + } catch (e) { + return ''; + } + }, + /** + * @method show + * @description This method needs to be called if the Editor was hidden (like in a TabView or Panel). It is used to reset the editor after being in a container that was set to display none. + */ + show: function() { + if (this.browser.gecko) { + this._setDesignMode('on'); + this.focus(); + } + if (this.browser.webkit) { + var self = this; + window.setTimeout(function() { + self._setInitialContent.call(self); + }, 10); + } + //Adding this will close all other Editor window's when showing this one. + if (this.currentWindow) { + this.closeWindow(); + } + //Put the iframe back in place + this.get('iframe').setStyle('position', 'static'); + this.get('iframe').setStyle('left', ''); + }, + /** + * @method hide + * @description This method needs to be called if the Editor is to be hidden (like in a TabView or Panel). It should be called to clear timeouts and close open editor windows. + */ + hide: function() { + //Adding this will close all other Editor window's. + if (this.currentWindow) { + this.closeWindow(); + } + if (this._fixNodesTimer) { + clearTimeout(this._fixNodesTimer); + this._fixNodesTimer = null; + } + if (this._nodeChangeTimer) { + clearTimeout(this._nodeChangeTimer); + this._nodeChangeTimer = null; + } + this._lastNodeChange = 0; + //Move the iframe off of the screen, so that in containers with visiblity hidden, IE will not cover other elements. + this.get('iframe').setStyle('position', 'absolute'); + this.get('iframe').setStyle('left', '-9999px'); + }, + /** + * @method _cleanIncomingHTML + * @param {String} html The unfiltered HTML + * @description Process the HTML with a few regexes to clean it up and stabilize the input + * @return {String} The filtered HTML + */ + _cleanIncomingHTML: function(html) { + html = html.replace(/{/gi, 'RIGHT_BRACKET'); + html = html.replace(/}/gi, 'LEFT_BRACKET'); + + html = html.replace(/]*)>/gi, ''); + html = html.replace(/<\/strong>/gi, '
                        '); + + //replace embed before em check + html = html.replace(/]*)>/gi, ''); + html = html.replace(/<\/embed>/gi, ''); + + html = html.replace(/]*)>/gi, ''); + html = html.replace(/<\/em>/gi, '
                        '); + html = html.replace(/_moz_dirty=""/gi, ''); + + //Put embed tags back in.. + html = html.replace(/]*)>/gi, ''); + html = html.replace(/<\/YUI_EMBED>/gi, ''); + if (this.get('plainText')) { + YAHOO.log('Filtering as plain text', 'info', 'SimpleEditor'); + html = html.replace(/\n/g, '
                        ').replace(/\r/g, '
                        '); + html = html.replace(/ /gi, '  '); //Replace all double spaces + html = html.replace(/\t/gi, '    '); //Replace all tabs + } + //Removing Script Tags from the Editor + html = html.replace(/]*)>/gi, ''); + html = html.replace(/<\/script([^>]*)>/gi, ''); + html = html.replace(/<script([^>]*)>/gi, ''); + html = html.replace(/<\/script([^>]*)>/gi, ''); + //Replace the line feeds + html = html.replace(/\r\n/g, '').replace(/\n/g, '').replace(/\r/g, ''); + + //Remove Bad HTML elements (used to be script nodes) + html = html.replace(new RegExp(']*)>(.*?)<\/bad>', 'gi'), ''); + //Replace the lines feeds + html = html.replace(//g, '\n'); + return html; + }, + /** + * @method cleanHTML + * @param {String} html The unfiltered HTML + * @description Process the HTML with a few regexes to clean it up and stabilize the output + * @return {String} The filtered HTML + */ + cleanHTML: function(html) { + //Start Filtering Output + //Begin RegExs.. + if (!html) { + html = this.getEditorHTML(); + } + var markup = this.get('markup'); + //Make some backups... + html = this.pre_filter_linebreaks(html, markup); + + //Filter MS Word + html = this.filter_msword(html); + + html = html.replace(/]*)\/>/gi, ''); + html = html.replace(/]*)>/gi, ''); + + html = html.replace(/]*)\/>/gi, ''); + html = html.replace(/]*)>/gi, ''); + + html = html.replace(/]*)>/gi, ''); + html = html.replace(/<\/ul>/gi, '<\/YUI_UL>'); + html = html.replace(/]*)>/gi, ''); + html = html.replace(/<\/blockquote>/gi, '<\/YUI_BQ>'); + + html = html.replace(/]*)>/gi, ''); + html = html.replace(/<\/embed>/gi, '<\/YUI_EMBED>'); + + //Convert b and i tags to strong and em tags + if ((markup == 'semantic') || (markup == 'xhtml')) { + html = html.replace(/]*)?>/gi, ''); + html = html.replace(/<\/i>/gi, ''); + html = html.replace(/]*)?>/gi, ''); + html = html.replace(/<\/b>/gi, ''); + } + + html = html.replace(/_moz_dirty=""/gi, ''); + + //normalize strikethrough + html = html.replace(//gi, '/span>'); + + + //Case Changing + if (this.browser.ie) { + html = html.replace(/text-decoration/gi, 'text-decoration'); + html = html.replace(/font-weight/gi, 'font-weight'); + html = html.replace(/_width="([^>]*)"/gi, ''); + html = html.replace(/_height="([^>]*)"/gi, ''); + //Cleanup Image URL's + var url = this._baseHREF.replace(/\//gi, '\\/'), + re = new RegExp('src="' + url, 'gi'); + html = html.replace(re, 'src="'); + } + html = html.replace(//gi, ''); + html = html.replace(//gi, ''); + if ((markup == 'semantic') || (markup == 'xhtml') || (markup == 'css')) { + html = html.replace(new RegExp(']*)face="([^>]*)">(.*?)<\/font>', 'gi'), '$3'); + html = html.replace(/([^>]*)<\/span>', 'gi'), '$1'); + html = html.replace(new RegExp('([^>]*)<\/span>', 'gi'), '$1'); + } + html = html.replace(/\/u>/gi, '/span>'); + if (markup == 'css') { + html = html.replace(/]*)>/gi, ''); + html = html.replace(/<\/em>/gi, ''); + html = html.replace(/]*)>/gi, ''); + html = html.replace(/<\/strong>/gi, ''); + html = html.replace(//gi, '/span>'); + html = html.replace(//gi, '/span>'); + } + html = html.replace(/ /gi, ' '); //Replace all double spaces and replace with a single + } else { + html = html.replace(//gi, '/u>'); + } + html = html.replace(/]*)>/gi, ''); + html = html.replace(/\/ol>/gi, '/ol>'); + html = html.replace(/
                      • /gi, '/li>'); + html = this.filter_safari(html); + + html = this.filter_internals(html); + + html = this.filter_all_rgb(html); + + //Replace our backups with the real thing + html = this.post_filter_linebreaks(html, markup); + + if (markup == 'xhtml') { + html = html.replace(/]*)>/g, ''); + html = html.replace(/]*)>/g, ''); + } else { + html = html.replace(/]*)>/g, ''); + html = html.replace(/]*)>/g, ''); + } + html = html.replace(/]*)>/g, ''); + html = html.replace(/<\/YUI_UL>/g, '<\/ul>'); + + html = this.filter_invalid_lists(html); + + html = html.replace(/]*)>/g, ''); + html = html.replace(/<\/YUI_BQ>/g, '<\/blockquote>'); + + html = html.replace(/]*)>/g, ''); + html = html.replace(/<\/YUI_EMBED>/g, '<\/embed>'); + + //This should fix &'s in URL's + html = html.replace(/ & /gi, ' YUI_AMP '); + html = html.replace(/ &/gi, ' YUI_AMP_F '); + html = html.replace(/& /gi, ' YUI_AMP_R '); + html = html.replace(/&/gi, '&'); + html = html.replace(/ YUI_AMP /gi, ' & '); + html = html.replace(/ YUI_AMP_F /gi, ' &'); + html = html.replace(/ YUI_AMP_R /gi, '& '); + + //Trim the output, removing whitespace from the beginning and end + html = YAHOO.lang.trim(html); + + if (this.get('removeLineBreaks')) { + html = html.replace(/\n/g, '').replace(/\r/g, ''); + html = html.replace(/ /gi, ' '); //Replace all double spaces and replace with a single + } + + for (var v in this.invalidHTML) { + if (YAHOO.lang.hasOwnProperty(this.invalidHTML, v)) { + if (Lang.isObject(v) && v.keepContents) { + html = html.replace(new RegExp('<' + v + '([^>]*)>(.*?)<\/' + v + '>', 'gi'), '$1'); + } else { + html = html.replace(new RegExp('<' + v + '([^>]*)>(.*?)<\/' + v + '>', 'gi'), ''); + } + } + } + + /* LATER -- Add DOM manipulation + console.log(html); + var frag = document.createDocumentFragment(); + frag.innerHTML = html; + + var ps = frag.getElementsByTagName('p'), + len = ps.length; + for (var i = 0; i < len; i++) { + var ps2 = ps[i].getElementsByTagName('p'); + if (ps2.length) { + + } + + } + html = frag.innerHTML; + console.log(html); + */ + + this.fireEvent('cleanHTML', { type: 'cleanHTML', target: this, html: html }); + + return html; + }, + /** + * @method filter_msword + * @param String html The HTML string to filter + * @description Filters out msword html attributes and other junk. Activate with filterWord: true in config + */ + filter_msword: function(html) { + if (!this.get('filterWord')) { + return html; + } + //Remove the ms o: tags + html = html.replace(/\s*<\/o:p>/g, ''); + html = html.replace(/[\s\S]*?<\/o:p>/g, ' '); + + //Remove the ms w: tags + html = html.replace( /]*>[\s\S]*?<\/w:[^>]*>/gi, ''); + + //Remove mso-? styles. + html = html.replace( /\s*mso-[^:]+:[^;"]+;?/gi, ''); + + //Remove more bogus MS styles. + html = html.replace( /\s*MARGIN: 0cm 0cm 0pt\s*;/gi, ''); + html = html.replace( /\s*MARGIN: 0cm 0cm 0pt\s*"/gi, "\""); + html = html.replace( /\s*TEXT-INDENT: 0cm\s*;/gi, ''); + html = html.replace( /\s*TEXT-INDENT: 0cm\s*"/gi, "\""); + html = html.replace( /\s*PAGE-BREAK-BEFORE: [^\s;]+;?"/gi, "\""); + html = html.replace( /\s*FONT-VARIANT: [^\s;]+;?"/gi, "\"" ); + html = html.replace( /\s*tab-stops:[^;"]*;?/gi, ''); + html = html.replace( /\s*tab-stops:[^"]*/gi, ''); + + //Remove XML declarations + html = html.replace(/<\\?\?xml[^>]*>/gi, ''); + + //Remove lang + html = html.replace(/<(\w[^>]*) lang=([^ |>]*)([^>]*)/gi, "<$1$3"); + + //Remove language tags + html = html.replace( /<(\w[^>]*) language=([^ |>]*)([^>]*)/gi, "<$1$3"); + + //Remove onmouseover and onmouseout events (from MS Word comments effect) + html = html.replace( /<(\w[^>]*) onmouseover="([^\"]*)"([^>]*)/gi, "<$1$3"); + html = html.replace( /<(\w[^>]*) onmouseout="([^\"]*)"([^>]*)/gi, "<$1$3"); + + return html; + }, + /** + * @method filter_invalid_lists + * @param String html The HTML string to filter + * @description Filters invalid ol and ul list markup, converts this:
                        1. ..
                        to this:
                        1. ..
                      • + */ + filter_invalid_lists: function(html) { + html = html.replace(/<\/li>\n/gi, ''); + + html = html.replace(/<\/li>
                          /gi, '
                          1. '); + html = html.replace(/<\/ol>/gi, '
                        1. '); + html = html.replace(/<\/ol><\/li>\n/gi, "
                        "); + + html = html.replace(/<\/li>
                          /gi, '
                          • '); + html = html.replace(/<\/ul>/gi, '
                        • '); + html = html.replace(/<\/ul><\/li>\n?/gi, "
                        "); + + html = html.replace(/<\/li>/gi, ""); + html = html.replace(/<\/ol>/gi, "
                  "); + html = html.replace(/
                    /gi, "
                      "); + html = html.replace(/
                        /gi, "
                          "); + return html; + }, + /** + * @method filter_safari + * @param String html The HTML string to filter + * @description Filters strings specific to Safari + * @return String + */ + filter_safari: function(html) { + if (this.browser.webkit) { + // + html = html.replace(/([^>])<\/span>/gi, '    '); + html = html.replace(/Apple-style-span/gi, ''); + html = html.replace(/style="line-height: normal;"/gi, ''); + html = html.replace(/yui-wk-div/gi, ''); + html = html.replace(/yui-wk-p/gi, ''); + + + //Remove bogus LI's + html = html.replace(/
                        • <\/li>/gi, ''); + html = html.replace(/
                        • <\/li>/gi, ''); + html = html.replace(/
                        • <\/li>/gi, ''); + //Remove bogus DIV's - updated from just removing the div's to replacing /div with a break + if (this.get('ptags')) { + html = html.replace(/]*)>/g, ''); + html = html.replace(/<\/div>/gi, '

                          '); + } else { + //html = html.replace(/
                          /gi, '
                          '); + html = html.replace(/]*)>([ tnr]*)<\/div>/gi, '
                          '); + html = html.replace(/<\/div>/gi, ''); + } + } + return html; + }, + /** + * @method filter_internals + * @param String html The HTML string to filter + * @description Filters internal RTE strings and bogus attrs we don't want + * @return String + */ + filter_internals: function(html) { + html = html.replace(/\r/g, ''); + //Fix stuff we don't want + html = html.replace(/<\/?(body|head|html)[^>]*>/gi, ''); + //Fix last BR in LI + html = html.replace(/<\/li>/gi, '
                        • '); + + html = html.replace(/yui-tag-span/gi, ''); + html = html.replace(/yui-tag/gi, ''); + html = html.replace(/yui-non/gi, ''); + html = html.replace(/yui-img/gi, ''); + html = html.replace(/ tag="span"/gi, ''); + html = html.replace(/ class=""/gi, ''); + html = html.replace(/ style=""/gi, ''); + html = html.replace(/ class=" "/gi, ''); + html = html.replace(/ class=" "/gi, ''); + html = html.replace(/ target=""/gi, ''); + html = html.replace(/ title=""/gi, ''); + + if (this.browser.ie) { + html = html.replace(/ class= /gi, ''); + html = html.replace(/ class= >/gi, ''); + } + + return html; + }, + /** + * @method filter_all_rgb + * @param String str The HTML string to filter + * @description Converts all RGB color strings found in passed string to a hex color, example: style="color: rgb(0, 255, 0)" converts to style="color: #00ff00" + * @return String + */ + filter_all_rgb: function(str) { + var exp = new RegExp("rgb\\s*?\\(\\s*?([0-9]+).*?,\\s*?([0-9]+).*?,\\s*?([0-9]+).*?\\)", "gi"); + var arr = str.match(exp); + if (Lang.isArray(arr)) { + for (var i = 0; i < arr.length; i++) { + var color = this.filter_rgb(arr[i]); + str = str.replace(arr[i].toString(), color); + } + } + + return str; + }, + /** + * @method filter_rgb + * @param String css The CSS string containing rgb(#,#,#); + * @description Converts an RGB color string to a hex color, example: rgb(0, 255, 0) converts to #00ff00 + * @return String + */ + filter_rgb: function(css) { + if (css.toLowerCase().indexOf('rgb') != -1) { + var exp = new RegExp("(.*?)rgb\\s*?\\(\\s*?([0-9]+).*?,\\s*?([0-9]+).*?,\\s*?([0-9]+).*?\\)(.*?)", "gi"); + var rgb = css.replace(exp, "$1,$2,$3,$4,$5").split(','); + + if (rgb.length == 5) { + var r = parseInt(rgb[1], 10).toString(16); + var g = parseInt(rgb[2], 10).toString(16); + var b = parseInt(rgb[3], 10).toString(16); + + r = r.length == 1 ? '0' + r : r; + g = g.length == 1 ? '0' + g : g; + b = b.length == 1 ? '0' + b : b; + + css = "#" + r + g + b; + } + } + return css; + }, + /** + * @method pre_filter_linebreaks + * @param String html The HTML to filter + * @param String markup The markup type to filter to + * @description HTML Pre Filter + * @return String + */ + pre_filter_linebreaks: function(html, markup) { + if (this.browser.webkit) { + html = html.replace(/
                          /gi, ''); + html = html.replace(/
                          /gi, ''); + } + html = html.replace(/
                          /gi, ''); + html = html.replace(/
                          /gi, ''); + html = html.replace(//gi, ''); + html = html.replace(/
                          /gi, ''); + html = html.replace(/
                          <\/div>/gi, ''); + html = html.replace(/

                          ( | )<\/p>/g, ''); + html = html.replace(/


                           <\/p>/gi, ''); + html = html.replace(/

                           <\/p>/gi, ''); + //Fix last BR + html = html.replace(/$/, ''); + //Fix last BR in P + html = html.replace(/<\/p>/g, '

                          '); + if (this.browser.ie) { + html = html.replace(/    /g, '\t'); + } + return html; + }, + /** + * @method post_filter_linebreaks + * @param String html The HTML to filter + * @param String markup The markup type to filter to + * @description HTML Pre Filter + * @return String + */ + post_filter_linebreaks: function(html, markup) { + if (markup == 'xhtml') { + html = html.replace(//g, '
                          '); + } else { + html = html.replace(//g, '
                          '); + } + return html; + }, + /** + * @method clearEditorDoc + * @description Clear the doc of the Editor + */ + clearEditorDoc: function() { + this._getDoc().body.innerHTML = ' '; + }, + /** + * @method openWindow + * @description Override Method for Advanced Editor + */ + openWindow: function(win) { + }, + /** + * @method moveWindow + * @description Override Method for Advanced Editor + */ + moveWindow: function() { + }, + /** + * @private + * @method _closeWindow + * @description Override Method for Advanced Editor + */ + _closeWindow: function() { + }, + /** + * @method closeWindow + * @description Override Method for Advanced Editor + */ + closeWindow: function() { + //this.unsubscribeAll('afterExecCommand'); + this.toolbar.resetAllButtons(); + this.focus(); + }, + /** + * @method destroy + * @description Destroys the editor, all of it's elements and objects. + * @return {Boolean} + */ + destructor: function() { + if (this._nodeChangeDelayTimer) { + clearTimeout(this._nodeChangeDelayTimer); + } + this.hide(); + + YAHOO.log('Destroying Editor', 'warn', 'SimpleEditor'); + if (this.resize) { + YAHOO.log('Destroying Resize', 'warn', 'SimpleEditor'); + this.resize.destroy(); + } + if (this.dd) { + YAHOO.log('Unreg DragDrop Instance', 'warn', 'SimpleEditor'); + this.dd.unreg(); + } + if (this.get('panel')) { + YAHOO.log('Destroying Editor Panel', 'warn', 'SimpleEditor'); + this.get('panel').destroy(); + } + this.saveHTML(); + this.toolbar.destroy(); + YAHOO.log('Restoring TextArea', 'info', 'SimpleEditor'); + this.setStyle('visibility', 'visible'); + this.setStyle('position', 'static'); + this.setStyle('top', ''); + this.setStyle('left', ''); + var textArea = this.get('element'); + this.get('element_cont').get('parentNode').replaceChild(textArea, this.get('element_cont').get('element')); + this.get('element_cont').get('element').innerHTML = ''; + this.set('handleSubmit', false); //Remove the submit handler + return true; + }, + /** + * @method toString + * @description Returns a string representing the editor. + * @return {String} + */ + toString: function() { + var str = 'SimpleEditor'; + return str; + } + }); + + Y.namespace('Editor'); + Y.Editor.Simple = SimpleEditor; + +/** +* @event toolbarLoaded +* @description Event is fired during the render process directly after the Toolbar is loaded. Allowing you to attach events to the toolbar. See Element.addListener for more information on listening for this event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event cleanHTML +* @description Event is fired after the cleanHTML method is called. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event afterRender +* @description Event is fired after the render process finishes. See Element.addListener for more information on listening for this event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event editorContentLoaded +* @description Event is fired after the editor iframe's document fully loads and fires it's onload event. From here you can start injecting your own things into the document. See Element.addListener for more information on listening for this event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event beforeNodeChange +* @description Event fires at the beginning of the nodeChange process. See Element.addListener for more information on listening for this event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event afterNodeChange +* @description Event fires at the end of the nodeChange process. See Element.addListener for more information on listening for this event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event beforeExecCommand +* @description Event fires at the beginning of the execCommand process. See Element.addListener for more information on listening for this event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event afterExecCommand +* @description Event fires at the end of the execCommand process. See Element.addListener for more information on listening for this event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event editorMouseUp +* @param {Event} ev The DOM Event that occured +* @description Passed through HTML Event. See Element.addListener for more information on listening for this event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event editorMouseDown +* @param {Event} ev The DOM Event that occured +* @description Passed through HTML Event. See Element.addListener for more information on listening for this event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event editorDoubleClick +* @param {Event} ev The DOM Event that occured +* @description Passed through HTML Event. See Element.addListener for more information on listening for this event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event editorClick +* @param {Event} ev The DOM Event that occured +* @description Passed through HTML Event. See Element.addListener for more information on listening for this event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event editorKeyUp +* @param {Event} ev The DOM Event that occured +* @description Passed through HTML Event. See Element.addListener for more information on listening for this event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event editorKeyPress +* @param {Event} ev The DOM Event that occured +* @description Passed through HTML Event. See Element.addListener for more information on listening for this event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event editorKeyDown +* @param {Event} ev The DOM Event that occured +* @description Passed through HTML Event. See Element.addListener for more information on listening for this event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event beforeEditorMouseUp +* @param {Event} ev The DOM Event that occured +* @description Fires before editor event, returning false will stop the internal processing. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event beforeEditorMouseDown +* @param {Event} ev The DOM Event that occured +* @description Fires before editor event, returning false will stop the internal processing. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event beforeEditorDoubleClick +* @param {Event} ev The DOM Event that occured +* @description Fires before editor event, returning false will stop the internal processing. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event beforeEditorClick +* @param {Event} ev The DOM Event that occured +* @description Fires before editor event, returning false will stop the internal processing. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event beforeEditorKeyUp +* @param {Event} ev The DOM Event that occured +* @description Fires before editor event, returning false will stop the internal processing. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event beforeEditorKeyPress +* @param {Event} ev The DOM Event that occured +* @description Fires before editor event, returning false will stop the internal processing. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event beforeEditorKeyDown +* @param {Event} ev The DOM Event that occured +* @description Fires before editor event, returning false will stop the internal processing. +* @type YAHOO.util.CustomEvent +*/ + +/** +* @event editorWindowFocus +* @description Fires when the iframe is focused. Note, this is window focus event, not an Editor focus event. +* @type YAHOO.util.CustomEvent +*/ +/** +* @event editorWindowBlur +* @description Fires when the iframe is blurred. Note, this is window blur event, not an Editor blur event. +* @type YAHOO.util.CustomEvent +*/ + + +/** + * @description Singleton object used to track the open window objects and panels across the various open editors + * @class Editor.Info + * @static +*/ +Y.Editor.Info = { + /** + * @private + * @property _instances + * @description A reference to all editors on the page. + * @type Object + */ + _instances: {}, + /** + * @private + * @property blankImage + * @description A reference to the blankImage url + * @type String + */ + blankImage: '', + /** + * @private + * @property window + * @description A reference to the currently open window object in any editor on the page. + * @type Object YAHOO.widget.EditorWindow + */ + window: {}, + /** + * @private + * @property panel + * @description A reference to the currently open panel in any editor on the page. + * @type Object YAHOO.widget.Overlay + */ + panel: null, + /** + * @method getEditorById + * @description Returns a reference to the Editor object associated with the given textarea + * @param {String/HTMLElement} id The id or reference of the textarea to return the Editor instance of + * @return Object YAHOO.widget.Editor + */ + getEditorById: function(id) { + if (!Lang.isString(id)) { + //Not a string, assume a node Reference + id = id.id; + } + if (this._instances[id]) { + return this._instances[id]; + } + return false; + }, + /** + * @method saveAll + * @description Saves all Editor instances on the page. If a form reference is passed, only Editor's bound to this form will be saved. + * @param {HTMLElement} form The form to check if this Editor instance belongs to + */ + saveAll: function(form) { + var i, e, items = Y.Editor.Info._instances; + if (form) { + Y.each(items, function(v, e) { + if (e.get('element').form && (e.get('element').form == form)) { + e.saveHTML(); + } + }); + } else { + Y.each(items, function(v) { + v.saveHTML(); + }); + } + }, + /** + * @method toString + * @description Returns a string representing the Editor.Info. + * @return {String} + */ + toString: function() { + var len = 0; + Y.each(this._instances, function() { + len++; + }); + return 'Editor Info (' + len + ' registered intance' + ((len > 1) ? 's' : '') + ')'; + } +}; + + +}, 'gallery-2009.11.02-20' ,{requires:['node','gallery-port','substitute']}); diff --git a/build/gallery-textarea-counter/gallery-textarea-counter-debug.js b/build/gallery-textarea-counter/gallery-textarea-counter-debug.js new file mode 100644 index 0000000000..11123e084c --- /dev/null +++ b/build/gallery-textarea-counter/gallery-textarea-counter-debug.js @@ -0,0 +1,159 @@ +YUI.add('gallery-textarea-counter', function(Y) { + + + + var setNode = function(node) { + return Y.one(node); + }, + setUI = function(attr, num) { + var n = this.get(attr); + if (n) { + n.set('innerHTML', num); + } + }, + TC = function(config) { + config.node = config.host; + TC.superclass.constructor.call(this, config); + }; + + TC.NAME = "gallery-textarea-counter"; + + TC.NS = "counter"; + + TC.ATTRS = { + node: { + setter: setNode + }, + wordsNode: { + setter: setNode + }, + charsNode: { + setter: setNode + }, + rowsNode: { + setter: setNode + }, + colsNode: { + setter: setNode + }, + rowCount: { + setter: function(num) { + setUI.call(this, 'rowsNode', num); + return num; + } + }, + colCount: { + setter: function(num) { + setUI.call(this, 'colsNode', num); + return num; + } + }, + wordCount: { + setter: function(num) { + setUI.call(this, 'wordsNode', num); + return num; + } + }, + charCount: { + setter: function(num) { + setUI.call(this, 'charsNode', num); + return num; + } + } + }; + + Y.extend(TC, Y.Base, { + _handles: null, + initializer: function() { + this._handles = []; + var n = this.get('node'), + fn = Y.bind(this.count, this); + this._handles.push(n.on('keyup', fn)); + this._handles.push(n.on('keypress', fn)); + this._handles.push(n.on('mouseup', fn)); + this._handles.push(n.on('focus', fn)); + + }, + count: function() { + var node = this.get('node'), + text = node.get('value'), + defWordCount = this.get('wordCount'), + defCharCount = this.get('charCount'), + defColCount = this.get('colCount'), + defRowCount = this.get('rowCount'), + wordCount = 0, + charCount = ((text.length) ? text.length : ((node.get('textLength')) ? node.get('textLength') : 0)), + start = node.get('selectionStart'), + end = node.get('selectionEnd'), + rows = 0, cols = 0, range, stored_range, rowList, rowCount, i, sc, ec, r = 0; + + if (charCount > 0) { + wordCount = text.match(/\b/g); + wordCount = ((wordCount) ? (wordCount.length / 2) : 0); + } + + + if (Y.UA.ie) { + if (document.selection) { + range = document.selection.createRange(); + stored_range = range.duplicate(); + stored_range.moveToElementText(node._node); + stored_range.setEndPoint('EndToEnd', range); + start = stored_range.text.length - range.text.length; + end = start + range.text.length; + } + + } + + rowList = text.split(/\n/); + rowCount = ((rowList) ? rowList.length : 1); + + for (i = 0; i < rowCount; i++) { + if (Y.UA.gecko) { + charCount++; + } + r += (rowList[i].length + 1); + sc = (r - rowList[i].length - 1); + ec = ((rowList[i].length + 1) + sc); + if ((start >= sc) && (start <= ec)) { + rows = (i + 1); + cols = ((start - sc) + 1); + } + } + + if (Y.UA.gecko) { + charCount--; + } + if (start !== end) { //No Selection + rows = 0; + cols = 0; + } + + if (defWordCount !== wordCount) { + this.set('wordCount', wordCount); + } + if (defCharCount !== charCount) { + this.set('charCount', charCount); + } + if (defRowCount !== rows) { + this.set('rowCount', rows); + } + if (defColCount !== cols) { + this.set('colCount', cols); + } + + }, + destructor: function() { + if (this._handles) { + Y.each(this._handles, function(v) { + v.detach(); + }); + } + } + }); + Y.namespace('Plugin'); + Y.Plugin.Counter = TC; + + + +}, 'gallery-2009.11.09-19' ,{requires:['node', 'event']}); diff --git a/build/gallery-textarea-counter/gallery-textarea-counter-min.js b/build/gallery-textarea-counter/gallery-textarea-counter-min.js new file mode 100644 index 0000000000..c6e5a75bb5 --- /dev/null +++ b/build/gallery-textarea-counter/gallery-textarea-counter-min.js @@ -0,0 +1 @@ +YUI.add("gallery-textarea-counter",function(D){var A=function(E){return D.one(E);},C=function(E,F){var G=this.get(E);if(G){G.set("innerHTML",F);}},B=function(E){E.node=E.host;B.superclass.constructor.call(this,E);};B.NAME="gallery-textarea-counter";B.NS="counter";B.ATTRS={node:{setter:A},wordsNode:{setter:A},charsNode:{setter:A},rowsNode:{setter:A},colsNode:{setter:A},rowCount:{setter:function(E){C.call(this,"rowsNode",E);return E;}},colCount:{setter:function(E){C.call(this,"colsNode",E);return E;}},wordCount:{setter:function(E){C.call(this,"wordsNode",E);return E;}},charCount:{setter:function(E){C.call(this,"charsNode",E);return E;}}};D.extend(B,D.Base,{_handles:null,initializer:function(){this._handles=[];var F=this.get("node"),E=D.bind(this.count,this);this._handles.push(F.on("keyup",E));this._handles.push(F.on("keypress",E));this._handles.push(F.on("mouseup",E));this._handles.push(F.on("focus",E));},count:function(){var T=this.get("node"),O=T.get("value"),F=this.get("wordCount"),K=this.get("charCount"),W=this.get("colCount"),S=this.get("rowCount"),R=0,X=((O.length)?O.length:((T.get("textLength"))?T.get("textLength"):0)),H=T.get("selectionStart"),G=T.get("selectionEnd"),L=0,N=0,P,V,J,M,U,E,I,Q=0;if(X>0){R=O.match(/\b/g);R=((R)?(R.length/2):0);}if(D.UA.ie){if(document.selection){P=document.selection.createRange();V=P.duplicate();V.moveToElementText(T._node);V.setEndPoint("EndToEnd",P);H=V.text.length-P.text.length;G=H+P.text.length;}}J=O.split(/\n/);M=((J)?J.length:1);for(U=0;U=E)&&(H<=I)){L=(U+1);N=((H-E)+1);}}if(D.UA.gecko){X--;}if(H!==G){L=0;N=0;}if(F!==R){this.set("wordCount",R);}if(K!==X){this.set("charCount",X);}if(S!==L){this.set("rowCount",L);}if(W!==N){this.set("colCount",N);}},destructor:function(){if(this._handles){D.each(this._handles,function(E){E.detach();});}}});D.namespace("Plugin");D.Plugin.Counter=B;},"gallery-2009.11.09-19",{requires:["node","event"]}); \ No newline at end of file diff --git a/build/gallery-textarea-counter/gallery-textarea-counter.js b/build/gallery-textarea-counter/gallery-textarea-counter.js new file mode 100644 index 0000000000..11123e084c --- /dev/null +++ b/build/gallery-textarea-counter/gallery-textarea-counter.js @@ -0,0 +1,159 @@ +YUI.add('gallery-textarea-counter', function(Y) { + + + + var setNode = function(node) { + return Y.one(node); + }, + setUI = function(attr, num) { + var n = this.get(attr); + if (n) { + n.set('innerHTML', num); + } + }, + TC = function(config) { + config.node = config.host; + TC.superclass.constructor.call(this, config); + }; + + TC.NAME = "gallery-textarea-counter"; + + TC.NS = "counter"; + + TC.ATTRS = { + node: { + setter: setNode + }, + wordsNode: { + setter: setNode + }, + charsNode: { + setter: setNode + }, + rowsNode: { + setter: setNode + }, + colsNode: { + setter: setNode + }, + rowCount: { + setter: function(num) { + setUI.call(this, 'rowsNode', num); + return num; + } + }, + colCount: { + setter: function(num) { + setUI.call(this, 'colsNode', num); + return num; + } + }, + wordCount: { + setter: function(num) { + setUI.call(this, 'wordsNode', num); + return num; + } + }, + charCount: { + setter: function(num) { + setUI.call(this, 'charsNode', num); + return num; + } + } + }; + + Y.extend(TC, Y.Base, { + _handles: null, + initializer: function() { + this._handles = []; + var n = this.get('node'), + fn = Y.bind(this.count, this); + this._handles.push(n.on('keyup', fn)); + this._handles.push(n.on('keypress', fn)); + this._handles.push(n.on('mouseup', fn)); + this._handles.push(n.on('focus', fn)); + + }, + count: function() { + var node = this.get('node'), + text = node.get('value'), + defWordCount = this.get('wordCount'), + defCharCount = this.get('charCount'), + defColCount = this.get('colCount'), + defRowCount = this.get('rowCount'), + wordCount = 0, + charCount = ((text.length) ? text.length : ((node.get('textLength')) ? node.get('textLength') : 0)), + start = node.get('selectionStart'), + end = node.get('selectionEnd'), + rows = 0, cols = 0, range, stored_range, rowList, rowCount, i, sc, ec, r = 0; + + if (charCount > 0) { + wordCount = text.match(/\b/g); + wordCount = ((wordCount) ? (wordCount.length / 2) : 0); + } + + + if (Y.UA.ie) { + if (document.selection) { + range = document.selection.createRange(); + stored_range = range.duplicate(); + stored_range.moveToElementText(node._node); + stored_range.setEndPoint('EndToEnd', range); + start = stored_range.text.length - range.text.length; + end = start + range.text.length; + } + + } + + rowList = text.split(/\n/); + rowCount = ((rowList) ? rowList.length : 1); + + for (i = 0; i < rowCount; i++) { + if (Y.UA.gecko) { + charCount++; + } + r += (rowList[i].length + 1); + sc = (r - rowList[i].length - 1); + ec = ((rowList[i].length + 1) + sc); + if ((start >= sc) && (start <= ec)) { + rows = (i + 1); + cols = ((start - sc) + 1); + } + } + + if (Y.UA.gecko) { + charCount--; + } + if (start !== end) { //No Selection + rows = 0; + cols = 0; + } + + if (defWordCount !== wordCount) { + this.set('wordCount', wordCount); + } + if (defCharCount !== charCount) { + this.set('charCount', charCount); + } + if (defRowCount !== rows) { + this.set('rowCount', rows); + } + if (defColCount !== cols) { + this.set('colCount', cols); + } + + }, + destructor: function() { + if (this._handles) { + Y.each(this._handles, function(v) { + v.detach(); + }); + } + } + }); + Y.namespace('Plugin'); + Y.Plugin.Counter = TC; + + + +}, 'gallery-2009.11.09-19' ,{requires:['node', 'event']}); diff --git a/build/gallery-timepicker/gallery-timepicker-debug.js b/build/gallery-timepicker/gallery-timepicker-debug.js new file mode 100644 index 0000000000..c6a3bb0de2 --- /dev/null +++ b/build/gallery-timepicker/gallery-timepicker-debug.js @@ -0,0 +1,429 @@ +YUI.add('gallery-timepicker', function(Y) { + +/* +Copyright (c) 2009, Stephen Woods +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Stephen Woods nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY STEPHEN WOODS ''AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL STEPHEN WOODS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Inspired by: http://haineault.com/media/jquery/ui-timepickr/page/ +This software is based on a concept by Maxime Haineault in code licensed under +an MIT license. However, none of his code or implementation details will reviewed +or used for this project. +*/ + + +/** + * A yui 3 timepicker + * + * @module Timepicker + * @requires oop, event-custom, attribute, base, dom, classnamemanager, widget, event + */ + + + /** + * a YUI 3 implementation of the classic jQuery + * timepicker widget + * @class Timepicker + * @namespace Y.Saw + * @extends Widget + */ + + + + var array = Y.Array, + getClassName= Y.ClassNameManager.getClassName, + + //repeated and/or magic strings + NAMESPACE = 'Saw', + CONSTRUCTOR = 'Timepicker', + + CELL_CLASS = 'cell', + HOUR_CLASS = 'hour', + MINUTE_CLASS= 'minute', + AMPM_CLASS = 'ampm', + ACTIVE_CLASS= 'active', + + NAME = 'NAME', + ROW = 'row', + AMSTR_KEY = 'strings.am', + PMSTR_KEY = 'strings.pm', + + DELAY_KEY = 'delay', + + + + //constants for AM & PM + AM = 0, + PM = 1; + + + /* utils */ + + /** + * Pad numbers to two digits + * @protected + * @method pad + * @param {Number} num the number to bad + * @returns {String} padded number as string + * @type String|Object|Array|Boolean|Number + */ + function pad(num){ + return (num < 10) ? '0' + num : num; + } + + /** + * Creates a cell based on the little template defined in "str" + * @protected + * @method makeCell + * @param {String} str The contents of the cell + * @param {String} rowId the unique classname for the row to identify it later + * + * @returns {String} returns the assembled html string + */ + + function makeCell(str, rowId){ + var thisClass = getClassName(Timepicker[NAME], Timepicker[str]), + myStr = '
                        • '+str+'
                        • '; + return myStr; + } + + + //create the constructor, chain the parent + function Timepicker(config){ + Timepicker.superclass.constructor.apply(this, arguments); + } + + Timepicker[NAME] = 'timepicker'; + + Timepicker.ATTRS = { + + time:{ + value:{ + hour:0, + minute:0, + ampm:AM + } + }, + + delay:{ + value:15 + }, + + strings: { + value: { + am : "AM", + pm : "PM", + seperator : ':' + } + } + }; + + + //build class names + Timepicker[HOUR_CLASS] = getClassName(Timepicker[NAME], HOUR_CLASS); + Timepicker[MINUTE_CLASS] = getClassName(Timepicker[NAME], MINUTE_CLASS); + Timepicker[AMPM_CLASS] = getClassName(Timepicker[NAME], AMPM_CLASS); + Timepicker[HOUR_CLASS] = getClassName(Timepicker[NAME], HOUR_CLASS, ROW); + Timepicker[MINUTE_CLASS] = getClassName(Timepicker[NAME], MINUTE_CLASS, ROW); + Timepicker[CELL_CLASS] = getClassName(Timepicker[NAME], CELL_CLASS); + Timepicker[ACTIVE_CLASS] = getClassName(Timepicker[NAME], ACTIVE_CLASS); + + + + Y.extend(Timepicker, Y.Widget, { + + + /* static vars */ + + AM:AM, + + PM:PM, + + /* the "model", actually a cache of dom refrences to find + elements quickly later */ + _model : {ampm:{},hour:{},minute:{}}, + + /* allow a short delay before highlight */ + _timer: null, + + + + initializer:function(){ + this.set('time.ampm', AM); + var hour = this.get('time.hour'); + this.set('time.hour', ((hour === 0) ? 12 : hour)); + + }, + + destructor: function(){ + // nuke the model, which is storing references to dom objects + + delete(this._model.ampm); + delete(this._model.hour); + delete(this._model.minute); + + }, + + /** + * This method syncs the value of time object, + * including building the strings for 12hr and 24hr + * also fires a 'timeset' event + * @method _syncTime + * @protected + * + */ + _syncTime:function(){ + + /** + * Fires when a new time has been set (after the time strings) + * have been built. Use this event rather than timeChange. + * @event timeset + * @param {Object} the time attribute object with the following attributes + *
                          + *
                          hour
                          + *
                          The 12 hour hour
                          + *
                          minute
                          + *
                          The minutes
                          + *
                          s12hour
                          + *
                          A string representing the 12 hour time, with a seperator and an am/pm indicator as defined in strings
                          + *
                          s24hour
                          + *
                          A string representing the 24 hour time, with the seperator defined in strings
                          + *
                          + */ + + + /** + * Fires when a cell is clicked on + * @event cellclick + * @param event {Event.Facade} An Event Facade object + */ + var time = this.get('time'), + + ampm = time.ampm, + seperator = this.get('strings.seperator'), + minute = pad(time.minute), + + //build the string for ampm based on the strings + ampmString = (ampm == AM) ? this.get(AMSTR_KEY) : this.get(PMSTR_KEY); + + //store the string representation of the 12 hour time + + this.set('time.s12hour', + ((time.hour === 0) ? 12 : time.hour) + + seperator + minute + ampmString); + + //convert 12 hour to 24 + var hour = (ampm == PM) ? parseInt(time.hour,10) + 12 : parseInt(time.hour,10); + if(hour == 24 || hour === 0 ) {hour = Math.abs(hour-12);} + if(hour == 12 && ampm == AM) {hour = 0;} + + //store the string for 24 hour time + this.set('time.s24hour', hour + seperator + minute); + + //fire time change event + this.fire('timeset', this.get('time')); + }, + + _handleClick:function(e){ + //dispatch 'cellclick' event on any clicks + if(e.target.test('.'+Timepicker[CELL_CLASS])){ + this.fire('cellclick', this.get('time')); + } + }, + + _handleOver:function(e){ + //this handles mouseover events, which it uses to change + //the store value of time as defined in the params + + var targ = e.target, delay = this.get(DELAY_KEY); + + if(this._timer){ + this._timer.cancel(); + this._timer = null; + } + + this._timer = Y.later(delay, this, this._highlight, targ); + + + }, + + _highlight:function(targ){ + //make sure this is one of our cells + if(targ.test('.'+Timepicker[CELL_CLASS])){ + + var value = targ.get('innerHTML'); + + //we are using classnames to figure out which row is which + if(targ.hasClass(Timepicker[HOUR_CLASS])){ + this.set('time.hour',value); + }else if (targ.hasClass(Timepicker[AMPM_CLASS])){ + + //ugly, but otherwise we would need to embed metadata + //somewhere else, this seemed easy enough + var amString = this.get(AMSTR_KEY); + + if(value == amString){ + this.set('time.ampm', AM); + } else{ + this.set('time.ampm', PM); + } + + }else{ + this.set('time.minute', value); + } + + } + this._syncTime(); + + this.syncUI(); + }, + + _handleOut:function(e){ + if(this._timer){ + this._timer.cancel(); + this._timer = null; + } + }, + + renderUI: function(){ + //FIXME: This could be more efficient! + + /* + current implementation builds three ordered lists, one for + each row. Then we use the makeCell private method tp create a cell + with the given class, based on string constants defined up top + */ + var cb = this.get('contentBox'), + m = this._model; + + //create row function is very simple... + function createRow(){ return cb.create('
                            ');} + + var row = []; + //only need three rows + for (var i=0; i <= 3; i++) { + row[i] = createRow(); + } + + //wrap make cell in node create + function mc (str, c){ + return cb.create(makeCell(str, c)); + } + + + m[AMPM_CLASS].AM = mc(this.get(AMSTR_KEY),AMPM_CLASS); + m[AMPM_CLASS].PM = mc(this.get(PMSTR_KEY),AMPM_CLASS); + row[0].appendChild(m[AMPM_CLASS].AM); + row[0].appendChild(m[AMPM_CLASS].PM); + + //build rows, creating a function to use only twice, but + //still remove duplicates + function assembleRow(start, row, max, step, c){ + for(var i = start; i<=max; i=i+step){ + var cell = mc(i, c); + m[c][i] = cell; + row.appendChild(cell); + } + } + + assembleRow(1, row[1], 12, 1, HOUR_CLASS); + + assembleRow(0, row[2], 45, 15, MINUTE_CLASS); + + this._model[AMPM_CLASS].row = row[0]; + this._model[HOUR_CLASS].row = row[1]; + this._model[MINUTE_CLASS].row = row[2]; + + var parent = cb.create('
                            '); + + + array.each(row, function(item){ + parent.appendChild(item); + }); + + cb.appendChild(parent); + + //store for later + this.allCells = cb.queryAll('li'); + + }, + + /** + * Show/hide the widget + * @method toggle + */ + toggle: function(){ + this[(this.get('visible') ? 'hide' : 'show')](); + this.syncUI(); //IE 6 has an issue without this + }, + + bindUI: function(){ + + var cb = this.get('contentBox'); + cb.on('click', this._handleClick, this); + cb.on('mouseover', this._handleOver, this); + cb.on('mouseout', this._handleOut, this); + }, + + syncUI: function(){ + + //get the current tine vlaue + var time = this.get('time'); + + //get all of the li elements to clear their active state + this.allCells.removeClass(Timepicker[ACTIVE_CLASS]); + + var m = this._model; + var apos = 0; + + //handle ampm row, because of l10n can't count on + //the value, so instead we use the "constant" + if(time.ampm == AM){ + m.ampm.AM.addClass(Timepicker[ACTIVE_CLASS]); + apos = m.ampm.AM.getX(); + }else if(time.ampm == PM){ + m.ampm.PM.addClass(Timepicker[ACTIVE_CLASS]); + apos = m.ampm.PM.getX(); + } + + //handle minute row + m.minute[time.minute].addClass(Timepicker[ACTIVE_CLASS]); + + + //handle hour row + m.hour[time.hour].addClass(Timepicker[ACTIVE_CLASS]); + + m.hour.row.setX(apos); + + m.minute.row.setX(m.hour[time.hour].getX()); + + } + }); + + Y.Base.build(Timepicker.NAME, Timepicker, {dynamic:false}); + Y.namespace(NAMESPACE +'.'+CONSTRUCTOR); + Y[NAMESPACE][CONSTRUCTOR] = Timepicker; + + +}, 'gallery-2009.11.02-20' ,{requires:['oop', 'event-custom', 'attribute', 'base', 'dom', 'classnamemanager', 'widget', 'event']}); diff --git a/build/gallery-timepicker/gallery-timepicker-min.js b/build/gallery-timepicker/gallery-timepicker-min.js new file mode 100644 index 0000000000..55b13f2a25 --- /dev/null +++ b/build/gallery-timepicker/gallery-timepicker-min.js @@ -0,0 +1 @@ +YUI.add("gallery-timepicker",function(A){var H=A.Array,J=A.ClassNameManager.getClassName,C="Saw",M="Timepicker",G="cell",E="hour",Q="minute",R="ampm",D="active",F="NAME",K="row",I="strings.am",O="strings.pm",P="delay",L=0,N=1;function S(U){return(U<10)?"0"+U:U;}function T(W,V){var U=J(B[F],B[W]),X='
                          1. '+W+"
                          2. ";return X;}function B(U){B.superclass.constructor.apply(this,arguments);}B[F]="timepicker";B.ATTRS={time:{value:{hour:0,minute:0,ampm:L}},delay:{value:15},strings:{value:{am:"AM",pm:"PM",seperator:":"}}};B[E]=J(B[F],E);B[Q]=J(B[F],Q);B[R]=J(B[F],R);B[E]=J(B[F],E,K);B[Q]=J(B[F],Q,K);B[G]=J(B[F],G);B[D]=J(B[F],D);A.extend(B,A.Widget,{AM:L,PM:N,_model:{ampm:{},hour:{},minute:{}},_timer:null,initializer:function(){this.set("time.ampm",L);var U=this.get("time.hour");this.set("time.hour",((U===0)?12:U));},destructor:function(){delete (this._model.ampm);delete (this._model.hour);delete (this._model.minute);},_syncTime:function(){var Y=this.get("time"),V=Y.ampm,W=this.get("strings.seperator"),Z=S(Y.minute),X=(V==L)?this.get(I):this.get(O);this.set("time.s12hour",((Y.hour===0)?12:Y.hour)+W+Z+X);var U=(V==N)?parseInt(Y.hour,10)+12:parseInt(Y.hour,10);if(U==24||U===0){U=Math.abs(U-12);}if(U==12&&V==L){U=0;}this.set("time.s24hour",U+W+Z);this.fire("timeset",this.get("time"));},_handleClick:function(U){if(U.target.test("."+B[G])){this.fire("cellclick",this.get("time"));}},_handleOver:function(W){var V=W.target,U=this.get(P);if(this._timer){this._timer.cancel();this._timer=null;}this._timer=A.later(U,this,this._highlight,V);},_highlight:function(U){if(U.test("."+B[G])){var V=U.get("innerHTML");if(U.hasClass(B[E])){this.set("time.hour",V);}else{if(U.hasClass(B[R])){var W=this.get(I);if(V==W){this.set("time.ampm",L);}else{this.set("time.ampm",N);}}else{this.set("time.minute",V);}}}this._syncTime();this.syncUI();},_handleOut:function(U){if(this._timer){this._timer.cancel();this._timer=null;}},renderUI:function(){var V=this.get("contentBox"),U=this._model;function Z(){return V.create("
                              ");}var b=[];for(var X=0;X<=3;X++){b[X]=Z();}function a(d,e){return V.create(T(d,e));}U[R].AM=a(this.get(I),R);U[R].PM=a(this.get(O),R);b[0].appendChild(U[R].AM);b[0].appendChild(U[R].PM);function W(k,h,e,g,j){for(var f=k;f<=e;f=f+g){var d=a(f,j);U[j][f]=d;h.appendChild(d);}}W(1,b[1],12,1,E);W(0,b[2],45,15,Q);this._model[R].row=b[0];this._model[E].row=b[1];this._model[Q].row=b[2];var Y=V.create("
                              ");H.each(b,function(c){Y.appendChild(c);});V.appendChild(Y);this.allCells=V.queryAll("li");},toggle:function(){this[(this.get("visible")?"hide":"show")]();this.syncUI();},bindUI:function(){var U=this.get("contentBox");U.on("click",this._handleClick,this);U.on("mouseover",this._handleOver,this);U.on("mouseout",this._handleOut,this);},syncUI:function(){var W=this.get("time");this.allCells.removeClass(B[D]);var U=this._model;var V=0;if(W.ampm==L){U.ampm.AM.addClass(B[D]);V=U.ampm.AM.getX();}else{if(W.ampm==N){U.ampm.PM.addClass(B[D]);V=U.ampm.PM.getX();}}U.minute[W.minute].addClass(B[D]);U.hour[W.hour].addClass(B[D]);U.hour.row.setX(V);U.minute.row.setX(U.hour[W.hour].getX());}});A.Base.build(B.NAME,B,{dynamic:false});A.namespace(C+"."+M);A[C][M]=B;},"gallery-2009.11.02-20",{requires:["oop","event-custom","attribute","base","dom","classnamemanager","widget","event"]}); \ No newline at end of file diff --git a/build/gallery-timepicker/gallery-timepicker.js b/build/gallery-timepicker/gallery-timepicker.js new file mode 100644 index 0000000000..c6a3bb0de2 --- /dev/null +++ b/build/gallery-timepicker/gallery-timepicker.js @@ -0,0 +1,429 @@ +YUI.add('gallery-timepicker', function(Y) { + +/* +Copyright (c) 2009, Stephen Woods +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Stephen Woods nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY STEPHEN WOODS ''AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL STEPHEN WOODS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Inspired by: http://haineault.com/media/jquery/ui-timepickr/page/ +This software is based on a concept by Maxime Haineault in code licensed under +an MIT license. However, none of his code or implementation details will reviewed +or used for this project. +*/ + + +/** + * A yui 3 timepicker + * + * @module Timepicker + * @requires oop, event-custom, attribute, base, dom, classnamemanager, widget, event + */ + + + /** + * a YUI 3 implementation of the classic jQuery + * timepicker widget + * @class Timepicker + * @namespace Y.Saw + * @extends Widget + */ + + + + var array = Y.Array, + getClassName= Y.ClassNameManager.getClassName, + + //repeated and/or magic strings + NAMESPACE = 'Saw', + CONSTRUCTOR = 'Timepicker', + + CELL_CLASS = 'cell', + HOUR_CLASS = 'hour', + MINUTE_CLASS= 'minute', + AMPM_CLASS = 'ampm', + ACTIVE_CLASS= 'active', + + NAME = 'NAME', + ROW = 'row', + AMSTR_KEY = 'strings.am', + PMSTR_KEY = 'strings.pm', + + DELAY_KEY = 'delay', + + + + //constants for AM & PM + AM = 0, + PM = 1; + + + /* utils */ + + /** + * Pad numbers to two digits + * @protected + * @method pad + * @param {Number} num the number to bad + * @returns {String} padded number as string + * @type String|Object|Array|Boolean|Number + */ + function pad(num){ + return (num < 10) ? '0' + num : num; + } + + /** + * Creates a cell based on the little template defined in "str" + * @protected + * @method makeCell + * @param {String} str The contents of the cell + * @param {String} rowId the unique classname for the row to identify it later + * + * @returns {String} returns the assembled html string + */ + + function makeCell(str, rowId){ + var thisClass = getClassName(Timepicker[NAME], Timepicker[str]), + myStr = '
                            1. '+str+'
                            2. '; + return myStr; + } + + + //create the constructor, chain the parent + function Timepicker(config){ + Timepicker.superclass.constructor.apply(this, arguments); + } + + Timepicker[NAME] = 'timepicker'; + + Timepicker.ATTRS = { + + time:{ + value:{ + hour:0, + minute:0, + ampm:AM + } + }, + + delay:{ + value:15 + }, + + strings: { + value: { + am : "AM", + pm : "PM", + seperator : ':' + } + } + }; + + + //build class names + Timepicker[HOUR_CLASS] = getClassName(Timepicker[NAME], HOUR_CLASS); + Timepicker[MINUTE_CLASS] = getClassName(Timepicker[NAME], MINUTE_CLASS); + Timepicker[AMPM_CLASS] = getClassName(Timepicker[NAME], AMPM_CLASS); + Timepicker[HOUR_CLASS] = getClassName(Timepicker[NAME], HOUR_CLASS, ROW); + Timepicker[MINUTE_CLASS] = getClassName(Timepicker[NAME], MINUTE_CLASS, ROW); + Timepicker[CELL_CLASS] = getClassName(Timepicker[NAME], CELL_CLASS); + Timepicker[ACTIVE_CLASS] = getClassName(Timepicker[NAME], ACTIVE_CLASS); + + + + Y.extend(Timepicker, Y.Widget, { + + + /* static vars */ + + AM:AM, + + PM:PM, + + /* the "model", actually a cache of dom refrences to find + elements quickly later */ + _model : {ampm:{},hour:{},minute:{}}, + + /* allow a short delay before highlight */ + _timer: null, + + + + initializer:function(){ + this.set('time.ampm', AM); + var hour = this.get('time.hour'); + this.set('time.hour', ((hour === 0) ? 12 : hour)); + + }, + + destructor: function(){ + // nuke the model, which is storing references to dom objects + + delete(this._model.ampm); + delete(this._model.hour); + delete(this._model.minute); + + }, + + /** + * This method syncs the value of time object, + * including building the strings for 12hr and 24hr + * also fires a 'timeset' event + * @method _syncTime + * @protected + * + */ + _syncTime:function(){ + + /** + * Fires when a new time has been set (after the time strings) + * have been built. Use this event rather than timeChange. + * @event timeset + * @param {Object} the time attribute object with the following attributes + *
                              + *
                              hour
                              + *
                              The 12 hour hour
                              + *
                              minute
                              + *
                              The minutes
                              + *
                              s12hour
                              + *
                              A string representing the 12 hour time, with a seperator and an am/pm indicator as defined in strings
                              + *
                              s24hour
                              + *
                              A string representing the 24 hour time, with the seperator defined in strings
                              + *
                              + */ + + + /** + * Fires when a cell is clicked on + * @event cellclick + * @param event {Event.Facade} An Event Facade object + */ + var time = this.get('time'), + + ampm = time.ampm, + seperator = this.get('strings.seperator'), + minute = pad(time.minute), + + //build the string for ampm based on the strings + ampmString = (ampm == AM) ? this.get(AMSTR_KEY) : this.get(PMSTR_KEY); + + //store the string representation of the 12 hour time + + this.set('time.s12hour', + ((time.hour === 0) ? 12 : time.hour) + + seperator + minute + ampmString); + + //convert 12 hour to 24 + var hour = (ampm == PM) ? parseInt(time.hour,10) + 12 : parseInt(time.hour,10); + if(hour == 24 || hour === 0 ) {hour = Math.abs(hour-12);} + if(hour == 12 && ampm == AM) {hour = 0;} + + //store the string for 24 hour time + this.set('time.s24hour', hour + seperator + minute); + + //fire time change event + this.fire('timeset', this.get('time')); + }, + + _handleClick:function(e){ + //dispatch 'cellclick' event on any clicks + if(e.target.test('.'+Timepicker[CELL_CLASS])){ + this.fire('cellclick', this.get('time')); + } + }, + + _handleOver:function(e){ + //this handles mouseover events, which it uses to change + //the store value of time as defined in the params + + var targ = e.target, delay = this.get(DELAY_KEY); + + if(this._timer){ + this._timer.cancel(); + this._timer = null; + } + + this._timer = Y.later(delay, this, this._highlight, targ); + + + }, + + _highlight:function(targ){ + //make sure this is one of our cells + if(targ.test('.'+Timepicker[CELL_CLASS])){ + + var value = targ.get('innerHTML'); + + //we are using classnames to figure out which row is which + if(targ.hasClass(Timepicker[HOUR_CLASS])){ + this.set('time.hour',value); + }else if (targ.hasClass(Timepicker[AMPM_CLASS])){ + + //ugly, but otherwise we would need to embed metadata + //somewhere else, this seemed easy enough + var amString = this.get(AMSTR_KEY); + + if(value == amString){ + this.set('time.ampm', AM); + } else{ + this.set('time.ampm', PM); + } + + }else{ + this.set('time.minute', value); + } + + } + this._syncTime(); + + this.syncUI(); + }, + + _handleOut:function(e){ + if(this._timer){ + this._timer.cancel(); + this._timer = null; + } + }, + + renderUI: function(){ + //FIXME: This could be more efficient! + + /* + current implementation builds three ordered lists, one for + each row. Then we use the makeCell private method tp create a cell + with the given class, based on string constants defined up top + */ + var cb = this.get('contentBox'), + m = this._model; + + //create row function is very simple... + function createRow(){ return cb.create('
                                ');} + + var row = []; + //only need three rows + for (var i=0; i <= 3; i++) { + row[i] = createRow(); + } + + //wrap make cell in node create + function mc (str, c){ + return cb.create(makeCell(str, c)); + } + + + m[AMPM_CLASS].AM = mc(this.get(AMSTR_KEY),AMPM_CLASS); + m[AMPM_CLASS].PM = mc(this.get(PMSTR_KEY),AMPM_CLASS); + row[0].appendChild(m[AMPM_CLASS].AM); + row[0].appendChild(m[AMPM_CLASS].PM); + + //build rows, creating a function to use only twice, but + //still remove duplicates + function assembleRow(start, row, max, step, c){ + for(var i = start; i<=max; i=i+step){ + var cell = mc(i, c); + m[c][i] = cell; + row.appendChild(cell); + } + } + + assembleRow(1, row[1], 12, 1, HOUR_CLASS); + + assembleRow(0, row[2], 45, 15, MINUTE_CLASS); + + this._model[AMPM_CLASS].row = row[0]; + this._model[HOUR_CLASS].row = row[1]; + this._model[MINUTE_CLASS].row = row[2]; + + var parent = cb.create('
                                '); + + + array.each(row, function(item){ + parent.appendChild(item); + }); + + cb.appendChild(parent); + + //store for later + this.allCells = cb.queryAll('li'); + + }, + + /** + * Show/hide the widget + * @method toggle + */ + toggle: function(){ + this[(this.get('visible') ? 'hide' : 'show')](); + this.syncUI(); //IE 6 has an issue without this + }, + + bindUI: function(){ + + var cb = this.get('contentBox'); + cb.on('click', this._handleClick, this); + cb.on('mouseover', this._handleOver, this); + cb.on('mouseout', this._handleOut, this); + }, + + syncUI: function(){ + + //get the current tine vlaue + var time = this.get('time'); + + //get all of the li elements to clear their active state + this.allCells.removeClass(Timepicker[ACTIVE_CLASS]); + + var m = this._model; + var apos = 0; + + //handle ampm row, because of l10n can't count on + //the value, so instead we use the "constant" + if(time.ampm == AM){ + m.ampm.AM.addClass(Timepicker[ACTIVE_CLASS]); + apos = m.ampm.AM.getX(); + }else if(time.ampm == PM){ + m.ampm.PM.addClass(Timepicker[ACTIVE_CLASS]); + apos = m.ampm.PM.getX(); + } + + //handle minute row + m.minute[time.minute].addClass(Timepicker[ACTIVE_CLASS]); + + + //handle hour row + m.hour[time.hour].addClass(Timepicker[ACTIVE_CLASS]); + + m.hour.row.setX(apos); + + m.minute.row.setX(m.hour[time.hour].getX()); + + } + }); + + Y.Base.build(Timepicker.NAME, Timepicker, {dynamic:false}); + Y.namespace(NAMESPACE +'.'+CONSTRUCTOR); + Y[NAMESPACE][CONSTRUCTOR] = Timepicker; + + +}, 'gallery-2009.11.02-20' ,{requires:['oop', 'event-custom', 'attribute', 'base', 'dom', 'classnamemanager', 'widget', 'event']}); diff --git a/build/gallery-treeview/assets/gallery-treeview-core.css b/build/gallery-treeview/assets/gallery-treeview-core.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/build/gallery-treeview/assets/skins/sam/check0.gif b/build/gallery-treeview/assets/skins/sam/check0.gif new file mode 100644 index 0000000000..193028b993 Binary files /dev/null and b/build/gallery-treeview/assets/skins/sam/check0.gif differ diff --git a/build/gallery-treeview/assets/skins/sam/check1.gif b/build/gallery-treeview/assets/skins/sam/check1.gif new file mode 100644 index 0000000000..7d9ceba384 Binary files /dev/null and b/build/gallery-treeview/assets/skins/sam/check1.gif differ diff --git a/build/gallery-treeview/assets/skins/sam/check2.gif b/build/gallery-treeview/assets/skins/sam/check2.gif new file mode 100644 index 0000000000..181317599b Binary files /dev/null and b/build/gallery-treeview/assets/skins/sam/check2.gif differ diff --git a/build/gallery-treeview/assets/skins/sam/gallery-treeview-skin.css b/build/gallery-treeview/assets/skins/sam/gallery-treeview-skin.css new file mode 100644 index 0000000000..a3f6300c3b --- /dev/null +++ b/build/gallery-treeview/assets/skins/sam/gallery-treeview-skin.css @@ -0,0 +1,243 @@ +/* +Copyright (c) 2008, Yahoo! Inc. All rights reserved. +Code licensed under the BSD License: +http://developer.yahoo.net/yui/license.txt +version: 2.5.2 +*/ + +/* the style of the div around each node */ +.ygtvitem { } + +table.ygtvtable { + margin-bottom:0; + border:none; + border-collapse:collapse; +} + +/*.ygtvitem td {*/ +td.ygtvcell { + border: none; + padding: 0; +} +a.ygtvspacer { + text-decoration:none; + outline-style:none; + display:block; +} + + +/* first or middle sibling, no children */ +.ygtvtn { + width:18px; height:22px; + background: url(treeview-sprite.gif) 0 -5600px no-repeat; + cursor:pointer ; +} + +/* first or middle sibling, collapsable */ +.ygtvtm { + width:18px; height:22px; + cursor:pointer ; + background: url(treeview-sprite.gif) 0 -4000px no-repeat; +} + +/* first or middle sibling, collapsable, hover */ +.ygtvtmh,.ygtvtmhh { + width:18px; height:22px; + cursor:pointer ; + background: url(treeview-sprite.gif) 0 -4800px no-repeat; +} + +/* first or middle sibling, expandable */ +.ygtvtp { + width:18px; height:22px; + cursor:pointer ; + background: url(treeview-sprite.gif) 0 -6400px no-repeat; +} + +/* first or middle sibling, expandable, hover */ +.ygtvtph ,.ygtvtphh { + width:18px; height:22px; + cursor:pointer ; + background: url(treeview-sprite.gif) 0 -7200px no-repeat; +} + +/* last sibling, no children */ +.ygtvln { + width:18px; height:22px; + background: url(treeview-sprite.gif) 0 -1600px no-repeat; + cursor:pointer ; +} + +/* Last sibling, collapsable */ +.ygtvlm { + width:18px; height:22px; + cursor:pointer ; + background: url(treeview-sprite.gif) 0 0px no-repeat; +} + +/* Last sibling, collapsable, hover */ +.ygtvlmh,.ygtvlmhh { + width:18px; height:22px; + cursor:pointer ; + background: url(treeview-sprite.gif) 0 -800px no-repeat; +} + +/* Last sibling, expandable */ +.ygtvlp { + width:18px; height:22px; + cursor:pointer ; + background: url(treeview-sprite.gif) 0 -2400px no-repeat; +} + +/* Last sibling, expandable, hover */ +.ygtvlph,.ygtvlphh { + width:18px; height:22px; cursor:pointer ; + background: url(treeview-sprite.gif) 0 -3200px no-repeat; + cursor:pointer ; +} + +/* Loading icon */ +.ygtvloading { + width:18px; height:22px; + background: url(treeview-loading.gif) 0 0 no-repeat; +} + +/* the style for the empty cells that are used for rendering the depth + * of the node */ +.ygtvdepthcell { + width:18px; height:22px; + background: url(treeview-sprite.gif) 0 -8000px no-repeat; +} + +.ygtvblankdepthcell { width:18px; height:22px; } + + +/* the style of the div around each node's collection of children */ +.ygtvchildren { } +* html .ygtvchildren { height:2%; } + +/* the style of the text label in ygTextNode */ +.ygtvlabel, .ygtvlabel:link, .ygtvlabel:visited, .ygtvlabel:hover { + margin-left:2px; + text-decoration: none; + background-color: white; /* workaround for IE font smoothing bug */ + cursor:pointer; +} + +.ygtvcontent { + cursor:default; +} + +.ygtvspacer { height: 22px; width: 18px; } + +.ygtvfocus { + background-color: #c0e0e0; + border: none; +} +.ygtvfocus .ygtvlabel, .ygtvfocus .ygtvlabel:link, .ygtvfocus .ygtvlabel:visited, .ygtvfocus .ygtvlabel:hover { + background-color: #c0e0e0; +} + +.ygtvfocus a { + outline-style:none; +} + + +.ygtvok { + width:18px; height:22px; + background: url(treeview-sprite.gif) 0 -8800px no-repeat; +} + +.ygtvok:hover { + background: url(treeview-sprite.gif) 0 -8844px no-repeat; +} + +.ygtvcancel { + width:18px; height:22px; + background: url(treeview-sprite.gif) 0 -8822px no-repeat; +} + +.ygtvcancel:hover { + background: url(treeview-sprite.gif) 0 -8866px no-repeat; +} + +.ygtv-label-editor { + background-color:#f2f2f2; + border: 1px solid silver; + position:absolute; + display:none; + overflow:hidden; + margin:auto; + z-index:9000; +} + +.ygtv-edit-TextNode { + width: 190px; +} + +.ygtv-edit-TextNode .ygtvcancel, .ygtv-edit-TextNode .ygtvok { + border:none; +} + +.ygtv-edit-TextNode .ygtv-button-container { + float: right; +} + +.ygtv-edit-TextNode .ygtv-input input{ + width: 140px; +} + +.ygtv-edit-DateNode .ygtvcancel { + border:none; +} +.ygtv-edit-DateNode .ygtvok { + display:none; +} + +.ygtv-edit-DateNode .ygtv-button-container { + text-align:right; + margin:auto; +} + +.ygtv-highlight .ygtv-highlight0 , .ygtv-highlight .ygtv-highlight0 .ygtvlabel{ +} + +.ygtv-highlight .ygtv-highlight1 , .ygtv-highlight .ygtv-highlight1 .ygtvlabel{ + background-color:blue; + color:white; +} + +.ygtv-highlight .ygtv-highlight2 , .ygtv-highlight .ygtv-highlight2 .ygtvlabel { + background-color:silver; +} + +.ygtv-highlight .ygtv-highlight0 .ygtvfocus .ygtvlabel, +.ygtv-highlight .ygtv-highlight1 .ygtvfocus .ygtvlabel, +.ygtv-highlight .ygtv-highlight2 .ygtvfocus .ygtvlabel { + background-color: #c0e0e0; +} + +.ygtv-highlight .ygtvcontent { + padding-right: 1em; +} + +.ygtv-checkbox .ygtv-highlight0 .ygtvcontent { + padding-left:1em; + background:url(check0.gif) no-repeat; +} + +.ygtv-checkbox .ygtv-highlight0 .ygtvfocus.ygtvcontent, +.ygtv-checkbox .ygtv-highlight1 .ygtvfocus.ygtvcontent , +.ygtv-checkbox .ygtv-highlight2 .ygtvfocus.ygtvcontent { + background-color:#c0e0e0; +} + +.ygtv-checkbox .ygtv-highlight1 .ygtvcontent { + padding-left:1em; + background:url(check1.gif) no-repeat; +} + +.ygtv-checkbox .ygtv-highlight2 .ygtvcontent{ + padding-left:1em; + background:url(check2.gif) no-repeat; +} diff --git a/build/gallery-treeview/assets/skins/sam/gallery-treeview.css b/build/gallery-treeview/assets/skins/sam/gallery-treeview.css new file mode 100644 index 0000000000..133bd0256b --- /dev/null +++ b/build/gallery-treeview/assets/skins/sam/gallery-treeview.css @@ -0,0 +1 @@ +table.ygtvtable{margin-bottom:0;border:none;border-collapse:collapse;}td.ygtvcell{border:none;padding:0;}a.ygtvspacer{text-decoration:none;outline-style:none;display:block;}.ygtvtn{width:18px;height:22px;background:url(treeview-sprite.gif) 0 -5600px no-repeat;cursor:pointer;}.ygtvtm{width:18px;height:22px;cursor:pointer;background:url(treeview-sprite.gif) 0 -4000px no-repeat;}.ygtvtmh,.ygtvtmhh{width:18px;height:22px;cursor:pointer;background:url(treeview-sprite.gif) 0 -4800px no-repeat;}.ygtvtp{width:18px;height:22px;cursor:pointer;background:url(treeview-sprite.gif) 0 -6400px no-repeat;}.ygtvtph,.ygtvtphh{width:18px;height:22px;cursor:pointer;background:url(treeview-sprite.gif) 0 -7200px no-repeat;}.ygtvln{width:18px;height:22px;background:url(treeview-sprite.gif) 0 -1600px no-repeat;cursor:pointer;}.ygtvlm{width:18px;height:22px;cursor:pointer;background:url(treeview-sprite.gif) 0 0 no-repeat;}.ygtvlmh,.ygtvlmhh{width:18px;height:22px;cursor:pointer;background:url(treeview-sprite.gif) 0 -800px no-repeat;}.ygtvlp{width:18px;height:22px;cursor:pointer;background:url(treeview-sprite.gif) 0 -2400px no-repeat;}.ygtvlph,.ygtvlphh{width:18px;height:22px;cursor:pointer;background:url(treeview-sprite.gif) 0 -3200px no-repeat;cursor:pointer;}.ygtvloading{width:18px;height:22px;background:url(treeview-loading.gif) 0 0 no-repeat;}.ygtvdepthcell{width:18px;height:22px;background:url(treeview-sprite.gif) 0 -8000px no-repeat;}.ygtvblankdepthcell{width:18px;height:22px;}* html .ygtvchildren{height:2%;}.ygtvlabel,.ygtvlabel:link,.ygtvlabel:visited,.ygtvlabel:hover{margin-left:2px;text-decoration:none;background-color:white;cursor:pointer;}.ygtvcontent{cursor:default;}.ygtvspacer{height:22px;width:18px;}.ygtvfocus{background-color:#c0e0e0;border:none;}.ygtvfocus .ygtvlabel,.ygtvfocus .ygtvlabel:link,.ygtvfocus .ygtvlabel:visited,.ygtvfocus .ygtvlabel:hover{background-color:#c0e0e0;}.ygtvfocus a{outline-style:none;}.ygtvok{width:18px;height:22px;background:url(treeview-sprite.gif) 0 -8800px no-repeat;}.ygtvok:hover{background:url(treeview-sprite.gif) 0 -8844px no-repeat;}.ygtvcancel{width:18px;height:22px;background:url(treeview-sprite.gif) 0 -8822px no-repeat;}.ygtvcancel:hover{background:url(treeview-sprite.gif) 0 -8866px no-repeat;}.ygtv-label-editor{background-color:#f2f2f2;border:1px solid silver;position:absolute;display:none;overflow:hidden;margin:auto;z-index:9000;}.ygtv-edit-TextNode{width:190px;}.ygtv-edit-TextNode .ygtvcancel,.ygtv-edit-TextNode .ygtvok{border:none;}.ygtv-edit-TextNode .ygtv-button-container{float:right;}.ygtv-edit-TextNode .ygtv-input input{width:140px;}.ygtv-edit-DateNode .ygtvcancel{border:none;}.ygtv-edit-DateNode .ygtvok{display:none;}.ygtv-edit-DateNode .ygtv-button-container{text-align:right;margin:auto;}.ygtv-highlight .ygtv-highlight1,.ygtv-highlight .ygtv-highlight1 .ygtvlabel{background-color:blue;color:white;}.ygtv-highlight .ygtv-highlight2,.ygtv-highlight .ygtv-highlight2 .ygtvlabel{background-color:silver;}.ygtv-highlight .ygtv-highlight0 .ygtvfocus .ygtvlabel,.ygtv-highlight .ygtv-highlight1 .ygtvfocus .ygtvlabel,.ygtv-highlight .ygtv-highlight2 .ygtvfocus .ygtvlabel{background-color:#c0e0e0;}.ygtv-highlight .ygtvcontent{padding-right:1em;}.ygtv-checkbox .ygtv-highlight0 .ygtvcontent{padding-left:1em;background:url(check0.gif) no-repeat;}.ygtv-checkbox .ygtv-highlight0 .ygtvfocus.ygtvcontent,.ygtv-checkbox .ygtv-highlight1 .ygtvfocus.ygtvcontent,.ygtv-checkbox .ygtv-highlight2 .ygtvfocus.ygtvcontent{background-color:#c0e0e0;}.ygtv-checkbox .ygtv-highlight1 .ygtvcontent{padding-left:1em;background:url(check1.gif) no-repeat;}.ygtv-checkbox .ygtv-highlight2 .ygtvcontent{padding-left:1em;background:url(check2.gif) no-repeat;} diff --git a/build/gallery-treeview/assets/skins/sam/loading.gif b/build/gallery-treeview/assets/skins/sam/loading.gif new file mode 100644 index 0000000000..0bbf3bc0c0 Binary files /dev/null and b/build/gallery-treeview/assets/skins/sam/loading.gif differ diff --git a/build/gallery-treeview/assets/skins/sam/treeview-loading.gif b/build/gallery-treeview/assets/skins/sam/treeview-loading.gif new file mode 100644 index 0000000000..0bbf3bc0c0 Binary files /dev/null and b/build/gallery-treeview/assets/skins/sam/treeview-loading.gif differ diff --git a/build/gallery-treeview/assets/skins/sam/treeview-sprite.gif b/build/gallery-treeview/assets/skins/sam/treeview-sprite.gif new file mode 100644 index 0000000000..8fb3f01377 Binary files /dev/null and b/build/gallery-treeview/assets/skins/sam/treeview-sprite.gif differ diff --git a/build/gallery-treeview/gallery-treeview-debug.js b/build/gallery-treeview/gallery-treeview-debug.js new file mode 100644 index 0000000000..ca709e8b5e --- /dev/null +++ b/build/gallery-treeview/gallery-treeview-debug.js @@ -0,0 +1,3810 @@ +YUI.add('gallery-treeview', function(Y) { + +(function () { + var YAHOO = Y.Port(), + Dom = Y.DOM, + Event = YAHOO.util.Event, + Lang = YAHOO.lang, + + KEY = { + ALT : 18, + BACK_SPACE : 8, + CAPS_LOCK : 20, + CONTROL : 17, + DELETE : 46, + DOWN : 40, + END : 35, + ENTER : 13, + ESCAPE : 27, + HOME : 36, + LEFT : 37, + META : 224, + NUM_LOCK : 144, + PAGE_DOWN : 34, + PAGE_UP : 33, + PAUSE : 19, + PRINTSCREEN : 44, + RIGHT : 39, + SCROLL_LOCK : 145, + SHIFT : 16, + SPACE : 32, + TAB : 9, + UP : 38 + }; + +YAHOO.widget = YAHOO.widget || {}; + +var Widget = YAHOO.widget; + +/** + * The treeview widget is a generic tree building tool. + * @module treeview + * @title TreeView Widget + * @requires yahoo, dom, event + * @optional animation, json, calendar + * @namespace YAHOO.widget + */ + +/** + * Contains the tree view state data and the root node. + * + * @class TreeView + * @uses YAHOO.util.EventProvider + * @constructor + * @param {string|HTMLElement} id The id of the element, or the element itself that the tree will be inserted into. + * Existing markup in this element, if valid, will be used to build the tree + * @param {Array|Object|String} oConfig (optional) If present, it will be used to build the tree via method buildTreeFromObject + * + */ +var TV = function(id, oConfig) { + + TV.superclass.constructor.call(this); + + if (id) { this._treeinit(id); } + if (oConfig) { + this.buildTreeFromObject(oConfig); + } else if (Lang.trim(this._el.innerHTML)) { + this.buildTreeFromMarkup(id); + } +}; + + +TV.KEY = KEY; +Widget.TreeView = TV; +var NS = Y.namespace('apm'); +NS.TreeView = TV; + +// YAHOO.augment(TV, YAHOO.util.EventProvider); +Y.extend(TV, Y.PortBase, { + + /** + * The id of tree container element + * @property id + * @type String + */ + id: null, + + /** + * The host element for this tree + * @property _el + * @private + * @type HTMLelement + */ + _el: null, + + /** + * Flat collection of all nodes in this tree. This is a sparse + * array, so the length property can't be relied upon for a + * node count for the tree. + * @property _nodes + * @type Node[] + * @private + */ + _nodes: null, + + /** + * We lock the tree control while waiting for the dynamic loader to return + * @property locked + * @type boolean + */ + locked: false, + + /** + * The animation to use for expanding children, if any + * @property _expandAnim + * @type string + * @private + */ + _expandAnim: null, + + /** + * The animation to use for collapsing children, if any + * @property _collapseAnim + * @type string + * @private + */ + _collapseAnim: null, + + /** + * The current number of animations that are executing + * @property _animCount + * @type int + * @private + */ + _animCount: 0, + + /** + * The maximum number of animations to run at one time. + * @property maxAnim + * @type int + */ + maxAnim: 2, + + /** + * Whether there is any subscriber to dblClickEvent + * @property _hasDblClickSubscriber + * @type boolean + * @private + */ + _hasDblClickSubscriber: false, + + /** + * Stores the timer used to check for double clicks + * @property _dblClickTimer + * @type window.timer object + * @private + */ + _dblClickTimer: null, + + /** + * A reference to the Node currently having the focus or null if none. + * @property currentFocus + * @type YAHOO.widget.Node + */ + currentFocus: null, + + /** + * If true, only one Node can be highlighted at a time + * @property singleNodeHighlight + * @type boolean + * @default false + */ + + singleNodeHighlight: false, + + /** + * A reference to the Node that is currently highlighted. + * It is only meaningful if singleNodeHighlight is enabled + * @property _currentlyHighlighted + * @type YAHOO.widget.Node + * @default null + * @private + */ + + _currentlyHighlighted: null, + + /** + * Sets up the animation for expanding children + * @method setExpandAnim + * @param {string} type the type of animation (acceptable values defined + * in YAHOO.widget.TVAnim) + */ + setExpandAnim: function(type) { + this._expandAnim = (Widget.TVAnim.isValid(type)) ? type : null; + }, + + /** + * Sets up the animation for collapsing children + * @method setCollapseAnim + * @param {string} type of animation (acceptable values defined in + * YAHOO.widget.TVAnim) + */ + setCollapseAnim: function(type) { + this._collapseAnim = (Widget.TVAnim.isValid(type)) ? type : null; + }, + + /** + * Perform the expand animation if configured, or just show the + * element if not configured or too many animations are in progress + * @method animateExpand + * @param el {HTMLElement} the element to animate + * @param node {YAHOO.util.Node} the node that was expanded + * @return {boolean} true if animation could be invoked, false otherwise + */ + animateExpand: function(el, node) { + Y.log("animating expand"); + + if (this._expandAnim && this._animCount < this.maxAnim) { + // this.locked = true; + var tree = this; + var a = Widget.TVAnim.getAnim(this._expandAnim, el, + function() { tree.expandComplete(node); }); + if (a) { + ++this._animCount; + this.fire("animStart", { + "node": node, + "type": "expand" + }); + a.animate(); + } + + return true; + } + + return false; + }, + + /** + * Perform the collapse animation if configured, or just show the + * element if not configured or too many animations are in progress + * @method animateCollapse + * @param el {HTMLElement} the element to animate + * @param node {YAHOO.util.Node} the node that was expanded + * @return {boolean} true if animation could be invoked, false otherwise + */ + animateCollapse: function(el, node) { + Y.log("animating collapse"); + + if (this._collapseAnim && this._animCount < this.maxAnim) { + // this.locked = true; + var tree = this; + var a = Widget.TVAnim.getAnim(this._collapseAnim, el, + function() { tree.collapseComplete(node); }); + if (a) { + ++this._animCount; + this.fire("animStart", { + "node": node, + "type": "collapse" + }); + a.animate(); + } + + return true; + } + + return false; + }, + + /** + * Function executed when the expand animation completes + * @method expandComplete + */ + expandComplete: function(node) { + Y.log("expand complete: " + this.id); + --this._animCount; + this.fire("animComplete", { + "node": node, + "type": "expand" + }); + // this.locked = false; + }, + + /** + * Function executed when the collapse animation completes + * @method collapseComplete + */ + collapseComplete: function(node) { + Y.log("collapse complete: " + this.id); + --this._animCount; + this.fire("animComplete", { + "node": node, + "type": "collapse" + }); + // this.locked = false; + }, + + /** + * Initializes the tree + * @method init + * @parm {string|HTMLElement} id the id of the element that will hold the tree + * @private + */ + _treeinit: function(id) { + this._el = Y.Selector.query('#' + id, null, true); + this.id = Y.guid(this._el,"yui-tv-auto-id-"); + + /** + * When animation is enabled, this event fires when the animation + * starts + * @event animStart + * @type CustomEvent + * @param {YAHOO.widget.Node} oArgs.node the node that is expanding/collapsing + * @param {String} oArgs.type the type of animation ("expand" or "collapse") + */ + this.publish("animStart", this); + + /** + * When animation is enabled, this event fires when the animation + * completes + * @event animComplete + * @type CustomEvent + * @param {YAHOO.widget.Node} oArgs.node the node that is expanding/collapsing + * @param {String} oArgs.type the type of animation ("expand" or "collapse") + */ + this.publish("animComplete", this); + + /** + * Fires when a node is going to be collapsed. Return false to stop + * the collapse. + * @event collapse + * @type CustomEvent + * @param {YAHOO.widget.Node} node the node that is collapsing + */ + this.publish("collapse", this); + + /** + * Fires after a node is successfully collapsed. This event will not fire + * if the "collapse" event was cancelled. + * @event collapseComplete + * @type CustomEvent + * @param {YAHOO.widget.Node} node the node that was collapsed + */ + this.publish("collapseComplete", this); + + /** + * Fires when a node is going to be expanded. Return false to stop + * the collapse. + * @event expand + * @type CustomEvent + * @param {YAHOO.widget.Node} node the node that is expanding + */ + this.publish("expand", this); + + /** + * Fires after a node is successfully expanded. This event will not fire + * if the "expand" event was cancelled. + * @event expandComplete + * @type CustomEvent + * @param {YAHOO.widget.Node} node the node that was expanded + */ + this.publish("expandComplete", this); + + /** + * Fires when the Enter key is pressed on a node that has the focus + * @event enterKeyPressed + * @type CustomEvent + * @param {YAHOO.widget.Node} node the node that has the focus + */ + this.publish("enterKeyPressed", this); + + /** + * Fires when the label in a TextNode or MenuNode or content in an HTMLNode receives a Click. + * The listener may return false to cancel toggling and focusing on the node. + * @event clickEvent + * @type CustomEvent + * @param oArgs.event {HTMLEvent} The event object + * @param oArgs.node {YAHOO.widget.Node} node the node that was clicked + */ + this.publish("clickEvent", this); + + /** + * Fires when the focus receives the focus, when it changes from a Node + * to another Node or when it is completely lost (blurred) + * @event focusChanged + * @type CustomEvent + * @param oArgs.oldNode {YAHOO.widget.Node} Node that had the focus or null if none + * @param oArgs.newNode {YAHOO.widget.Node} Node that receives the focus or null if none + */ + + this.publish('focusChanged',this); + + /** + * Fires when the label in a TextNode or MenuNode or content in an HTMLNode receives a double Click + * @event dblClickEvent + * @type CustomEvent + * @param oArgs.event {HTMLEvent} The event object + * @param oArgs.node {YAHOO.widget.Node} node the node that was clicked + */ + var self = this; + this.publish("dblClickEvent", { + onSubscribeCallback: function() { + self._hasDblClickSubscriber = true; + } + }); + + /** + * Custom event that is fired when the text node label is clicked. + * The node clicked is provided as an argument + * + * @event labelClick + * @type CustomEvent + * @param {YAHOO.widget.Node} node the node clicked + * @deprecated use clickEvent or dblClickEvent + */ + this.publish("labelClick", this); + + /** + * Custom event fired when the highlight of a node changes. + * The node that triggered the change is provided as an argument: + * The status of the highlight can be checked in + * nodeRef.highlightState. + * Depending on nodeRef.propagateHighlight, other nodes might have changed + * @event highlightEvent + * @type CustomEvent + * @param node {YAHOO.widget.Node} the node that started the change in highlighting state + */ + this.publish("highlightEvent",this); + + + + this._nodes = []; + + // store a global reference + TV.trees[this.id] = this; + + // Set up the root node + this.root = new Widget.RootNode(this); + + Y.log("tree init: " + this.id); + + if (this._initEditor) { + this._initEditor(); + } + + }, + + /** + * Builds the TreeView from an object. + * This is the method called by the constructor to build the tree when it has a second argument. + * A tree can be described by an array of objects, each object corresponding to a node. + * Node descriptions may contain values for any property of a node plus the following extra properties:
                                  + *
                                • type: can be one of the following:
                                    + *
                                  • A shortname for a node type ('text','menu','html')
                                  • + *
                                  • The name of a Node class under YAHOO.widget ('TextNode', 'MenuNode', 'DateNode', etc)
                                  • + *
                                  • a reference to an actual class: YAHOO.widget.DateNode
                                  • + *
                                • + *
                                • children: an array containing further node definitions
                                + * A string instead of an object will produce a node of type 'text' with the given string as its label. + * @method buildTreeFromObject + * @param oConfig {Array|Object|String} array containing a full description of the tree. + * An object or a string will be turned into an array with the given object or string as its only element. + * + */ + buildTreeFromObject: function (oConfig) { + var logger = Y; + logger.log('Building tree from object'); + var build = function (parent, oConfig) { + var i, item, node, children, type, NodeType, ThisType; + for (i = 0; i < oConfig.length; i++) { + item = oConfig[i]; + if (Lang.isString(item)) { + node = new Widget.TextNode(item, parent); + } else if (Lang.isObject(item)) { + children = item.children; + delete item.children; + type = item.type || 'text'; + delete item.type; + switch (Lang.isString(type) && type.toLowerCase()) { + case 'text': + node = new Widget.TextNode(item, parent); + break; + case 'menu': + node = new Widget.MenuNode(item, parent); + break; + case 'html': + node = new Widget.HTMLNode(item, parent); + break; + default: + if (Lang.isString(type)) { + NodeType = Widget[type]; + } else { + NodeType = type; + } + if (Lang.isObject(NodeType)) { + for (ThisType = NodeType; ThisType && ThisType !== Widget.Node; ThisType = ThisType.superclass.constructor) {} + if (ThisType) { + node = new NodeType(item, parent); + } else { + logger.log('Invalid type in node definition: ' + type,'error'); + } + } else { + logger.log('Invalid type in node definition: ' + type,'error'); + } + } + if (children) { + build(node,children); + } + } else { + logger.log('Invalid node definition','error'); + } + } + }; + if (!Lang.isArray(oConfig)) { + oConfig = [oConfig]; + } + + + build(this.root,oConfig); + }, +/** + * Builds the TreeView from existing markup. Markup should consist of <UL> or <OL> elements containing <LI> elements. + * Each <LI> can have one element used as label and a second optional element which is to be a <UL> or <OL> + * containing nested nodes. + * Depending on what the first element of the <LI> element is, the following Nodes will be created:
                                  + *
                                • plain text: a regular TextNode
                                • + *
                                • anchor <A>: a TextNode with its href and target taken from the anchor
                                • + *
                                • anything else: an HTMLNode
                                + * Only the first outermost (un-)ordered list in the markup and its children will be parsed. + * Nodes will be collapsed unless an <LI> tag has a className called 'expanded'. + * All other className attributes will be copied over to the Node className property. + * If the <LI> element contains an attribute called yuiConfig, its contents should be a JSON-encoded object + * as the one used in method buildTreeFromObject. + * @method buildTreeFromMarkup + * @param id {string|HTMLElement} The id of the element that contains the markup or a reference to it. + */ + buildTreeFromMarkup: function (id) { + Y.log('Building tree from existing markup'); + var build = function (markup) { + var el, child, branch = [], config = {}, label, yuiConfig; + // Dom's getFirstChild and getNextSibling skip over text elements + for (el = Dom.getFirstChild(markup); el; el = Dom.getNextSibling(el)) { + switch (el.tagName.toUpperCase()) { + case 'LI': + label = ''; + config = { + expanded: Dom.hasClass(el,'expanded'), + title: el.title || el.alt || null, + className: Lang.trim(el.className.replace(/\bexpanded\b/,'')) || null + }; + // I cannot skip over text elements here because I want them for labels + child = el.firstChild; + if (child.nodeType == 3) { + // nodes with only whitespace, tabs and new lines don't count, they are probably just formatting. + label = Lang.trim(child.nodeValue.replace(/[\n\t\r]*/g,'')); + if (label) { + config.type = 'text'; + config.label = label; + } else { + child = Dom.getNextSibling(child); + } + } + if (!label) { + if (child.tagName.toUpperCase() == 'A') { + config.type = 'text'; + config.label = child.innerHTML; + config.href = child.href; + config.target = child.target; + config.title = child.title || child.alt || config.title; + } else { + config.type = 'html'; + var d = document.createElement('div'); + d.appendChild(child.cloneNode(true)); + config.html = d.innerHTML; + config.hasIcon = true; + } + } + // see if after the label it has a further list which will become children of this node. + child = Dom.getNextSibling(child); + switch (child && child.tagName.toUpperCase()) { + case 'UL': + case 'OL': + config.children = build(child); + break; + } + // if there are further elements or text, it will be ignored. + + if (YAHOO.lang.JSON) { + yuiConfig = el.getAttribute('yuiConfig'); + if (yuiConfig) { + yuiConfig = YAHOO.lang.JSON.parse(yuiConfig); + config = YAHOO.lang.merge(config,yuiConfig); + } + } + + branch.push(config); + break; + case 'UL': + case 'OL': + Y.log('ULs or OLs can only contain LI elements, not other UL or OL. This will not work in some browsers','error'); + config = { + type: 'text', + label: '', + children: build(child) + }; + branch.push(config); + break; + } + } + return branch; + }; + + // var markup = Dom.getChildrenBy(Dom.get(id),function (el) { + // var tag = el.tagName.toUpperCase(); + // return tag == 'UL' || tag == 'OL'; + // }); + + var markup = Y.Selector.query('#' + this.id + '> ul,ol'); + + if (markup.length) { + this.buildTreeFromObject(build(markup[0])); + } else { + Y.log('Markup contains no UL or OL elements','warn'); + } + }, + /** + * Returns the TD element where the event has occurred + * @method _getEventTargetTdEl + * @private + */ + _getEventTargetTdEl: function (ev) { + var target = ev.target, + crit = 'td .ygtvrow', + targetEl, pnode = target.get('parentNode'); + + if (target && pnode && !pnode.hasClass('ygtvrow')) { + target = target.ancestor('td'); + pnode = target && target.get('parentNode'); + } + + if (!target) { + return null; + } + + targetEl = target._node || target; + + // If it is a spacer cell, do nothing + if (/\bygtv(blank)?depthcell/.test(targetEl.className)) { + return null; + } + + // If it has an id, search for the node number and see if it belongs to a node in this tree. + if (targetEl.id) { + var m = targetEl.id.match(/\bygtv([^\d]*)(.*)/); + if (m && m[2] && this._nodes[m[2]]) { + return targetEl; + } + } + + return null; + }, + /** + *.DOM, Event listener for click events + * @method _onClickEvent + * @private + */ + _onClickEvent: function (ev) { + var self = this, + td = this._getEventTargetTdEl(ev), + node, + target, + toggle = function (force) { + node.focus(); + if (force || !node.href) { + node.toggle(); + try { + ev.preventDefault(); + } catch (e) { + // @TODO + // For some reason IE8 is providing an event object with + // most of the fields missing, but only when clicking on + // the node's label, and only when working with inline + // editing. This generates a "Member not found" error + // in that browser. Determine if this is a browser + // bug, or a problem with this code. Already checked to + // see if the problem has to do with access the event + // in the outer scope, and that isn't the problem. + // Maybe the markup for inline editing is broken. + } + } + }; + + if (!td) { + return; + } + + node = this.getNodeByElement(td); + if (!node) { + return; + } + + // exception to handle deprecated event labelClick + // @TODO take another look at this deprecation. It is common for people to + // only be interested in the label click, so why make them have to test + // the node type to figure out whether the click was on the label? + + // target = Event.getTarget(ev); + target = ev.target; + + // if (Dom.hasClass(target, node.labelStyle) || Dom.getAncestorByClassName(target,node.labelStyle)) { + if (target.hasClass(node.labelStyle) || target.ancestor(node.labelStyle)) { + Y.log("onLabelClick " + node.label); + this.fire('labelClick', node); + } + + // If it is a toggle cell, toggle + if (/\bygtv[tl][mp]h?h?/.test(td.className)) { + toggle(true); + } else { + if (this._dblClickTimer) { + window.clearTimeout(this._dblClickTimer); + this._dblClickTimer = null; + } else { + if (this._hasDblClickSubscriber) { + this._dblClickTimer = window.setTimeout(function () { + self._dblClickTimer = null; + if (self.fire('clickEvent', {event:ev,node:node}) !== false) { + toggle(); + } + }, 200); + } else { + if (self.fire('clickEvent', {event:ev,node:node}) !== false) { + toggle(); + } + } + } + } + }, + + /** + * Event listener for double-click events + * @method _onDblClickEvent + * @private + */ + _onDblClickEvent: function (ev) { + if (!this._hasDblClickSubscriber) { + return; + } + + var td = this._getEventTargetTdEl(ev); + if (!td) { + return; + } + + if (!(/\bygtv[tl][mp]h?h?/.test(td.className))) { + + this.fire('dblClickEvent', { + event: ev, + node: this.getNodeByElement(td) + }); + + if (this._dblClickTimer) { + window.clearTimeout(this._dblClickTimer); + this._dblClickTimer = null; + } + } + }, + /** + * Event listener for mouse over events + * @method _onMouseOverEvent + * @private + */ + _onMouseOverEvent:function (ev) { + var target; + if ((target = this._getEventTargetTdEl(ev)) && (target = this.getNodeByElement(target)) && (target = target.getToggleEl())) { + target.className = target.className.replace(/\bygtv([lt])([mp])\b/gi,'ygtv$1$2h'); + } + }, + /** + * Event listener for mouse out events + * @method _onMouseOutEvent + * @private + */ + _onMouseOutEvent: function (ev) { + var target; + if ((target = this._getEventTargetTdEl(ev)) && (target = this.getNodeByElement(target)) && (target = target.getToggleEl())) { + target.className = target.className.replace(/\bygtv([lt])([mp])h\b/gi,'ygtv$1$2'); + } + }, + /** + * Event listener for key down events + * @method _onKeyDownEvent + * @private + */ + _onKeyDownEvent: function (ev) { + var target = ev.target, + targetEl = target._node, + node = this.getNodeByElement(targetEl), + newNode = node; + + switch(ev.keyCode) { + case KEY.UP: + Y.log('UP'); + do { + if (newNode.previousSibling) { + newNode = newNode.previousSibling; + } else { + newNode = newNode.parent; + } + } while (newNode && !newNode._canHaveFocus()); + if (newNode) { newNode.focus(); } + ev.preventDefault(); + break; + case KEY.DOWN: + Y.log('DOWN'); + do { + if (newNode.nextSibling) { + newNode = newNode.nextSibling; + } else { + newNode.expand(); + newNode = (newNode.children.length || null) && newNode.children[0]; + } + } while (newNode && !newNode._canHaveFocus); + if (newNode) { newNode.focus();} + ev.preventDefault(); + break; + case KEY.LEFT: + Y.log('LEFT'); + do { + if (newNode.parent) { + newNode = newNode.parent; + } else { + newNode = newNode.previousSibling; + } + } while (newNode && !newNode._canHaveFocus()); + if (newNode) { newNode.focus();} + ev.preventDefault(); + break; + case KEY.RIGHT: + Y.log('RIGHT'); + var self = this, + moveFocusRight, + focusOnExpand = function (newNode) { + self.unsubscribe('expandComplete',focusOnExpand); + moveFocusRight(newNode); + }; + moveFocusRight = function (newNode) { + do { + if (newNode.isDynamic() && !newNode.childrenRendered) { + self.subscribe('expandComplete',focusOnExpand); + newNode.expand(); + newNode = null; + break; + } else { + newNode.expand(); + if (newNode.children.length) { + newNode = newNode.children[0]; + } else { + newNode = newNode.nextSibling; + } + } + } while (newNode && !newNode._canHaveFocus()); + if (newNode) { newNode.focus();} + }; + + moveFocusRight(newNode); + ev.preventDefault(); + break; + case KEY.ENTER: + Y.log('ENTER: ' + newNode.href); + if (node.href) { + if (node.target) { + window.open(node.href,node.target); + } else { + window.location(node.href); + } + } else { + node.toggle(); + } + this.fire('enterKeyPressed',node); + ev.preventDefault(); + break; + case KEY.HOME: + Y.log('HOME'); + newNode = this.getRoot(); + if (newNode.children.length) {newNode = newNode.children[0];} + if (newNode._canHaveFocus()) { newNode.focus(); } + ev.preventDefault(); + break; + case KEY.END: + Y.log('END'); + newNode = newNode.parent.children; + newNode = newNode[newNode.length -1]; + if (newNode._canHaveFocus()) { newNode.focus(); } + ev.preventDefault(); + break; + // case KEY.PAGE_UP: + // Y.log('PAGE_UP'); + // break; + // case KEY.PAGE_DOWN: + // Y.log('PAGE_DOWN'); + // break; + case 107: // plus key + if (ev.shiftKey) { + Y.log('Shift-PLUS'); + node.parent.expandAll(); + } else { + Y.log('PLUS'); + node.expand(); + } + break; + case 109: // minus key + if (ev.shiftKey) { + Y.log('Shift-MINUS'); + node.parent.collapseAll(); + } else { + Y.log('MINUS'); + node.collapse(); + } + break; + default: + break; + } + }, + /** + * Renders the tree boilerplate and visible nodes + * @method render + */ + render: function() { + var html = this.root.getHtml(), + el = this.getEl(); + el.innerHTML = html; + if (!this._hasEvents) { + Y.on('click', this._onClickEvent, el, this); + Y.on('dblclick', this._onDblClickEvent, el, this, true); + Y.on('mouseover', this._onMouseOverEvent, el, this, true); + Y.on('mouseout', this._onMouseOutEvent, el, this, true); + Y.on('keydown', this._onKeyDownEvent, el, this, true); + } + this._hasEvents = true; + }, + + /** + * Returns the tree's host element + * @method getEl + * @return {HTMLElement} the host element + */ + getEl: function() { + if (! this._el) { + var el = Y.Selector.query('#' + this.id, null, true); + this._el = el; + + } + return this._el; + }, + + /** + * Nodes register themselves with the tree instance when they are created. + * @method regNode + * @param node {Node} the node to register + * @private + */ + regNode: function(node) { + this._nodes[node.index] = node; + }, + + /** + * Returns the root node of this tree + * @method getRoot + * @return {Node} the root node + */ + getRoot: function() { + return this.root; + }, + + /** + * Configures this tree to dynamically load all child data + * @method setDynamicLoad + * @param {function} fnDataLoader the function that will be called to get the data + * @param iconMode {int} configures the icon that is displayed when a dynamic + * load node is expanded the first time without children. By default, the + * "collapse" icon will be used. If set to 1, the leaf node icon will be + * displayed. + */ + setDynamicLoad: function(fnDataLoader, iconMode) { + this.root.setDynamicLoad(fnDataLoader, iconMode); + }, + + /** + * Expands all child nodes. Note: this conflicts with the "multiExpand" + * node property. If expand all is called in a tree with nodes that + * do not allow multiple siblings to be displayed, only the last sibling + * will be expanded. + * @method expandAll + */ + expandAll: function() { + if (!this.locked) { + this.root.expandAll(); + } + }, + + /** + * Collapses all expanded child nodes in the entire tree. + * @method collapseAll + */ + collapseAll: function() { + if (!this.locked) { + this.root.collapseAll(); + } + }, + + /** + * Returns a node in the tree that has the specified index (this index + * is created internally, so this function probably will only be used + * in html generated for a given node.) + * @method getNodeByIndex + * @param {int} nodeIndex the index of the node wanted + * @return {Node} the node with index=nodeIndex, null if no match + */ + getNodeByIndex: function(nodeIndex) { + var n = this._nodes[nodeIndex]; + return (n) ? n : null; + }, + + /** + * Returns a node that has a matching property and value in the data + * object that was passed into its constructor. + * @method getNodeByProperty + * @param {object} property the property to search (usually a string) + * @param {object} value the value we want to find (usuall an int or string) + * @return {Node} the matching node, null if no match + */ + getNodeByProperty: function(property, value) { + for (var i in this._nodes) { + if (this._nodes.hasOwnProperty(i)) { + var n = this._nodes[i]; + if ((property in n && n[property] == value) || (n.data && value == n.data[property])) { + return n; + } + } + } + + return null; + }, + + /** + * Returns a collection of nodes that have a matching property + * and value in the data object that was passed into its constructor. + * @method getNodesByProperty + * @param {object} property the property to search (usually a string) + * @param {object} value the value we want to find (usuall an int or string) + * @return {Array} the matching collection of nodes, null if no match + */ + getNodesByProperty: function(property, value) { + var values = []; + for (var i in this._nodes) { + if (this._nodes.hasOwnProperty(i)) { + var n = this._nodes[i]; + if ((property in n && n[property] == value) || (n.data && value == n.data[property])) { + values.push(n); + } + } + } + + return (values.length) ? values : null; + }, + + + /** + * Returns a collection of nodes that have passed the test function + * passed as its only argument. + * The function will receive a reference to each node to be tested. + * @method getNodesBy + * @param {function} a boolean function that receives a Node instance and returns true to add the node to the results list + * @return {Array} the matching collection of nodes, null if no match + */ + getNodesBy: function(fn) { + var values = []; + for (var i in this._nodes) { + if (this._nodes.hasOwnProperty(i)) { + var n = this._nodes[i]; + if (fn(n)) { + values.push(n); + } + } + } + return (values.length) ? values : null; + }, + /** + * Returns the treeview node reference for an ancestor element + * of the node, or null if it is not contained within any node + * in this tree. + * @method getNodeByElement + * @param el {HTMLElement} the element to test + * @return {YAHOO.widget.Node} a node reference or null + */ + getNodeByElement: function(el) { + + var p=el, m, re=/ygtv([^\d]*)(.*)/; + + do { + + if (p && p.id) { + m = p.id.match(re); + if (m && m[2]) { + return this.getNodeByIndex(m[2]); + } + } + + p = p.parentNode; + + if (!p || !p.tagName) { + break; + } + + } + while (p.id !== this.id && p.tagName.toLowerCase() !== "body"); + + return null; + }, + + /** + * When in singleNodeHighlight it returns the node highlighted + * or null if none. Returns null if singleNodeHighlight is false. + * @method getHighlightedNode + * @return {YAHOO.widget.Node} a node reference or null + */ + getHighlightedNode: function() { + return this._currentlyHighlighted; + }, + + + /** + * Removes the node and its children, and optionally refreshes the + * branch of the tree that was affected. + * @method removeNode + * @param {Node} node to remove + * @param {boolean} autoRefresh automatically refreshes branch if true + * @return {boolean} False is there was a problem, true otherwise. + */ + removeNode: function(node, autoRefresh) { + + // Don't delete the root node + if (node.isRoot()) { + return false; + } + + // Get the branch that we may need to refresh + var p = node.parent; + if (p.parent) { + p = p.parent; + } + + // Delete the node and its children + this._deleteNode(node); + + // Refresh the parent of the parent + if (autoRefresh && p && p.childrenRendered) { + p.refresh(); + } + + return true; + }, + + /** + * wait until the animation is complete before deleting + * to avoid javascript errors + * @method _removeChildren_animComplete + * @param o the custom event payload + * @private + */ + _removeChildren_animComplete: function(o) { + this.unsubscribe(this._removeChildren_animComplete); + this.removeChildren(o.node); + }, + + /** + * Deletes this nodes child collection, recursively. Also collapses + * the node, and resets the dynamic load flag. The primary use for + * this method is to purge a node and allow it to fetch its data + * dynamically again. + * @method removeChildren + * @param {Node} node the node to purge + */ + removeChildren: function(node) { + + if (node.expanded) { + // wait until the animation is complete before deleting to + // avoid javascript errors + if (this._collapseAnim) { + this.subscribe("animComplete", + this._removeChildren_animComplete, this, true); + Widget.Node.prototype.collapse.call(node); + return; + } + + node.collapse(); + } + + Y.log("Removing children for " + node); + while (node.children.length) { + this._deleteNode(node.children[0]); + } + + if (node.isRoot()) { + Widget.Node.prototype.expand.call(node); + } + + node.childrenRendered = false; + node.dynamicLoadComplete = false; + + node.updateIcon(); + }, + + /** + * Deletes the node and recurses children + * @method _deleteNode + * @private + */ + _deleteNode: function(node) { + // Remove all the child nodes first + this.removeChildren(node); + + // Remove the node from the tree + this.popNode(node); + }, + + /** + * Removes the node from the tree, preserving the child collection + * to make it possible to insert the branch into another part of the + * tree, or another tree. + * @method popNode + * @param {Node} node to remove + */ + popNode: function(node) { + var p = node.parent; + + // Update the parent's collection of children + var a = []; + + for (var i=0, len=p.children.length;i
                        + * @property highlightState + * @type integer + * @default 0 + */ + + highlightState: 0, + + /** + * Tells whether highlighting will be propagated up to the parents of the clicked node + * @property propagateHighlightUp + * @type boolean + * @default false + */ + + propagateHighlightUp: false, + + /** + * Tells whether highlighting will be propagated down to the children of the clicked node + * @property propagateHighlightDown + * @type boolean + * @default false + */ + + propagateHighlightDown: false, + + /** + * User-defined className to be added to the Node + * @property className + * @type string + * @default null + */ + + className: null, + + /** + * The node type + * @property _type + * @private + * @type string + * @default "Node" +*/ + _type: "Node", + + /* + spacerPath: "http://l.yimg.com/a/i/space.gif", + expandedText: "Expanded", + collapsedText: "Collapsed", + loadingText: "Loading", + */ + + /** + * Initializes this node, gets some of the properties from the parent + * @method init + * @param oData {object} a string or object containing the data that will + * be used to render this node + * @param oParent {Node} this node's parent node + * @param expanded {boolean} the initial expanded/collapsed state + */ + _nodeinit: function(oData, oParent, expanded) { + + YAHOO.widget.Node.superclass.constructor.call(this); + + this.data = {}; + this.children = []; + this.index = YAHOO.widget.TreeView.nodeCount; + ++YAHOO.widget.TreeView.nodeCount; + this.contentElId = "ygtvcontentel" + this.index; + + if (Lang.isObject(oData)) { + for (var property in oData) { + if (oData.hasOwnProperty(property)) { + if (property.charAt(0) != '_' && !Lang.isUndefined(this[property]) && !Lang.isFunction(this[property]) ) { + this[property] = oData[property]; + } else { + this.data[property] = oData[property]; + } + } + } + } + if (!Lang.isUndefined(expanded) ) { this.expanded = expanded; } + + /** + * The parentChange event is fired when a parent element is applied + * to the node. This is useful if you need to apply tree-level + * properties to a tree that need to happen if a node is moved from + * one tree to another. + * + * @event parentChange + * @type CustomEvent + */ + this.publish("parentChange"); + + // oParent should never be null except when we create the root node. + if (oParent) { + oParent.appendChild(this); + } + }, + + /** + * Certain properties for the node cannot be set until the parent + * is known. This is called after the node is inserted into a tree. + * the parent is also applied to this node's children in order to + * make it possible to move a branch from one tree to another. + * @method applyParent + * @param {Node} parentNode this node's parent node + * @return {boolean} true if the application was successful + */ + applyParent: function(parentNode) { + if (!parentNode) { + return false; + } + + this.tree = parentNode.tree; + this.parent = parentNode; + this.depth = parentNode.depth + 1; + + // @todo why was this put here. This causes new nodes added at the + // root level to lose the menu behavior. + // if (! this.multiExpand) { + // this.multiExpand = parentNode.multiExpand; + // } + + this.tree.regNode(this); + parentNode.childrenRendered = false; + + // cascade update existing children + for (var i=0, len=this.children.length;i 0 || + (checkForLazyLoad && this.isDynamic() && !this.dynamicLoadComplete) + ); + } + }, + + /** + * Expands if node is collapsed, collapses otherwise. + * @method toggle + */ + toggle: function() { + if (!this.tree.locked && ( this.hasChildren(true) || this.isDynamic()) ) { + if (this.expanded) { this.collapse(); } else { this.expand(); } + } + }, + + /** + * Returns the markup for this node and its children. + * @method getHtml + * @return {string} the markup for this node and its expanded children. + */ + getHtml: function() { + + this.childrenRendered = false; + + return ['
                        ' ,this.getNodeHtml() , this.getChildrenHtml() ,'
                        '].join(""); + }, + + /** + * Called when first rendering the tree. We always build the div that will + * contain this nodes children, but we don't render the children themselves + * unless this node is expanded. + * @method getChildrenHtml + * @return {string} the children container div html and any expanded children + * @private + */ + getChildrenHtml: function() { + + + var sb = []; + sb[sb.length] = '
                        = this.depth || depth < 0) { + Y.log("illegal getAncestor depth: " + depth); + return null; + } + + var p = this.parent; + + while (p.depth > depth) { + p = p.parent; + } + + return p; + }, + + /** + * Returns the css class for the spacer at the specified depth for + * this node. If this node's ancestor at the specified depth + * has a next sibling the presentation is different than if it + * does not have a next sibling + * @method getDepthStyle + * @param {int} depth the depth of the ancestor. + * @return {string} the css class for the spacer + */ + getDepthStyle: function(depth) { + return (this.getAncestor(depth).nextSibling) ? + "ygtvdepthcell" : "ygtvblankdepthcell"; + }, + + /** + * Get the markup for the node. This may be overrided so that we can + * support different types of nodes. + * @method getNodeHtml + * @return {string} The HTML that will render this node. + */ + getNodeHtml: function() { + Y.log("Generating html"); + var sb = []; + + sb[sb.length] = ''; + + for (var i=0;i
                        '; + } + + if (this.hasIcon) { + sb[sb.length] = '
                        '; + } + + sb[sb.length] = '
                          0; + }, + /** + * Removes the focus of previously selected Node + * @method _removeFocus + * @private + */ + _removeFocus:function () { + if (this._focusedItem) { + Y.Event.detach('blur', this._focusedItem); + this._focusedItem = null; + } + var el; + while ((el = this._focusHighlightedItems.shift())) { // yes, it is meant as an assignment, really + Dom.removeClass(el,YAHOO.widget.TreeView.FOCUS_CLASS_NAME); + } + }, + /** + * Sets the focus on the node element. + * It will only be able to set the focus on nodes that have anchor elements in it. + * Toggle or branch icons have anchors and can be focused on. + * If will fail in nodes that have no anchor + * @method focus + * @return {boolean} success + */ + focus: function () { + var focused = false, self = this; + + if (this.tree.currentFocus) { + this.tree.currentFocus._removeFocus(); + } + + var expandParent = function (node) { + if (node.parent) { + expandParent(node.parent); + node.parent.expand(); + } + }; + expandParent(this); + + var root = new Y.Node(self.getEl().firstChild); + + var tds = root.queryAll('td'); + + tds.each(function(node) { + + if ((/ygtv(([tl][pmn]h?)|(content))/).test(node.get('className'))) { + + node.addClass(YAHOO.widget.TreeView.FOCUS_CLASS_NAME); + + if (!focused) { + + var a = node.query('a', null, true); + if (a) { + var aRef = a._node; + a.focus(); + self._focusedItem = aRef; + a.on('blur', function () { + self.tree.fire('focusChanged', { + oldNode: self.tree.currentFocus, + newNode: null + }); + self.tree.currentFocus = null; + self._removeFocus(); + }); + focused = true; + } + } + self._focusHighlightedItems.push(node._node); + } + }); + + if (focused) { + this.tree.fire('focusChanged',{oldNode:this.tree.currentFocus,newNode:this}); + this.tree.currentFocus = this; + } else { + this.tree.fire('focusChanged',{oldNode:self.tree.currentFocus,newNode:null}); + this.tree.currentFocus = null; + this._removeFocus(); + } + return focused; + }, + + /** + * Count of nodes in a branch + * @method getNodeCount + * @return {int} number of nodes in the branch + */ + getNodeCount: function() { + for (var i = 0, count = 0;i< this.children.length;i++) { + count += this.children[i].getNodeCount(); + } + return count + 1; + }, + + /** + * Returns an object which could be used to build a tree out of this node and its children. + * It can be passed to the tree constructor to reproduce this node as a tree. + * It will return false if the node or any children loads dynamically, regardless of whether it is loaded or not. + * @method getNodeDefinition + * @return {Object | false} definition of the tree or false if the node or any children is defined as dynamic + */ + getNodeDefinition: function() { + + if (this.isDynamic()) { return false; } + + var def, defs = Lang.merge(this.data), children = []; + + + + if (this.expanded) {defs.expanded = this.expanded; } + if (!this.multiExpand) { defs.multiExpand = this.multiExpand; } + if (!this.renderHidden) { defs.renderHidden = this.renderHidden; } + if (!this.hasIcon) { defs.hasIcon = this.hasIcon; } + if (this.nowrap) { defs.nowrap = this.nowrap; } + if (this.className) { defs.className = this.className; } + if (this.editable) { defs.editable = this.editable; } + if (this.enableHighlight) { defs.enableHighlight = this.enableHighlight; } + if (this.highlightState) { defs.highlightState = this.highlightState; } + if (this.propagateHighlightUp) { defs.propagateHighlightUp = this.propagateHighlightUp; } + if (this.propagateHighlightDown) { defs.propagateHighlightDown = this.propagateHighlightDown; } + defs.type = this._type; + + + + for (var i = 0; i < this.children.length;i++) { + def = this.children[i].getNodeDefinition(); + if (def === false) { return false;} + children.push(def); + } + if (children.length) { defs.children = children; } + return defs; + }, + + + /** + * Generates the link that will invoke this node's toggle method + * @method getToggleLink + * @return {string} the javascript url for toggling this node + */ + getToggleLink: function() { + return 'return false;'; + }, + + /** + * Sets the value of property for this node and all loaded descendants. + * Only public and defined properties can be set, not methods. + * Values for unknown properties will be assigned to the refNode.data object + * @method setNodesProperty + * @param name {string} Name of the property to be set + * @param value {any} value to be set + * @param refresh {boolean} if present and true, it does a refresh + */ + setNodesProperty: function(name, value, refresh) { + if (name.charAt(0) != '_' && !Lang.isUndefined(this[name]) && !Lang.isFunction(this[name]) ) { + this[name] = value; + } else { + this.data[name] = value; + } + for (var i = 0; i < this.children.length;i++) { + this.children[i].setNodesProperty(name,value); + } + if (refresh) { + this.refresh(); + } + }, + /** + * Toggles the highlighted state of a Node + * @method toggleHighlight + */ + toggleHighlight: function() { + if (this.enableHighlight) { + // unhighlights only if fully highligthed. For not or partially highlighted it will highlight + if (this.highlightState == 1) { + this.unhighlight(); + } else { + this.highlight(); + } + } + }, + + /** + * Turns highlighting on node. + * @method highlight + * @param _silent {boolean} optional, don't fire the highlightEvent + */ + highlight: function(_silent) { + if (this.enableHighlight) { + if (this.tree.singleNodeHighlight) { + if (this.tree._currentlyHighlighted) { + this.tree._currentlyHighlighted.unhighlight(_silent); + } + this.tree._currentlyHighlighted = this; + } + this.highlightState = 1; + this._setHighlightClassName(); + if (!this.tree.singleNodeHighlight) { + if (this.propagateHighlightDown) { + for (var i = 0;i < this.children.length;i++) { + this.children[i].highlight(true); + } + } + if (this.propagateHighlightUp) { + if (this.parent) { + this.parent._childrenHighlighted(); + } + } + } + if (!_silent) { + this.tree.fire('highlightEvent',this); + } + } + }, + /** + * Turns highlighting off a node. + * @method unhighlight + * @param _silent {boolean} optional, don't fire the highlightEvent + */ + unhighlight: function(_silent) { + if (this.enableHighlight) { + // might have checked singleNodeHighlight but it wouldn't really matter either way + this.tree._currentlyHighlighted = null; + this.highlightState = 0; + this._setHighlightClassName(); + if (!this.tree.singleNodeHighlight) { + if (this.propagateHighlightDown) { + for (var i = 0;i < this.children.length;i++) { + this.children[i].unhighlight(true); + } + } + if (this.propagateHighlightUp) { + if (this.parent) { + this.parent._childrenHighlighted(); + } + } + } + if (!_silent) { + this.tree.fire('highlightEvent',this); + } + } + }, + /** + * Checks whether all or part of the children of a node are highlighted and + * sets the node highlight to full, none or partial highlight. + * If set to propagate it will further call the parent + * @method _childrenHighlighted + * @private + */ + _childrenHighlighted: function() { + var yes = false, no = false; + if (this.enableHighlight) { + for (var i = 0;i < this.children.length;i++) { + switch(this.children[i].highlightState) { + case 0: + no = true; + break; + case 1: + yes = true; + break; + case 2: + yes = no = true; + break; + } + } + if (yes && no) { + this.highlightState = 2; + } else if (yes) { + this.highlightState = 1; + } else { + this.highlightState = 0; + } + this._setHighlightClassName(); + if (this.propagateHighlightUp) { + if (this.parent) { + this.parent._childrenHighlighted(); + } + } + } + }, + + /** + * Changes the classNames on the toggle and content containers to reflect the current highlighting + * @method _setHighlightClassName + * @private + */ + _setHighlightClassName: function() { + var el = document.getElementById('ygtvtableel' + this.index); + if (el) { + el.className = el.className.replace(/\bygtv-highlight\d\b/gi,'ygtv-highlight' + this.highlightState); + } + } + +}); + +})(); +(function () { + var YAHOO = Y.Port(); +/** + * A custom YAHOO.widget.Node that handles the unique nature of + * the virtual, presentationless root node. + * @namespace YAHOO.widget + * @class RootNode + * @extends YAHOO.widget.Node + * @param oTree {YAHOO.widget.TreeView} The tree instance this node belongs to + * @constructor + */ +var RootNode = function(oTree) { + // Initialize the node with null params. The root node is a + // special case where the node has no presentation. So we have + // to alter the standard properties a bit. + this._nodeinit(null, null, true); + + /* + * For the root node, we get the tree reference from as a param + * to the constructor instead of from the parent element. + */ + this.tree = oTree; +}; + +YAHOO.widget.RootNode = RootNode; +var NS = Y.namespace('apm'); +NS.RootNode = YAHOO.widget.RootNode; + +Y.extend(RootNode, YAHOO.widget.Node, { + + /** + * The node type + * @property _type + * @type string + * @private + * @default "RootNode" + */ + _type: "RootNode", + + // overrides YAHOO.widget.Node + getNodeHtml: function() { + return ""; + }, + + toString: function() { + return this._type; + }, + + loadComplete: function() { + this.tree.draw(); + }, + + /** + * Count of nodes in tree. + * It overrides Nodes.getNodeCount because the root node should not be counted. + * @method getNodeCount + * @return {int} number of nodes in the tree + */ + getNodeCount: function() { + for (var i = 0, count = 0;i< this.children.length;i++) { + count += this.children[i].getNodeCount(); + } + return count; + }, + + /** + * Returns an object which could be used to build a tree out of this node and its children. + * It can be passed to the tree constructor to reproduce this node as a tree. + * Since the RootNode is automatically created by treeView, + * its own definition is excluded from the returned node definition + * which only contains its children. + * @method getNodeDefinition + * @return {Object | false} definition of the tree or false if any child node is defined as dynamic + */ + getNodeDefinition: function() { + + for (var def, defs = [], i = 0; i < this.children.length;i++) { + def = this.children[i].getNodeDefinition(); + if (def === false) { return false;} + defs.push(def); + } + return defs; + }, + + collapse: function() {}, + expand: function() {}, + getSiblings: function() { return null; }, + focus: function () {} + +}); +})(); +(function () { + var YAHOO = Y.Port(), + Lang = YAHOO.lang; +/** + * The default node presentation. The first parameter should be + * either a string that will be used as the node's label, or an object + * that has at least a string property called label. By default, clicking the + * label will toggle the expanded/collapsed state of the node. By + * setting the href property of the instance, this behavior can be + * changed so that the label will go to the specified href. + * @namespace YAHOO.widget + * @class TextNode + * @extends YAHOO.widget.Node + * @constructor + * @param oData {object} a string or object containing the data that will + * be used to render this node. + * Providing a string is the same as providing an object with a single property named label. + * All values in the oData will be used to set equally named properties in the node + * as long as the node does have such properties, they are not undefined, private or functions. + * All attributes are made available in noderef.data, which + * can be used to store custom attributes. TreeView.getNode(s)ByProperty + * can be used to retrieve a node by one of the attributes. + * @param oParent {YAHOO.widget.Node} this node's parent node + * @param expanded {boolean} the initial expanded/collapsed state (deprecated; use oData.expanded) + */ +YAHOO.widget.TextNode = function(oData, oParent, expanded) { + + if (oData) { + if (Lang.isString(oData)) { + oData = { label: oData }; + } + this._nodeinit(oData, oParent, expanded); + this.setUpLabel(oData); + } +}; +var NS = Y.namespace('apm'); +NS.TextNode = YAHOO.widget.TextNode; + +Y.extend(YAHOO.widget.TextNode, YAHOO.widget.Node, { + + /** + * The CSS class for the label href. Defaults to ygtvlabel, but can be + * overridden to provide a custom presentation for a specific node. + * @property labelStyle + * @type string + */ + labelStyle: "ygtvlabel", + + /** + * The derived element id of the label for this node + * @property labelElId + * @type string + */ + labelElId: null, + + /** + * The text for the label. It is assumed that the oData parameter will + * either be a string that will be used as the label, or an object that + * has a property called "label" that we will use. + * @property label + * @type string + */ + label: null, + + /** + * The text for the title (tooltip) for the label element + * @property title + * @type string + */ + title: null, + + /** + * The href for the node's label. If one is not specified, the href will + * be set so that it toggles the node. + * @property href + * @type string + */ + href: null, + + /** + * The label href target, defaults to current window + * @property target + * @type string + */ + target: "_self", + + /** + * The node type + * @property _type + * @private + * @type string + * @default "TextNode" + */ + _type: "TextNode", + + + /** + * Sets up the node label + * @method setUpLabel + * @param oData string containing the label, or an object with a label property + */ + setUpLabel: function(oData) { + + if (Lang.isString(oData)) { + oData = { + label: oData + }; + } else { + if (oData.style) { + this.labelStyle = oData.style; + } + } + + this.label = oData.label; + + this.labelElId = "ygtvlabelel" + this.index; + + }, + + /** + * Returns the label element + * @for YAHOO.widget.TextNode + * @method getLabelEl + * @return {object} the element + */ + getLabelEl: function() { + return document.getElementById(this.labelElId); + }, + + // overrides YAHOO.widget.Node + getContentHtml: function() { + var sb = []; + sb[sb.length] = this.href?'':''; + return sb.join(""); + }, + + + + /** + * Returns an object which could be used to build a tree out of this node and its children. + * It can be passed to the tree constructor to reproduce this node as a tree. + * It will return false if the node or any descendant loads dynamically, regardless of whether it is loaded or not. + * @method getNodeDefinition + * @return {Object | false} definition of the tree or false if this node or any descendant is defined as dynamic + */ + getNodeDefinition: function() { + var def = YAHOO.widget.TextNode.superclass.getNodeDefinition.call(this); + if (def === false) { return false; } + + // Node specific properties + def.label = this.label; + if (this.labelStyle != 'ygtvlabel') { def.style = this.labelStyle; } + if (this.title) { def.title = this.title; } + if (this.href) { def.href = this.href; } + if (this.target != '_self') { def.target = this.target; } + + return def; + + }, + + toString: function() { + return YAHOO.widget.TextNode.superclass.toString.call(this) + ": " + this.label; + }, + + // deprecated + onLabelClick: function() { + return false; + }, + refresh: function() { + YAHOO.widget.TextNode.superclass.refresh.call(this); + var label = this.getLabelEl(); + label.innerHTML = this.label; + if (label.tagName.toUpperCase() == 'A') { + label.href = this.href; + label.target = this.target; + } + } + + + + +}); +})(); +(function () { + var YAHOO = Y.Port(); +/** + * A menu-specific implementation that differs from TextNode in that only + * one sibling can be expanded at a time. + * @namespace YAHOO.widget + * @class MenuNode + * @extends YAHOO.widget.TextNode + * @param oData {object} a string or object containing the data that will + * be used to render this node. + * Providing a string is the same as providing an object with a single property named label. + * All values in the oData will be used to set equally named properties in the node + * as long as the node does have such properties, they are not undefined, private or functions. + * All attributes are made available in noderef.data, which + * can be used to store custom attributes. TreeView.getNode(s)ByProperty + * can be used to retrieve a node by one of the attributes. + * @param oParent {YAHOO.widget.Node} this node's parent node + * @param expanded {boolean} the initial expanded/collapsed state (deprecated; use oData.expanded) + * @constructor + */ +YAHOO.widget.MenuNode = function(oData, oParent, expanded) { + YAHOO.widget.MenuNode.superclass.constructor.call(this,oData,oParent,expanded); + + /* + * Menus usually allow only one branch to be open at a time. + */ + this.multiExpand = false; + +}; +var NS = Y.namespace('apm'); +NS.MenuNode = YAHOO.widget.MenuNode; + +Y.extend(YAHOO.widget.MenuNode, YAHOO.widget.TextNode, { + + /** + * The node type + * @property _type + * @private + * @default "MenuNode" + */ + _type: "MenuNode" + +}); +})(); +(function () { + var YAHOO = Y.Port(), + Lang = YAHOO.lang; + +/** + * This implementation takes either a string or object for the + * oData argument. If is it a string, it will use it for the display + * of this node (and it can contain any html code). If the parameter + * is an object,it looks for a parameter called "html" that will be + * used for this node's display. + * @namespace YAHOO.widget + * @class HTMLNode + * @extends YAHOO.widget.Node + * @constructor + * @param oData {object} a string or object containing the data that will + * be used to render this node. + * Providing a string is the same as providing an object with a single property named html. + * All values in the oData will be used to set equally named properties in the node + * as long as the node does have such properties, they are not undefined, private or functions. + * All other attributes are made available in noderef.data, which + * can be used to store custom attributes. TreeView.getNode(s)ByProperty + * can be used to retrieve a node by one of the attributes. + * @param oParent {YAHOO.widget.Node} this node's parent node + * @param expanded {boolean} the initial expanded/collapsed state (deprecated; use oData.expanded) + * @param hasIcon {boolean} specifies whether or not leaf nodes should + * be rendered with or without a horizontal line line and/or toggle icon. If the icon + * is not displayed, the content fills the space it would have occupied. + * This option operates independently of the leaf node presentation logic + * for dynamic nodes. + * (deprecated; use oData.hasIcon) + */ +YAHOO.widget.HTMLNode = function(oData, oParent, expanded, hasIcon) { + if (oData) { + this.init(oData, oParent, expanded); + this.initContent(oData, hasIcon); + } +}; +var NS = Y.namespace('apm'); +NS.HTMLNode = YAHOO.widget.HTMLNode; + +Y.extend(YAHOO.widget.HTMLNode, YAHOO.widget.Node, { + + /** + * The CSS class for the html content container. Defaults to ygtvhtml, but + * can be overridden to provide a custom presentation for a specific node. + * @property contentStyle + * @type string + */ + contentStyle: "ygtvhtml", + + + /** + * The HTML content to use for this node's display + * @property html + * @type string + */ + html: null, + +/** + * The node type + * @property _type + * @private + * @type string + * @default "HTMLNode" + */ + _type: "HTMLNode", + + /** + * Sets up the node label + * @property initContent + * @param oData {object} An html string or object containing an html property + * @param hasIcon {boolean} determines if the node will be rendered with an + * icon or not + */ + initContent: function(oData, hasIcon) { + this.setHtml(oData); + this.contentElId = "ygtvcontentel" + this.index; + if (!Lang.isUndefined(hasIcon)) { this.hasIcon = hasIcon; } + + }, + + /** + * Synchronizes the node.html, and the node's content + * @property setHtml + * @param o {object} An html string or object containing an html property + */ + setHtml: function(o) { + + this.html = (typeof o === "string") ? o : o.html; + + var el = this.getContentEl(); + if (el) { + el.innerHTML = this.html; + } + + }, + + // overrides YAHOO.widget.Node + getContentHtml: function() { + return this.html; + }, + + /** + * Returns an object which could be used to build a tree out of this node and its children. + * It can be passed to the tree constructor to reproduce this node as a tree. + * It will return false if any node loads dynamically, regardless of whether it is loaded or not. + * @method getNodeDefinition + * @return {Object | false} definition of the tree or false if any node is defined as dynamic + */ + getNodeDefinition: function() { + var def = YAHOO.widget.HTMLNode.superclass.getNodeDefinition.call(this); + if (def === false) { return false; } + def.html = this.html; + return def; + + } +}); +})(); +(function () { + var YAHOO = Y.Port(), + Dom = YAHOO.util.Dom, + Lang = YAHOO.lang, + Event = YAHOO.util.Event, + TV = YAHOO.widget.TreeView, + TVproto = TV.prototype; + + /** + * An object to store information used for in-line editing + * for all Nodes of all TreeViews. It contains: + *
                          + *
                        • active {boolean}, whether there is an active cell editor
                        • + *
                        • whoHasIt {YAHOO.widget.TreeView} TreeView instance that is currently using the editor
                        • + *
                        • nodeType {string} value of static Node._type property, allows reuse of input element if node is of the same type.
                        • + *
                        • editorPanel {HTMLelement (<div>)} element holding the in-line editor
                        • + *
                        • inputContainer {HTMLelement (<div>)} element which will hold the type-specific input element(s) to be filled by the fillEditorContainer method
                        • + *
                        • buttonsContainer {HTMLelement (<div>)} element which holds the <button> elements for Ok/Cancel. If you don't want any of the buttons, hide it via CSS styles, don't destroy it
                        • + *
                        • node {YAHOO.widget.Node} reference to the Node being edited
                        • + *
                        • saveOnEnter {boolean}, whether the Enter key should be accepted as a Save command (Esc. is always taken as Cancel), disable for multi-line input elements
                        • + *
                        • oldValue {any} value before editing
                        • + *
                        + * Editors are free to use this object to store additional data. + * @property editorData + * @static + * @for YAHOO.widget.TreeView + */ + TV.editorData = { + active:false, + whoHasIt:null, // which TreeView has it + nodeType:null, + editorPanel:null, + inputContainer:null, + buttonsContainer:null, + node:null, // which Node is being edited + saveOnEnter:true, + oldValue:undefined + // Each node type is free to add its own properties to this as it sees fit. + }; + + /** + * Validator function for edited data, called from the TreeView instance scope, + * receives the arguments (newValue, oldValue, nodeInstance) + * and returns either the validated (or type-converted) value or undefined. + * An undefined return will prevent the editor from closing + * @property validator + * @type function + * @default null + * @for YAHOO.widget.TreeView + */ + TVproto.validator = null; + + /** + * Entry point for initializing the editing plug-in. + * TreeView will call this method on initializing if it exists + * @method _initEditor + * @for YAHOO.widget.TreeView + * @private + */ + + TVproto._initEditor = function () { + /** + * Fires when the user clicks on the ok button of a node editor + * @event editorSaveEvent + * @type CustomEvent + * @param oArgs.newValue {mixed} the new value just entered + * @param oArgs.oldValue {mixed} the value originally in the tree + * @param oArgs.node {YAHOO.widget.Node} the node that has the focus + * @for YAHOO.widget.TreeView + */ + // this.publish("editorSaveEvent"); + + /** + * Fires when the user clicks on the cancel button of a node editor + * @event editorCancelEvent + * @type CustomEvent + * @param {YAHOO.widget.Node} node the node that has the focus + * @for YAHOO.widget.TreeView + */ + // this.publish("editorCancelEvent"); + + }; + + /** + * Entry point of the editing plug-in. + * TreeView will call this method if it exists when a node label is clicked + * @method _nodeEditing + * @param node {YAHOO.widget.Node} the node to be edited + * @return {Boolean} true to indicate that the node is editable and prevent any further bubbling of the click. + * @for YAHOO.widget.TreeView + * @private + */ + TVproto._nodeEditing = function (node) { + + if (node.fillEditorContainer && node.editable) { + + var ed, topLeft, buttons, button, editorData = TV.editorData; + editorData.active = true; + editorData.whoHasIt = this; + if (!editorData.nodeType) { + editorData.editorPanel = ed = document.body.appendChild(document.createElement('div')); + Dom.addClass(ed,'ygtv-label-editor'); + + buttons = editorData.buttonsContainer = ed.appendChild(document.createElement('div')); + Dom.addClass(buttons,'ygtv-button-container'); + button = buttons.appendChild(document.createElement('button')); + Dom.addClass(button,'ygtvok'); + button.innerHTML = ' '; + button = buttons.appendChild(document.createElement('button')); + Dom.addClass(button,'ygtvcancel'); + button.innerHTML = ' '; + + Y.on('click', function (ev) { + Y.log('click on editor'); + var target = ev.target, + targetEl = target._node, + node = TV.editorData.node; + + // if (Dom.hasClass(target,'ygtvok')) { + if (target.hasClass('ygtvok')) { + Y.log('ygtvok'); + ev.halt(); + this._closeEditor(true); + } + // if (Dom.hasClass(target,'ygtvcancel')) { + if (target.hasClass('ygtvcancel')) { + Y.log('ygtvcancel'); + ev.halt(); + this._closeEditor(false); + } + }, buttons, this); + + editorData.inputContainer = ed.appendChild(document.createElement('div')); + Dom.addClass(editorData.inputContainer,'ygtv-input'); + + Y.on('keydown', function (ev) { + var editorData = TV.editorData, + KEY = Y.TreeView.KEY; + switch (ev.keyCode) { + case KEY.ENTER: + Y.log('ENTER'); + ev.halt(); + if (editorData.saveOnEnter) { + this._closeEditor(true); + } + break; + case KEY.ESCAPE: + Y.log('ESC'); + ev.halt(); + this._closeEditor(false); + break; + } + }, ed, this); + + + + } else { + ed = editorData.editorPanel; + } + editorData.node = node; + if (editorData.nodeType) { + Dom.removeClass(ed,'ygtv-edit-' + editorData.nodeType); + } + Dom.addClass(ed,' ygtv-edit-' + node._type); + topLeft = Dom.getXY(node.getContentEl()); + Dom.setStyle(ed,'left',topLeft[0] + 'px'); + Dom.setStyle(ed,'top',topLeft[1] + 'px'); + Dom.setStyle(ed,'display','block'); + ed.focus(); + node.fillEditorContainer(editorData); + + return true; // If inline editor available, don't do anything else. + } + }; + + /** + * Method to be associated with an event (clickEvent, dblClickEvent or enterKeyPressed) to pop up the contents editor + * It calls the corresponding node editNode method. + * @method onEventEditNode + * @param oArgs {object} Object passed as arguments to TreeView event listeners + * @for YAHOO.widget.TreeView + */ + TVproto.onEventEditNode = function (oArgs) { + if (oArgs instanceof YAHOO.widget.Node) { + oArgs.editNode(); + } else if (oArgs.node instanceof YAHOO.widget.Node) { + oArgs.node.editNode(); + } + }; + + /** + * Method to be called when the inline editing is finished and the editor is to be closed + * @method _closeEditor + * @param save {Boolean} true if the edited value is to be saved, false if discarded + * @private + * @for YAHOO.widget.TreeView + */ + TVproto._closeEditor = function (save) { + var ed = TV.editorData, + node = ed.node, + close = true; + if (save) { + close = (ed.node.saveEditorValue(ed) !== false); + } else { + this.fire('editorCancelEvent', node); + } + + if (close) { + Dom.setStyle(ed.editorPanel,'display','none'); + ed.active = false; + node.focus(); + } + }; + + /** + * Entry point for TreeView's destroy method to destroy whatever the editing plug-in has created + * @method _destroyEditor + * @private + * @for YAHOO.widget.TreeView + */ + TVproto._destroyEditor = function() { + var ed = TV.editorData; + if (ed && ed.nodeType && (!ed.active || ed.whoHasIt === this)) { + Y.Event.detach('keydown', ed.editorPanel); + Y.Event.detach('click', ed.buttonContainer); + ed.node.destroyEditorContents(ed); + document.body.removeChild(ed.editorPanel); + ed.nodeType = ed.editorPanel = ed.inputContainer = ed.buttonsContainer = ed.whoHasIt = ed.node = null; + ed.active = false; + } + }; + + var Nproto = YAHOO.widget.Node.prototype; + + /** + * Signals if the label is editable. (Ignored on TextNodes with href set.) + * @property editable + * @type boolean + * @for YAHOO.widget.Node + */ + Nproto.editable = false; + + /** + * pops up the contents editor, if there is one and the node is declared editable + * @method editNode + * @for YAHOO.widget.Node + */ + Nproto.editNode = function () { + this.tree._nodeEditing(this); + }; + + /** Placeholder for a function that should provide the inline node label editor. + * Leaving it set to null will indicate that this node type is not editable. + * It should be overridden by nodes that provide inline editing. + * The Node-specific editing element (input box, textarea or whatever) should be inserted into editorData.inputContainer. + * @method fillEditorContainer + * @param editorData {YAHOO.widget.TreeView.editorData} a shortcut to the static object holding editing information + * @return void + * @for YAHOO.widget.Node + */ + Nproto.fillEditorContainer = null; + + /** + * Node-specific destroy function to empty the contents of the inline editor panel. + * This function is the worst case alternative that will purge all possible events and remove the editor contents. + * Method Event.purgeElement is somewhat costly so if it can be replaced by specifc Event.removeListeners, it is better to do so. + * @method destroyEditorContents + * @param editorData {YAHOO.widget.TreeView.editorData} a shortcut to the static object holding editing information + * @for YAHOO.widget.Node + */ + Nproto.destroyEditorContents = function (editorData) { + // In the worst case, if the input editor (such as the Calendar) has no destroy method + // we can only try to remove all possible events on it. + Event.purgeElement(editorData.inputContainer,true); + editorData.inputContainer.innerHTML = ''; + }; + + /** + * Saves the value entered into the editor. + * @method saveEditorValue + * @param editorData {YAHOO.widget.TreeView.editorData} a shortcut to the static object holding editing information + * @return {false or none} a return of exactly false will prevent the editor from closing + * @for YAHOO.widget.Node + */ + Nproto.saveEditorValue = function (editorData) { + var node = editorData.node, + value, + validator = node.tree.validator; + + value = this.getEditorValue(editorData); + + if (Lang.isFunction(validator)) { + value = validator(value,editorData.oldValue,node); + if (Lang.isUndefined(value)) { + return false; + } + } + + var ret = this.tree.fire('editorSaveEvent', { + newValue:value, + oldValue:editorData.oldValue, + node:node + }); + + + if (ret !== false) { + this.displayEditedValue(value, editorData); + } + }; + + /** + * Returns the value(s) from the input element(s) . + * Should be overridden by each node type. + * @method getEditorValue + * @param editorData {YAHOO.widget.TreeView.editorData} a shortcut to the static object holding editing information + * @return {any} value entered + * @for YAHOO.widget.Node + */ + + Nproto.getEditorValue = function (editorData) { + }; + + /** + * Finally displays the newly edited value(s) in the tree. + * Should be overridden by each node type. + * @method displayEditedValue + * @param value {any} value to be displayed and stored in the node + * @param editorData {YAHOO.widget.TreeView.editorData} a shortcut to the static object holding editing information + * @for YAHOO.widget.Node + */ + Nproto.displayEditedValue = function (value,editorData) { + }; + + var TNproto = YAHOO.widget.TextNode.prototype; + + /** + * Places an <input> textbox in the input container and loads the label text into it. + * @method fillEditorContainer + * @param editorData {YAHOO.widget.TreeView.editorData} a shortcut to the static object holding editing information + * @return void + * @for YAHOO.widget.TextNode + */ + TNproto.fillEditorContainer = function (editorData) { + + var input; + // If last node edited is not of the same type as this one, delete it and fill it with our editor + if (editorData.nodeType != this._type) { + editorData.nodeType = this._type; + editorData.saveOnEnter = true; + editorData.node.destroyEditorContents(editorData); + + editorData.inputElement = input = editorData.inputContainer.appendChild(document.createElement('input')); + + } else { + // if the last node edited was of the same time, reuse the input element. + input = editorData.inputElement; + } + editorData.oldValue = this.label; + input.value = this.label; + input.focus(); + input.select(); + }; + + /** + * Returns the value from the input element. + * Overrides Node.getEditorValue. + * @method getEditorValue + * @param editorData {YAHOO.widget.TreeView.editorData} a shortcut to the static object holding editing information + * @return {string} value entered + * @for YAHOO.widget.TextNode + */ + TNproto.getEditorValue = function (editorData) { + return editorData.inputElement.value; + }; + + /** + * Finally displays the newly edited value in the tree. + * Overrides Node.displayEditedValue. + * @method displayEditedValue + * @param value {string} value to be displayed and stored in the node + * @param editorData {YAHOO.widget.TreeView.editorData} a shortcut to the static object holding editing information + * @for YAHOO.widget.TextNode + */ + TNproto.displayEditedValue = function (value,editorData) { + var node = editorData.node; + node.label = value; + node.getLabelEl().innerHTML = value; + }; + + /** + * Destroys the contents of the inline editor panel. + * Overrides Node.destroyEditorContent. + * Since we didn't set any event listeners on this inline editor, it is more efficient to avoid the generic method in Node. + * @method destroyEditorContents + * @param editorData {YAHOO.widget.TreeView.editorData} a shortcut to the static object holding editing information + * @for YAHOO.widget.TextNode + */ + TNproto.destroyEditorContents = function (editorData) { + editorData.inputContainer.innerHTML = ''; + }; +})(); + + +}, 'gallery-2009.11.19-20' ,{requires:['node','gallery-port']}); diff --git a/build/gallery-treeview/gallery-treeview-min.js b/build/gallery-treeview/gallery-treeview-min.js new file mode 100644 index 0000000000..ff53298585 --- /dev/null +++ b/build/gallery-treeview/gallery-treeview-min.js @@ -0,0 +1,6 @@ +YUI.add("gallery-treeview",function(A){(function(){var B=A.Port(),E=A.DOM,J=B.util.Event,G=B.lang,F={ALT:18,BACK_SPACE:8,CAPS_LOCK:20,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,META:224,NUM_LOCK:144,PAGE_DOWN:34,PAGE_UP:33,PAUSE:19,PRINTSCREEN:44,RIGHT:39,SCROLL_LOCK:145,SHIFT:16,SPACE:32,TAB:9,UP:38};B.widget=B.widget||{};var H=B.widget;var C=function(L,K){C.superclass.constructor.call(this);if(L){this._treeinit(L);}if(K){this.buildTreeFromObject(K);}else{if(G.trim(this._el.innerHTML)){this.buildTreeFromMarkup(L);}}};C.KEY=F;H.TreeView=C;var D=A.namespace("apm");D.TreeView=C;A.extend(C,A.PortBase,{id:null,_el:null,_nodes:null,locked:false,_expandAnim:null,_collapseAnim:null,_animCount:0,maxAnim:2,_hasDblClickSubscriber:false,_dblClickTimer:null,currentFocus:null,singleNodeHighlight:false,_currentlyHighlighted:null,setExpandAnim:function(K){this._expandAnim=(H.TVAnim.isValid(K))?K:null;},setCollapseAnim:function(K){this._collapseAnim=(H.TVAnim.isValid(K))?K:null;},animateExpand:function(M,N){if(this._expandAnim&&this._animCount ul,ol");if(K.length){this.buildTreeFromObject(L(K[0]));}else{}},_getEventTargetTdEl:function(M){var O=M.target,L="td .ygtvrow",P,N=O.get("parentNode");if(O&&N&&!N.hasClass("ygtvrow")){O=O.ancestor("td");N=O&&O.get("parentNode");}if(!O){return null;}P=O._node||O;if(/\bygtv(blank)?depthcell/.test(P.className)){return null;}if(P.id){var K=P.id.match(/\bygtv([^\d]*)(.*)/);if(K&&K[2]&&this._nodes[K[2]]){return P;}}return null;},_onClickEvent:function(N){var L=this,P=this._getEventTargetTdEl(N),M,O,K=function(Q){M.focus();if(Q||!M.href){M.toggle();try{N.preventDefault();}catch(R){}}};if(!P){return;}M=this.getNodeByElement(P);if(!M){return;}O=N.target;if(O.hasClass(M.labelStyle)||O.ancestor(M.labelStyle)){this.fire("labelClick",M);}if(/\bygtv[tl][mp]h?h?/.test(P.className)){K(true);}else{if(this._dblClickTimer){window.clearTimeout(this._dblClickTimer);this._dblClickTimer=null;}else{if(this._hasDblClickSubscriber){this._dblClickTimer=window.setTimeout(function(){L._dblClickTimer=null;if(L.fire("clickEvent",{event:N,node:M})!==false){K();}},200);}else{if(L.fire("clickEvent",{event:N,node:M})!==false){K();}}}}},_onDblClickEvent:function(K){if(!this._hasDblClickSubscriber){return;}var L=this._getEventTargetTdEl(K);if(!L){return;}if(!(/\bygtv[tl][mp]h?h?/.test(L.className))){this.fire("dblClickEvent",{event:K,node:this.getNodeByElement(L)});if(this._dblClickTimer){window.clearTimeout(this._dblClickTimer);this._dblClickTimer=null;}}},_onMouseOverEvent:function(K){var L;if((L=this._getEventTargetTdEl(K))&&(L=this.getNodeByElement(L))&&(L=L.getToggleEl())){L.className=L.className.replace(/\bygtv([lt])([mp])\b/gi,"ygtv$1$2h");}},_onMouseOutEvent:function(K){var L;if((L=this._getEventTargetTdEl(K))&&(L=this.getNodeByElement(L))&&(L=L.getToggleEl())){L.className=L.className.replace(/\bygtv([lt])([mp])h\b/gi,"ygtv$1$2");}},_onKeyDownEvent:function(O){var Q=O.target,R=Q._node,N=this.getNodeByElement(R),M=N; +switch(O.keyCode){case F.UP:do{if(M.previousSibling){M=M.previousSibling;}else{M=M.parent;}}while(M&&!M._canHaveFocus());if(M){M.focus();}O.preventDefault();break;case F.DOWN:do{if(M.nextSibling){M=M.nextSibling;}else{M.expand();M=(M.children.length||null)&&M.children[0];}}while(M&&!M._canHaveFocus);if(M){M.focus();}O.preventDefault();break;case F.LEFT:do{if(M.parent){M=M.parent;}else{M=M.previousSibling;}}while(M&&!M._canHaveFocus());if(M){M.focus();}O.preventDefault();break;case F.RIGHT:var L=this,P,K=function(S){L.unsubscribe("expandComplete",K);P(S);};P=function(S){do{if(S.isDynamic()&&!S.childrenRendered){L.subscribe("expandComplete",K);S.expand();S=null;break;}else{S.expand();if(S.children.length){S=S.children[0];}else{S=S.nextSibling;}}}while(S&&!S._canHaveFocus());if(S){S.focus();}};P(M);O.preventDefault();break;case F.ENTER:if(N.href){if(N.target){window.open(N.href,N.target);}else{window.location(N.href);}}else{N.toggle();}this.fire("enterKeyPressed",N);O.preventDefault();break;case F.HOME:M=this.getRoot();if(M.children.length){M=M.children[0];}if(M._canHaveFocus()){M.focus();}O.preventDefault();break;case F.END:M=M.parent.children;M=M[M.length-1];if(M._canHaveFocus()){M.focus();}O.preventDefault();break;case 107:if(O.shiftKey){N.parent.expandAll();}else{N.expand();}break;case 109:if(O.shiftKey){N.parent.collapseAll();}else{N.collapse();}break;default:break;}},render:function(){var K=this.root.getHtml(),L=this.getEl();L.innerHTML=K;if(!this._hasEvents){A.on("click",this._onClickEvent,L,this);A.on("dblclick",this._onDblClickEvent,L,this,true);A.on("mouseover",this._onMouseOverEvent,L,this,true);A.on("mouseout",this._onMouseOutEvent,L,this,true);A.on("keydown",this._onKeyDownEvent,L,this,true);}this._hasEvents=true;},getEl:function(){if(!this._el){var K=A.Selector.query("#"+this.id,null,true);this._el=K;}return this._el;},regNode:function(K){this._nodes[K.index]=K;},getRoot:function(){return this.root;},setDynamicLoad:function(K,L){this.root.setDynamicLoad(K,L);},expandAll:function(){if(!this.locked){this.root.expandAll();}},collapseAll:function(){if(!this.locked){this.root.collapseAll();}},getNodeByIndex:function(L){var K=this._nodes[L];return(K)?K:null;},getNodeByProperty:function(M,L){for(var K in this._nodes){if(this._nodes.hasOwnProperty(K)){var N=this._nodes[K];if((M in N&&N[M]==L)||(N.data&&L==N.data[M])){return N;}}}return null;},getNodesByProperty:function(N,M){var K=[];for(var L in this._nodes){if(this._nodes.hasOwnProperty(L)){var O=this._nodes[L];if((N in O&&O[N]==M)||(O.data&&M==O.data[N])){K.push(O);}}}return(K.length)?K:null;},getNodesBy:function(M){var K=[];for(var L in this._nodes){if(this._nodes.hasOwnProperty(L)){var N=this._nodes[L];if(M(N)){K.push(N);}}}return(K.length)?K:null;},getNodeByElement:function(M){var N=M,K,L=/ygtv([^\d]*)(.*)/;do{if(N&&N.id){K=N.id.match(L);if(K&&K[2]){return this.getNodeByIndex(K[2]);}}N=N.parentNode;if(!N||!N.tagName){break;}}while(N.id!==this.id&&N.tagName.toLowerCase()!=="body");return null;},getHighlightedNode:function(){return this._currentlyHighlighted;},removeNode:function(L,K){if(L.isRoot()){return false;}var M=L.parent;if(M.parent){M=M.parent;}this._deleteNode(L);if(K&&M&&M.childrenRendered){M.refresh();}return true;},_removeChildren_animComplete:function(K){this.unsubscribe(this._removeChildren_animComplete);this.removeChildren(K.node);},removeChildren:function(K){if(K.expanded){if(this._collapseAnim){this.subscribe("animComplete",this._removeChildren_animComplete,this,true);H.Node.prototype.collapse.call(K);return;}K.collapse();}while(K.children.length){this._deleteNode(K.children[0]);}if(K.isRoot()){H.Node.prototype.expand.call(K);}K.childrenRendered=false;K.dynamicLoadComplete=false;K.updateIcon();},_deleteNode:function(K){this.removeChildren(K);this.popNode(K);},popNode:function(N){var O=N.parent;var L=[];for(var M=0,K=O.children.length;M0||(F&&this.isDynamic()&&!this.dynamicLoadComplete));}},toggle:function(){if(!this.tree.locked&&(this.hasChildren(true)||this.isDynamic())){if(this.expanded){this.collapse();}else{this.expand();}}},getHtml:function(){this.childrenRendered=false;return['
                        ',this.getNodeHtml(),this.getChildrenHtml(),"
                        "].join("");},getChildrenHtml:function(){var F=[];F[F.length]='
                        ";return F.join("");},renderChildren:function(){var F=this;if(this.isDynamic()&&!this.dynamicLoadComplete){this.isLoading=true;this.tree.locked=true;if(this.dataLoader){setTimeout(function(){F.dataLoader(F,function(){F.loadComplete();});},10);}else{if(this.tree.root.dataLoader){setTimeout(function(){F.tree.root.dataLoader(F,function(){F.loadComplete(); +});},10);}else{return"Error: data loader not found or not specified.";}}return"";}else{return this.completeRender();}},completeRender:function(){var G=[];for(var F=0;F=this.depth||G<0){return null;}var F=this.parent;while(F.depth>G){F=F.parent;}return F;},getDepthStyle:function(F){return(this.getAncestor(F).nextSibling)?"ygtvdepthcell":"ygtvblankdepthcell";},getNodeHtml:function(){var G=[];G[G.length]='
                        ';}if(this.hasIcon){G[G.length]='';}G[G.length]='
                         
                        ";return G.join("");},getContentHtml:function(){return"";},refresh:function(){this.getChildrenEl().innerHTML=this.completeRender();if(this.hasIcon){var F=this.getToggleEl();if(F){F.className=F.className.replace(/\bygtv[lt][nmp]h*\b/gi,this.getStyle());}}},toString:function(){return this._type+" ("+this.index+")";},_focusHighlightedItems:[],_focusedItem:null,_canHaveFocus:function(){return this.getEl().getElementsByTagName("a").length>0;},_removeFocus:function(){if(this._focusedItem){A.Event.detach("blur",this._focusedItem);this._focusedItem=null;}var F;while((F=this._focusHighlightedItems.shift())){B.removeClass(F,E.widget.TreeView.FOCUS_CLASS_NAME);}},focus:function(){var J=false,G=this;if(this.tree.currentFocus){this.tree.currentFocus._removeFocus();}var I=function(K){if(K.parent){I(K.parent);K.parent.expand();}};I(this);var F=new A.Node(G.getEl().firstChild);var H=F.queryAll("td");H.each(function(L){if((/ygtv(([tl][pmn]h?)|(content))/).test(L.get("className"))){L.addClass(E.widget.TreeView.FOCUS_CLASS_NAME);if(!J){var K=L.query("a",null,true);if(K){var M=K._node;K.focus();G._focusedItem=M;K.on("blur",function(){G.tree.fire("focusChanged",{oldNode:G.tree.currentFocus,newNode:null});G.tree.currentFocus=null;G._removeFocus();});J=true;}}G._focusHighlightedItems.push(L._node);}});if(J){this.tree.fire("focusChanged",{oldNode:this.tree.currentFocus,newNode:this});this.tree.currentFocus=this;}else{this.tree.fire("focusChanged",{oldNode:G.tree.currentFocus,newNode:null});this.tree.currentFocus=null;this._removeFocus();}return J;},getNodeCount:function(){for(var F=0,G=0;F":"";return E.join("");},getNodeDefinition:function(){var E=D.widget.TextNode.superclass.getNodeDefinition.call(this);if(E===false){return false;}E.label=this.label;if(this.labelStyle!="ygtvlabel"){E.style=this.labelStyle;}if(this.title){E.title=this.title;}if(this.href){E.href=this.href;}if(this.target!="_self"){E.target=this.target;}return E;},toString:function(){return D.widget.TextNode.superclass.toString.call(this)+": "+this.label;},onLabelClick:function(){return false;},refresh:function(){D.widget.TextNode.superclass.refresh.call(this);var E=this.getLabelEl();E.innerHTML=this.label;if(E.tagName.toUpperCase()=="A"){E.href=this.href;E.target=this.target;}}});})();(function(){var C=A.Port();C.widget.MenuNode=function(F,E,D){C.widget.MenuNode.superclass.constructor.call(this,F,E,D);this.multiExpand=false;};var B=A.namespace("apm");B.MenuNode=C.widget.MenuNode;A.extend(C.widget.MenuNode,C.widget.TextNode,{_type:"MenuNode"});})();(function(){var D=A.Port(),C=D.lang;D.widget.HTMLNode=function(H,G,F,E){if(H){this.init(H,G,F);this.initContent(H,E);}};var B=A.namespace("apm");B.HTMLNode=D.widget.HTMLNode;A.extend(D.widget.HTMLNode,D.widget.Node,{contentStyle:"ygtvhtml",html:null,_type:"HTMLNode",initContent:function(F,E){this.setHtml(F);this.contentElId="ygtvcontentel"+this.index;if(!C.isUndefined(E)){this.hasIcon=E;}},setHtml:function(F){this.html=(typeof F==="string")?F:F.html;var E=this.getContentEl();if(E){E.innerHTML=this.html;}},getContentHtml:function(){return this.html;},getNodeDefinition:function(){var E=D.widget.HTMLNode.superclass.getNodeDefinition.call(this);if(E===false){return false;}E.html=this.html;return E;}});})();(function(){var I=A.Port(),F=I.util.Dom,G=I.lang,C=I.util.Event,E=I.widget.TreeView,D=E.prototype;E.editorData={active:false,whoHasIt:null,nodeType:null,editorPanel:null,inputContainer:null,buttonsContainer:null,node:null,saveOnEnter:true,oldValue:undefined};D.validator=null;D._initEditor=function(){};D._nodeEditing=function(O){if(O.fillEditorContainer&&O.editable){var K,M,N,L,J=E.editorData;J.active=true;J.whoHasIt=this;if(!J.nodeType){J.editorPanel=K=document.body.appendChild(document.createElement("div"));F.addClass(K,"ygtv-label-editor");N=J.buttonsContainer=K.appendChild(document.createElement("div"));F.addClass(N,"ygtv-button-container");L=N.appendChild(document.createElement("button"));F.addClass(L,"ygtvok");L.innerHTML=" ";L=N.appendChild(document.createElement("button"));F.addClass(L,"ygtvcancel");L.innerHTML=" ";A.on("click",function(Q){var R=Q.target,S=R._node,P=E.editorData.node;if(R.hasClass("ygtvok")){Q.halt();this._closeEditor(true);}if(R.hasClass("ygtvcancel")){Q.halt();this._closeEditor(false);}},N,this);J.inputContainer=K.appendChild(document.createElement("div"));F.addClass(J.inputContainer,"ygtv-input");A.on("keydown",function(R){var Q=E.editorData,P=A.TreeView.KEY;switch(R.keyCode){case P.ENTER:R.halt();if(Q.saveOnEnter){this._closeEditor(true);}break;case P.ESCAPE:R.halt();this._closeEditor(false);break;}},K,this);}else{K=J.editorPanel;}J.node=O;if(J.nodeType){F.removeClass(K,"ygtv-edit-"+J.nodeType);}F.addClass(K," ygtv-edit-"+O._type);M=F.getXY(O.getContentEl());F.setStyle(K,"left",M[0]+"px");F.setStyle(K,"top",M[1]+"px");F.setStyle(K,"display","block");K.focus();O.fillEditorContainer(J);return true;}};D.onEventEditNode=function(J){if(J instanceof I.widget.Node){J.editNode();}else{if(J.node instanceof I.widget.Node){J.node.editNode();}}};D._closeEditor=function(L){var J=E.editorData,K=J.node,M=true;if(L){M=(J.node.saveEditorValue(J)!==false);}else{this.fire("editorCancelEvent",K);}if(M){F.setStyle(J.editorPanel,"display","none");J.active=false;K.focus();}};D._destroyEditor=function(){var J=E.editorData;if(J&&J.nodeType&&(!J.active||J.whoHasIt===this)){A.Event.detach("keydown",J.editorPanel);A.Event.detach("click",J.buttonContainer);J.node.destroyEditorContents(J); +document.body.removeChild(J.editorPanel);J.nodeType=J.editorPanel=J.inputContainer=J.buttonsContainer=J.whoHasIt=J.node=null;J.active=false;}};var H=I.widget.Node.prototype;H.editable=false;H.editNode=function(){this.tree._nodeEditing(this);};H.fillEditorContainer=null;H.destroyEditorContents=function(J){C.purgeElement(J.inputContainer,true);J.inputContainer.innerHTML="";};H.saveEditorValue=function(J){var M=J.node,N,L=M.tree.validator;N=this.getEditorValue(J);if(G.isFunction(L)){N=L(N,J.oldValue,M);if(G.isUndefined(N)){return false;}}var K=this.tree.fire("editorSaveEvent",{newValue:N,oldValue:J.oldValue,node:M});if(K!==false){this.displayEditedValue(N,J);}};H.getEditorValue=function(J){};H.displayEditedValue=function(K,J){};var B=I.widget.TextNode.prototype;B.fillEditorContainer=function(K){var J;if(K.nodeType!=this._type){K.nodeType=this._type;K.saveOnEnter=true;K.node.destroyEditorContents(K);K.inputElement=J=K.inputContainer.appendChild(document.createElement("input"));}else{J=K.inputElement;}K.oldValue=this.label;J.value=this.label;J.focus();J.select();};B.getEditorValue=function(J){return J.inputElement.value;};B.displayEditedValue=function(L,J){var K=J.node;K.label=L;K.getLabelEl().innerHTML=L;};B.destroyEditorContents=function(J){J.inputContainer.innerHTML="";};})();},"gallery-2009.11.19-20",{requires:["node","gallery-port"]}); \ No newline at end of file diff --git a/build/gallery-treeview/gallery-treeview.js b/build/gallery-treeview/gallery-treeview.js new file mode 100644 index 0000000000..40756d87ee --- /dev/null +++ b/build/gallery-treeview/gallery-treeview.js @@ -0,0 +1,3747 @@ +YUI.add('gallery-treeview', function(Y) { + +(function () { + var YAHOO = Y.Port(), + Dom = Y.DOM, + Event = YAHOO.util.Event, + Lang = YAHOO.lang, + + KEY = { + ALT : 18, + BACK_SPACE : 8, + CAPS_LOCK : 20, + CONTROL : 17, + DELETE : 46, + DOWN : 40, + END : 35, + ENTER : 13, + ESCAPE : 27, + HOME : 36, + LEFT : 37, + META : 224, + NUM_LOCK : 144, + PAGE_DOWN : 34, + PAGE_UP : 33, + PAUSE : 19, + PRINTSCREEN : 44, + RIGHT : 39, + SCROLL_LOCK : 145, + SHIFT : 16, + SPACE : 32, + TAB : 9, + UP : 38 + }; + +YAHOO.widget = YAHOO.widget || {}; + +var Widget = YAHOO.widget; + +/** + * The treeview widget is a generic tree building tool. + * @module treeview + * @title TreeView Widget + * @requires yahoo, dom, event + * @optional animation, json, calendar + * @namespace YAHOO.widget + */ + +/** + * Contains the tree view state data and the root node. + * + * @class TreeView + * @uses YAHOO.util.EventProvider + * @constructor + * @param {string|HTMLElement} id The id of the element, or the element itself that the tree will be inserted into. + * Existing markup in this element, if valid, will be used to build the tree + * @param {Array|Object|String} oConfig (optional) If present, it will be used to build the tree via method buildTreeFromObject + * + */ +var TV = function(id, oConfig) { + + TV.superclass.constructor.call(this); + + if (id) { this._treeinit(id); } + if (oConfig) { + this.buildTreeFromObject(oConfig); + } else if (Lang.trim(this._el.innerHTML)) { + this.buildTreeFromMarkup(id); + } +}; + + +TV.KEY = KEY; +Widget.TreeView = TV; +var NS = Y.namespace('apm'); +NS.TreeView = TV; + +// YAHOO.augment(TV, YAHOO.util.EventProvider); +Y.extend(TV, Y.PortBase, { + + /** + * The id of tree container element + * @property id + * @type String + */ + id: null, + + /** + * The host element for this tree + * @property _el + * @private + * @type HTMLelement + */ + _el: null, + + /** + * Flat collection of all nodes in this tree. This is a sparse + * array, so the length property can't be relied upon for a + * node count for the tree. + * @property _nodes + * @type Node[] + * @private + */ + _nodes: null, + + /** + * We lock the tree control while waiting for the dynamic loader to return + * @property locked + * @type boolean + */ + locked: false, + + /** + * The animation to use for expanding children, if any + * @property _expandAnim + * @type string + * @private + */ + _expandAnim: null, + + /** + * The animation to use for collapsing children, if any + * @property _collapseAnim + * @type string + * @private + */ + _collapseAnim: null, + + /** + * The current number of animations that are executing + * @property _animCount + * @type int + * @private + */ + _animCount: 0, + + /** + * The maximum number of animations to run at one time. + * @property maxAnim + * @type int + */ + maxAnim: 2, + + /** + * Whether there is any subscriber to dblClickEvent + * @property _hasDblClickSubscriber + * @type boolean + * @private + */ + _hasDblClickSubscriber: false, + + /** + * Stores the timer used to check for double clicks + * @property _dblClickTimer + * @type window.timer object + * @private + */ + _dblClickTimer: null, + + /** + * A reference to the Node currently having the focus or null if none. + * @property currentFocus + * @type YAHOO.widget.Node + */ + currentFocus: null, + + /** + * If true, only one Node can be highlighted at a time + * @property singleNodeHighlight + * @type boolean + * @default false + */ + + singleNodeHighlight: false, + + /** + * A reference to the Node that is currently highlighted. + * It is only meaningful if singleNodeHighlight is enabled + * @property _currentlyHighlighted + * @type YAHOO.widget.Node + * @default null + * @private + */ + + _currentlyHighlighted: null, + + /** + * Sets up the animation for expanding children + * @method setExpandAnim + * @param {string} type the type of animation (acceptable values defined + * in YAHOO.widget.TVAnim) + */ + setExpandAnim: function(type) { + this._expandAnim = (Widget.TVAnim.isValid(type)) ? type : null; + }, + + /** + * Sets up the animation for collapsing children + * @method setCollapseAnim + * @param {string} type of animation (acceptable values defined in + * YAHOO.widget.TVAnim) + */ + setCollapseAnim: function(type) { + this._collapseAnim = (Widget.TVAnim.isValid(type)) ? type : null; + }, + + /** + * Perform the expand animation if configured, or just show the + * element if not configured or too many animations are in progress + * @method animateExpand + * @param el {HTMLElement} the element to animate + * @param node {YAHOO.util.Node} the node that was expanded + * @return {boolean} true if animation could be invoked, false otherwise + */ + animateExpand: function(el, node) { + + if (this._expandAnim && this._animCount < this.maxAnim) { + // this.locked = true; + var tree = this; + var a = Widget.TVAnim.getAnim(this._expandAnim, el, + function() { tree.expandComplete(node); }); + if (a) { + ++this._animCount; + this.fire("animStart", { + "node": node, + "type": "expand" + }); + a.animate(); + } + + return true; + } + + return false; + }, + + /** + * Perform the collapse animation if configured, or just show the + * element if not configured or too many animations are in progress + * @method animateCollapse + * @param el {HTMLElement} the element to animate + * @param node {YAHOO.util.Node} the node that was expanded + * @return {boolean} true if animation could be invoked, false otherwise + */ + animateCollapse: function(el, node) { + + if (this._collapseAnim && this._animCount < this.maxAnim) { + // this.locked = true; + var tree = this; + var a = Widget.TVAnim.getAnim(this._collapseAnim, el, + function() { tree.collapseComplete(node); }); + if (a) { + ++this._animCount; + this.fire("animStart", { + "node": node, + "type": "collapse" + }); + a.animate(); + } + + return true; + } + + return false; + }, + + /** + * Function executed when the expand animation completes + * @method expandComplete + */ + expandComplete: function(node) { + --this._animCount; + this.fire("animComplete", { + "node": node, + "type": "expand" + }); + // this.locked = false; + }, + + /** + * Function executed when the collapse animation completes + * @method collapseComplete + */ + collapseComplete: function(node) { + --this._animCount; + this.fire("animComplete", { + "node": node, + "type": "collapse" + }); + // this.locked = false; + }, + + /** + * Initializes the tree + * @method init + * @parm {string|HTMLElement} id the id of the element that will hold the tree + * @private + */ + _treeinit: function(id) { + this._el = Y.Selector.query('#' + id, null, true); + this.id = Y.guid(this._el,"yui-tv-auto-id-"); + + /** + * When animation is enabled, this event fires when the animation + * starts + * @event animStart + * @type CustomEvent + * @param {YAHOO.widget.Node} oArgs.node the node that is expanding/collapsing + * @param {String} oArgs.type the type of animation ("expand" or "collapse") + */ + this.publish("animStart", this); + + /** + * When animation is enabled, this event fires when the animation + * completes + * @event animComplete + * @type CustomEvent + * @param {YAHOO.widget.Node} oArgs.node the node that is expanding/collapsing + * @param {String} oArgs.type the type of animation ("expand" or "collapse") + */ + this.publish("animComplete", this); + + /** + * Fires when a node is going to be collapsed. Return false to stop + * the collapse. + * @event collapse + * @type CustomEvent + * @param {YAHOO.widget.Node} node the node that is collapsing + */ + this.publish("collapse", this); + + /** + * Fires after a node is successfully collapsed. This event will not fire + * if the "collapse" event was cancelled. + * @event collapseComplete + * @type CustomEvent + * @param {YAHOO.widget.Node} node the node that was collapsed + */ + this.publish("collapseComplete", this); + + /** + * Fires when a node is going to be expanded. Return false to stop + * the collapse. + * @event expand + * @type CustomEvent + * @param {YAHOO.widget.Node} node the node that is expanding + */ + this.publish("expand", this); + + /** + * Fires after a node is successfully expanded. This event will not fire + * if the "expand" event was cancelled. + * @event expandComplete + * @type CustomEvent + * @param {YAHOO.widget.Node} node the node that was expanded + */ + this.publish("expandComplete", this); + + /** + * Fires when the Enter key is pressed on a node that has the focus + * @event enterKeyPressed + * @type CustomEvent + * @param {YAHOO.widget.Node} node the node that has the focus + */ + this.publish("enterKeyPressed", this); + + /** + * Fires when the label in a TextNode or MenuNode or content in an HTMLNode receives a Click. + * The listener may return false to cancel toggling and focusing on the node. + * @event clickEvent + * @type CustomEvent + * @param oArgs.event {HTMLEvent} The event object + * @param oArgs.node {YAHOO.widget.Node} node the node that was clicked + */ + this.publish("clickEvent", this); + + /** + * Fires when the focus receives the focus, when it changes from a Node + * to another Node or when it is completely lost (blurred) + * @event focusChanged + * @type CustomEvent + * @param oArgs.oldNode {YAHOO.widget.Node} Node that had the focus or null if none + * @param oArgs.newNode {YAHOO.widget.Node} Node that receives the focus or null if none + */ + + this.publish('focusChanged',this); + + /** + * Fires when the label in a TextNode or MenuNode or content in an HTMLNode receives a double Click + * @event dblClickEvent + * @type CustomEvent + * @param oArgs.event {HTMLEvent} The event object + * @param oArgs.node {YAHOO.widget.Node} node the node that was clicked + */ + var self = this; + this.publish("dblClickEvent", { + onSubscribeCallback: function() { + self._hasDblClickSubscriber = true; + } + }); + + /** + * Custom event that is fired when the text node label is clicked. + * The node clicked is provided as an argument + * + * @event labelClick + * @type CustomEvent + * @param {YAHOO.widget.Node} node the node clicked + * @deprecated use clickEvent or dblClickEvent + */ + this.publish("labelClick", this); + + /** + * Custom event fired when the highlight of a node changes. + * The node that triggered the change is provided as an argument: + * The status of the highlight can be checked in + * nodeRef.highlightState. + * Depending on nodeRef.propagateHighlight, other nodes might have changed + * @event highlightEvent + * @type CustomEvent + * @param node {YAHOO.widget.Node} the node that started the change in highlighting state + */ + this.publish("highlightEvent",this); + + + + this._nodes = []; + + // store a global reference + TV.trees[this.id] = this; + + // Set up the root node + this.root = new Widget.RootNode(this); + + + if (this._initEditor) { + this._initEditor(); + } + + }, + + /** + * Builds the TreeView from an object. + * This is the method called by the constructor to build the tree when it has a second argument. + * A tree can be described by an array of objects, each object corresponding to a node. + * Node descriptions may contain values for any property of a node plus the following extra properties:
                          + *
                        • type: can be one of the following:
                            + *
                          • A shortname for a node type ('text','menu','html')
                          • + *
                          • The name of a Node class under YAHOO.widget ('TextNode', 'MenuNode', 'DateNode', etc)
                          • + *
                          • a reference to an actual class: YAHOO.widget.DateNode
                          • + *
                        • + *
                        • children: an array containing further node definitions
                        + * A string instead of an object will produce a node of type 'text' with the given string as its label. + * @method buildTreeFromObject + * @param oConfig {Array|Object|String} array containing a full description of the tree. + * An object or a string will be turned into an array with the given object or string as its only element. + * + */ + buildTreeFromObject: function (oConfig) { + var build = function (parent, oConfig) { + var i, item, node, children, type, NodeType, ThisType; + for (i = 0; i < oConfig.length; i++) { + item = oConfig[i]; + if (Lang.isString(item)) { + node = new Widget.TextNode(item, parent); + } else if (Lang.isObject(item)) { + children = item.children; + delete item.children; + type = item.type || 'text'; + delete item.type; + switch (Lang.isString(type) && type.toLowerCase()) { + case 'text': + node = new Widget.TextNode(item, parent); + break; + case 'menu': + node = new Widget.MenuNode(item, parent); + break; + case 'html': + node = new Widget.HTMLNode(item, parent); + break; + default: + if (Lang.isString(type)) { + NodeType = Widget[type]; + } else { + NodeType = type; + } + if (Lang.isObject(NodeType)) { + for (ThisType = NodeType; ThisType && ThisType !== Widget.Node; ThisType = ThisType.superclass.constructor) {} + if (ThisType) { + node = new NodeType(item, parent); + } else { + } + } else { + } + } + if (children) { + build(node,children); + } + } else { + } + } + }; + if (!Lang.isArray(oConfig)) { + oConfig = [oConfig]; + } + + + build(this.root,oConfig); + }, +/** + * Builds the TreeView from existing markup. Markup should consist of <UL> or <OL> elements containing <LI> elements. + * Each <LI> can have one element used as label and a second optional element which is to be a <UL> or <OL> + * containing nested nodes. + * Depending on what the first element of the <LI> element is, the following Nodes will be created:
                          + *
                        • plain text: a regular TextNode
                        • + *
                        • anchor <A>: a TextNode with its href and target taken from the anchor
                        • + *
                        • anything else: an HTMLNode
                        + * Only the first outermost (un-)ordered list in the markup and its children will be parsed. + * Nodes will be collapsed unless an <LI> tag has a className called 'expanded'. + * All other className attributes will be copied over to the Node className property. + * If the <LI> element contains an attribute called yuiConfig, its contents should be a JSON-encoded object + * as the one used in method buildTreeFromObject. + * @method buildTreeFromMarkup + * @param id {string|HTMLElement} The id of the element that contains the markup or a reference to it. + */ + buildTreeFromMarkup: function (id) { + var build = function (markup) { + var el, child, branch = [], config = {}, label, yuiConfig; + // Dom's getFirstChild and getNextSibling skip over text elements + for (el = Dom.getFirstChild(markup); el; el = Dom.getNextSibling(el)) { + switch (el.tagName.toUpperCase()) { + case 'LI': + label = ''; + config = { + expanded: Dom.hasClass(el,'expanded'), + title: el.title || el.alt || null, + className: Lang.trim(el.className.replace(/\bexpanded\b/,'')) || null + }; + // I cannot skip over text elements here because I want them for labels + child = el.firstChild; + if (child.nodeType == 3) { + // nodes with only whitespace, tabs and new lines don't count, they are probably just formatting. + label = Lang.trim(child.nodeValue.replace(/[\n\t\r]*/g,'')); + if (label) { + config.type = 'text'; + config.label = label; + } else { + child = Dom.getNextSibling(child); + } + } + if (!label) { + if (child.tagName.toUpperCase() == 'A') { + config.type = 'text'; + config.label = child.innerHTML; + config.href = child.href; + config.target = child.target; + config.title = child.title || child.alt || config.title; + } else { + config.type = 'html'; + var d = document.createElement('div'); + d.appendChild(child.cloneNode(true)); + config.html = d.innerHTML; + config.hasIcon = true; + } + } + // see if after the label it has a further list which will become children of this node. + child = Dom.getNextSibling(child); + switch (child && child.tagName.toUpperCase()) { + case 'UL': + case 'OL': + config.children = build(child); + break; + } + // if there are further elements or text, it will be ignored. + + if (YAHOO.lang.JSON) { + yuiConfig = el.getAttribute('yuiConfig'); + if (yuiConfig) { + yuiConfig = YAHOO.lang.JSON.parse(yuiConfig); + config = YAHOO.lang.merge(config,yuiConfig); + } + } + + branch.push(config); + break; + case 'UL': + case 'OL': + config = { + type: 'text', + label: '', + children: build(child) + }; + branch.push(config); + break; + } + } + return branch; + }; + + // var markup = Dom.getChildrenBy(Dom.get(id),function (el) { + // var tag = el.tagName.toUpperCase(); + // return tag == 'UL' || tag == 'OL'; + // }); + + var markup = Y.Selector.query('#' + this.id + '> ul,ol'); + + if (markup.length) { + this.buildTreeFromObject(build(markup[0])); + } else { + } + }, + /** + * Returns the TD element where the event has occurred + * @method _getEventTargetTdEl + * @private + */ + _getEventTargetTdEl: function (ev) { + var target = ev.target, + crit = 'td .ygtvrow', + targetEl, pnode = target.get('parentNode'); + + if (target && pnode && !pnode.hasClass('ygtvrow')) { + target = target.ancestor('td'); + pnode = target && target.get('parentNode'); + } + + if (!target) { + return null; + } + + targetEl = target._node || target; + + // If it is a spacer cell, do nothing + if (/\bygtv(blank)?depthcell/.test(targetEl.className)) { + return null; + } + + // If it has an id, search for the node number and see if it belongs to a node in this tree. + if (targetEl.id) { + var m = targetEl.id.match(/\bygtv([^\d]*)(.*)/); + if (m && m[2] && this._nodes[m[2]]) { + return targetEl; + } + } + + return null; + }, + /** + *.DOM, Event listener for click events + * @method _onClickEvent + * @private + */ + _onClickEvent: function (ev) { + var self = this, + td = this._getEventTargetTdEl(ev), + node, + target, + toggle = function (force) { + node.focus(); + if (force || !node.href) { + node.toggle(); + try { + ev.preventDefault(); + } catch (e) { + // @TODO + // For some reason IE8 is providing an event object with + // most of the fields missing, but only when clicking on + // the node's label, and only when working with inline + // editing. This generates a "Member not found" error + // in that browser. Determine if this is a browser + // bug, or a problem with this code. Already checked to + // see if the problem has to do with access the event + // in the outer scope, and that isn't the problem. + // Maybe the markup for inline editing is broken. + } + } + }; + + if (!td) { + return; + } + + node = this.getNodeByElement(td); + if (!node) { + return; + } + + // exception to handle deprecated event labelClick + // @TODO take another look at this deprecation. It is common for people to + // only be interested in the label click, so why make them have to test + // the node type to figure out whether the click was on the label? + + // target = Event.getTarget(ev); + target = ev.target; + + // if (Dom.hasClass(target, node.labelStyle) || Dom.getAncestorByClassName(target,node.labelStyle)) { + if (target.hasClass(node.labelStyle) || target.ancestor(node.labelStyle)) { + this.fire('labelClick', node); + } + + // If it is a toggle cell, toggle + if (/\bygtv[tl][mp]h?h?/.test(td.className)) { + toggle(true); + } else { + if (this._dblClickTimer) { + window.clearTimeout(this._dblClickTimer); + this._dblClickTimer = null; + } else { + if (this._hasDblClickSubscriber) { + this._dblClickTimer = window.setTimeout(function () { + self._dblClickTimer = null; + if (self.fire('clickEvent', {event:ev,node:node}) !== false) { + toggle(); + } + }, 200); + } else { + if (self.fire('clickEvent', {event:ev,node:node}) !== false) { + toggle(); + } + } + } + } + }, + + /** + * Event listener for double-click events + * @method _onDblClickEvent + * @private + */ + _onDblClickEvent: function (ev) { + if (!this._hasDblClickSubscriber) { + return; + } + + var td = this._getEventTargetTdEl(ev); + if (!td) { + return; + } + + if (!(/\bygtv[tl][mp]h?h?/.test(td.className))) { + + this.fire('dblClickEvent', { + event: ev, + node: this.getNodeByElement(td) + }); + + if (this._dblClickTimer) { + window.clearTimeout(this._dblClickTimer); + this._dblClickTimer = null; + } + } + }, + /** + * Event listener for mouse over events + * @method _onMouseOverEvent + * @private + */ + _onMouseOverEvent:function (ev) { + var target; + if ((target = this._getEventTargetTdEl(ev)) && (target = this.getNodeByElement(target)) && (target = target.getToggleEl())) { + target.className = target.className.replace(/\bygtv([lt])([mp])\b/gi,'ygtv$1$2h'); + } + }, + /** + * Event listener for mouse out events + * @method _onMouseOutEvent + * @private + */ + _onMouseOutEvent: function (ev) { + var target; + if ((target = this._getEventTargetTdEl(ev)) && (target = this.getNodeByElement(target)) && (target = target.getToggleEl())) { + target.className = target.className.replace(/\bygtv([lt])([mp])h\b/gi,'ygtv$1$2'); + } + }, + /** + * Event listener for key down events + * @method _onKeyDownEvent + * @private + */ + _onKeyDownEvent: function (ev) { + var target = ev.target, + targetEl = target._node, + node = this.getNodeByElement(targetEl), + newNode = node; + + switch(ev.keyCode) { + case KEY.UP: + do { + if (newNode.previousSibling) { + newNode = newNode.previousSibling; + } else { + newNode = newNode.parent; + } + } while (newNode && !newNode._canHaveFocus()); + if (newNode) { newNode.focus(); } + ev.preventDefault(); + break; + case KEY.DOWN: + do { + if (newNode.nextSibling) { + newNode = newNode.nextSibling; + } else { + newNode.expand(); + newNode = (newNode.children.length || null) && newNode.children[0]; + } + } while (newNode && !newNode._canHaveFocus); + if (newNode) { newNode.focus();} + ev.preventDefault(); + break; + case KEY.LEFT: + do { + if (newNode.parent) { + newNode = newNode.parent; + } else { + newNode = newNode.previousSibling; + } + } while (newNode && !newNode._canHaveFocus()); + if (newNode) { newNode.focus();} + ev.preventDefault(); + break; + case KEY.RIGHT: + var self = this, + moveFocusRight, + focusOnExpand = function (newNode) { + self.unsubscribe('expandComplete',focusOnExpand); + moveFocusRight(newNode); + }; + moveFocusRight = function (newNode) { + do { + if (newNode.isDynamic() && !newNode.childrenRendered) { + self.subscribe('expandComplete',focusOnExpand); + newNode.expand(); + newNode = null; + break; + } else { + newNode.expand(); + if (newNode.children.length) { + newNode = newNode.children[0]; + } else { + newNode = newNode.nextSibling; + } + } + } while (newNode && !newNode._canHaveFocus()); + if (newNode) { newNode.focus();} + }; + + moveFocusRight(newNode); + ev.preventDefault(); + break; + case KEY.ENTER: + if (node.href) { + if (node.target) { + window.open(node.href,node.target); + } else { + window.location(node.href); + } + } else { + node.toggle(); + } + this.fire('enterKeyPressed',node); + ev.preventDefault(); + break; + case KEY.HOME: + newNode = this.getRoot(); + if (newNode.children.length) {newNode = newNode.children[0];} + if (newNode._canHaveFocus()) { newNode.focus(); } + ev.preventDefault(); + break; + case KEY.END: + newNode = newNode.parent.children; + newNode = newNode[newNode.length -1]; + if (newNode._canHaveFocus()) { newNode.focus(); } + ev.preventDefault(); + break; + // case KEY.PAGE_UP: + // break; + // case KEY.PAGE_DOWN: + // break; + case 107: // plus key + if (ev.shiftKey) { + node.parent.expandAll(); + } else { + node.expand(); + } + break; + case 109: // minus key + if (ev.shiftKey) { + node.parent.collapseAll(); + } else { + node.collapse(); + } + break; + default: + break; + } + }, + /** + * Renders the tree boilerplate and visible nodes + * @method render + */ + render: function() { + var html = this.root.getHtml(), + el = this.getEl(); + el.innerHTML = html; + if (!this._hasEvents) { + Y.on('click', this._onClickEvent, el, this); + Y.on('dblclick', this._onDblClickEvent, el, this, true); + Y.on('mouseover', this._onMouseOverEvent, el, this, true); + Y.on('mouseout', this._onMouseOutEvent, el, this, true); + Y.on('keydown', this._onKeyDownEvent, el, this, true); + } + this._hasEvents = true; + }, + + /** + * Returns the tree's host element + * @method getEl + * @return {HTMLElement} the host element + */ + getEl: function() { + if (! this._el) { + var el = Y.Selector.query('#' + this.id, null, true); + this._el = el; + + } + return this._el; + }, + + /** + * Nodes register themselves with the tree instance when they are created. + * @method regNode + * @param node {Node} the node to register + * @private + */ + regNode: function(node) { + this._nodes[node.index] = node; + }, + + /** + * Returns the root node of this tree + * @method getRoot + * @return {Node} the root node + */ + getRoot: function() { + return this.root; + }, + + /** + * Configures this tree to dynamically load all child data + * @method setDynamicLoad + * @param {function} fnDataLoader the function that will be called to get the data + * @param iconMode {int} configures the icon that is displayed when a dynamic + * load node is expanded the first time without children. By default, the + * "collapse" icon will be used. If set to 1, the leaf node icon will be + * displayed. + */ + setDynamicLoad: function(fnDataLoader, iconMode) { + this.root.setDynamicLoad(fnDataLoader, iconMode); + }, + + /** + * Expands all child nodes. Note: this conflicts with the "multiExpand" + * node property. If expand all is called in a tree with nodes that + * do not allow multiple siblings to be displayed, only the last sibling + * will be expanded. + * @method expandAll + */ + expandAll: function() { + if (!this.locked) { + this.root.expandAll(); + } + }, + + /** + * Collapses all expanded child nodes in the entire tree. + * @method collapseAll + */ + collapseAll: function() { + if (!this.locked) { + this.root.collapseAll(); + } + }, + + /** + * Returns a node in the tree that has the specified index (this index + * is created internally, so this function probably will only be used + * in html generated for a given node.) + * @method getNodeByIndex + * @param {int} nodeIndex the index of the node wanted + * @return {Node} the node with index=nodeIndex, null if no match + */ + getNodeByIndex: function(nodeIndex) { + var n = this._nodes[nodeIndex]; + return (n) ? n : null; + }, + + /** + * Returns a node that has a matching property and value in the data + * object that was passed into its constructor. + * @method getNodeByProperty + * @param {object} property the property to search (usually a string) + * @param {object} value the value we want to find (usuall an int or string) + * @return {Node} the matching node, null if no match + */ + getNodeByProperty: function(property, value) { + for (var i in this._nodes) { + if (this._nodes.hasOwnProperty(i)) { + var n = this._nodes[i]; + if ((property in n && n[property] == value) || (n.data && value == n.data[property])) { + return n; + } + } + } + + return null; + }, + + /** + * Returns a collection of nodes that have a matching property + * and value in the data object that was passed into its constructor. + * @method getNodesByProperty + * @param {object} property the property to search (usually a string) + * @param {object} value the value we want to find (usuall an int or string) + * @return {Array} the matching collection of nodes, null if no match + */ + getNodesByProperty: function(property, value) { + var values = []; + for (var i in this._nodes) { + if (this._nodes.hasOwnProperty(i)) { + var n = this._nodes[i]; + if ((property in n && n[property] == value) || (n.data && value == n.data[property])) { + values.push(n); + } + } + } + + return (values.length) ? values : null; + }, + + + /** + * Returns a collection of nodes that have passed the test function + * passed as its only argument. + * The function will receive a reference to each node to be tested. + * @method getNodesBy + * @param {function} a boolean function that receives a Node instance and returns true to add the node to the results list + * @return {Array} the matching collection of nodes, null if no match + */ + getNodesBy: function(fn) { + var values = []; + for (var i in this._nodes) { + if (this._nodes.hasOwnProperty(i)) { + var n = this._nodes[i]; + if (fn(n)) { + values.push(n); + } + } + } + return (values.length) ? values : null; + }, + /** + * Returns the treeview node reference for an ancestor element + * of the node, or null if it is not contained within any node + * in this tree. + * @method getNodeByElement + * @param el {HTMLElement} the element to test + * @return {YAHOO.widget.Node} a node reference or null + */ + getNodeByElement: function(el) { + + var p=el, m, re=/ygtv([^\d]*)(.*)/; + + do { + + if (p && p.id) { + m = p.id.match(re); + if (m && m[2]) { + return this.getNodeByIndex(m[2]); + } + } + + p = p.parentNode; + + if (!p || !p.tagName) { + break; + } + + } + while (p.id !== this.id && p.tagName.toLowerCase() !== "body"); + + return null; + }, + + /** + * When in singleNodeHighlight it returns the node highlighted + * or null if none. Returns null if singleNodeHighlight is false. + * @method getHighlightedNode + * @return {YAHOO.widget.Node} a node reference or null + */ + getHighlightedNode: function() { + return this._currentlyHighlighted; + }, + + + /** + * Removes the node and its children, and optionally refreshes the + * branch of the tree that was affected. + * @method removeNode + * @param {Node} node to remove + * @param {boolean} autoRefresh automatically refreshes branch if true + * @return {boolean} False is there was a problem, true otherwise. + */ + removeNode: function(node, autoRefresh) { + + // Don't delete the root node + if (node.isRoot()) { + return false; + } + + // Get the branch that we may need to refresh + var p = node.parent; + if (p.parent) { + p = p.parent; + } + + // Delete the node and its children + this._deleteNode(node); + + // Refresh the parent of the parent + if (autoRefresh && p && p.childrenRendered) { + p.refresh(); + } + + return true; + }, + + /** + * wait until the animation is complete before deleting + * to avoid javascript errors + * @method _removeChildren_animComplete + * @param o the custom event payload + * @private + */ + _removeChildren_animComplete: function(o) { + this.unsubscribe(this._removeChildren_animComplete); + this.removeChildren(o.node); + }, + + /** + * Deletes this nodes child collection, recursively. Also collapses + * the node, and resets the dynamic load flag. The primary use for + * this method is to purge a node and allow it to fetch its data + * dynamically again. + * @method removeChildren + * @param {Node} node the node to purge + */ + removeChildren: function(node) { + + if (node.expanded) { + // wait until the animation is complete before deleting to + // avoid javascript errors + if (this._collapseAnim) { + this.subscribe("animComplete", + this._removeChildren_animComplete, this, true); + Widget.Node.prototype.collapse.call(node); + return; + } + + node.collapse(); + } + + while (node.children.length) { + this._deleteNode(node.children[0]); + } + + if (node.isRoot()) { + Widget.Node.prototype.expand.call(node); + } + + node.childrenRendered = false; + node.dynamicLoadComplete = false; + + node.updateIcon(); + }, + + /** + * Deletes the node and recurses children + * @method _deleteNode + * @private + */ + _deleteNode: function(node) { + // Remove all the child nodes first + this.removeChildren(node); + + // Remove the node from the tree + this.popNode(node); + }, + + /** + * Removes the node from the tree, preserving the child collection + * to make it possible to insert the branch into another part of the + * tree, or another tree. + * @method popNode + * @param {Node} node to remove + */ + popNode: function(node) { + var p = node.parent; + + // Update the parent's collection of children + var a = []; + + for (var i=0, len=p.children.length;i