1 /*
  2 Copyright (c) 2003-2010, CKSource - Frederico Knabben. All rights reserved.
  3 For licensing, see LICENSE.html or http://ckeditor.com/license
  4 */
  5 
  6 CKEDITOR.plugins.add( 'richcombo',
  7 {
  8 	requires : [ 'floatpanel', 'listblock', 'button' ],
  9 
 10 	beforeInit : function( editor )
 11 	{
 12 		editor.ui.addHandler( CKEDITOR.UI_RICHCOMBO, CKEDITOR.ui.richCombo.handler );
 13 	}
 14 });
 15 
 16 /**
 17  * Button UI element.
 18  * @constant
 19  * @example
 20  */
 21 CKEDITOR.UI_RICHCOMBO = 3;
 22 
 23 CKEDITOR.ui.richCombo = CKEDITOR.tools.createClass(
 24 {
 25 	$ : function( definition )
 26 	{
 27 		// Copy all definition properties to this object.
 28 		CKEDITOR.tools.extend( this, definition,
 29 			// Set defaults.
 30 			{
 31 				title : definition.label,
 32 				modes : { wysiwyg : 1 }
 33 			});
 34 
 35 		// We don't want the panel definition in this object.
 36 		var panelDefinition = this.panel || {};
 37 		delete this.panel;
 38 
 39 		this.id = CKEDITOR.tools.getNextNumber();
 40 
 41 		this.document = ( panelDefinition
 42 							&& panelDefinition.parent
 43 							&& panelDefinition.parent.getDocument() )
 44 						|| CKEDITOR.document;
 45 
 46 		panelDefinition.className = ( panelDefinition.className || '' ) + ' cke_rcombopanel';
 47 		panelDefinition.block =
 48 		{
 49 			multiSelect : panelDefinition.multiSelect,
 50 			attributes : panelDefinition.attributes
 51 		};
 52 
 53 		this._ =
 54 		{
 55 			panelDefinition : panelDefinition,
 56 			items : {},
 57 			state : CKEDITOR.TRISTATE_OFF
 58 		};
 59 	},
 60 
 61 	statics :
 62 	{
 63 		handler :
 64 		{
 65 			create : function( definition )
 66 			{
 67 				return new CKEDITOR.ui.richCombo( definition );
 68 			}
 69 		}
 70 	},
 71 
 72 	proto :
 73 	{
 74 		renderHtml : function( editor )
 75 		{
 76 			var output = [];
 77 			this.render( editor, output );
 78 			return output.join( '' );
 79 		},
 80 
 81 		/**
 82 		 * Renders the combo.
 83 		 * @param {CKEDITOR.editor} editor The editor instance which this button is
 84 		 *		to be used by.
 85 		 * @param {Array} output The output array to which append the HTML relative
 86 		 *		to this button.
 87 		 * @example
 88 		 */
 89 		render : function( editor, output )
 90 		{
 91 			var env = CKEDITOR.env;
 92 
 93 			var id = 'cke_' + this.id;
 94 			var clickFn = CKEDITOR.tools.addFunction( function( $element )
 95 				{
 96 					var _ = this._;
 97 
 98 					if ( _.state == CKEDITOR.TRISTATE_DISABLED )
 99 						return;
100 
101 					this.createPanel( editor );
102 
103 					if ( _.on )
104 					{
105 						_.panel.hide();
106 						return;
107 					}
108 
109 					if ( !_.committed )
110 					{
111 						_.list.commit();
112 						_.committed = 1;
113 					}
114 
115 					var value = this.getValue();
116 					if ( value )
117 						_.list.mark( value );
118 					else
119 						_.list.unmarkAll();
120 
121 					_.panel.showBlock( this.id, new CKEDITOR.dom.element( $element ), 4 );
122 				},
123 				this );
124 
125 			var instance = {
126 				id : id,
127 				combo : this,
128 				focus : function()
129 				{
130 					var element = CKEDITOR.document.getById( id ).getChild( 1 );
131 					element.focus();
132 				},
133 				clickFn : clickFn
134 			};
135 
136 			editor.on( 'mode', function()
137 				{
138 					this.setState( this.modes[ editor.mode ] ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );
139 				},
140 				this );
141 
142 			var keyDownFn = CKEDITOR.tools.addFunction( function( ev, element )
143 				{
144 					ev = new CKEDITOR.dom.event( ev );
145 
146 					var keystroke = ev.getKeystroke();
147 					switch ( keystroke )
148 					{
149 						case 13 :	// ENTER
150 						case 32 :	// SPACE
151 						case 40 :	// ARROW-DOWN
152 							// Show panel
153 							CKEDITOR.tools.callFunction( clickFn, element );
154 							break;
155 						default :
156 							// Delegate the default behavior to toolbar button key handling.
157 							instance.onkey( instance,  keystroke );
158 					}
159 
160 					// Avoid subsequent focus grab on editor document.
161 					ev.preventDefault();
162 				});
163 
164 			// For clean up
165 			instance.keyDownFn = keyDownFn;
166 
167 			output.push(
168 				'<span class="cke_rcombo">',
169 				'<span id=', id );
170 
171 			if ( this.className )
172 				output.push( ' class="', this.className, ' cke_off"');
173 
174 			output.push(
175 				'>',
176 					'<span id="' + id+ '_label" class=cke_label>', this.label, '</span>',
177 					'<a hidefocus=true title="', this.title, '" tabindex="-1"',
178 						env.gecko && env.version >= 10900 && !env.hc ? '' : ' href="javascript:void(\'' + this.label + '\')"',
179 						' role="button" aria-labelledby="', id , '_label" aria-describedby="', id, '_text" aria-haspopup="true"' );
180 
181 			// Some browsers don't cancel key events in the keydown but in the
182 			// keypress.
183 			// TODO: Check if really needed for Gecko+Mac.
184 			if ( CKEDITOR.env.opera || ( CKEDITOR.env.gecko && CKEDITOR.env.mac ) )
185 			{
186 				output.push(
187 					' onkeypress="return false;"' );
188 			}
189 
190 			// With Firefox, we need to force it to redraw, otherwise it
191 			// will remain in the focus state.
192 			if ( CKEDITOR.env.gecko )
193 			{
194 				output.push(
195 					' onblur="this.style.cssText = this.style.cssText;"' );
196 			}
197 
198 			output.push(
199 					' onkeydown="CKEDITOR.tools.callFunction( ', keyDownFn, ', event, this );"' +
200 					' onclick="CKEDITOR.tools.callFunction(', clickFn, ', this); return false;">' +
201 						'<span>' +
202 							'<span id="' + id + '_text" class="cke_text cke_inline_label">' + this.label + '</span>' +
203 						'</span>' +
204 						'<span class=cke_openbutton>' + ( CKEDITOR.env.hc ? '<span>▼</span>' : '' ) + '</span>' +	// BLACK DOWN-POINTING TRIANGLE
205 					'</a>' +
206 				'</span>' +
207 				'</span>' );
208 
209 			if ( this.onRender )
210 				this.onRender();
211 
212 			return instance;
213 		},
214 
215 		createPanel : function( editor )
216 		{
217 			if ( this._.panel )
218 				return;
219 
220 			var panelDefinition = this._.panelDefinition,
221 				panelBlockDefinition = this._.panelDefinition.block,
222 				panelParentElement = panelDefinition.parent || CKEDITOR.document.getBody(),
223 				panel = new CKEDITOR.ui.floatPanel( editor, panelParentElement, panelDefinition ),
224 				list = panel.addListBlock( this.id, panelBlockDefinition ),
225 				me = this;
226 
227 			panel.onShow = function()
228 				{
229 					if ( me.className )
230 						this.element.getFirst().addClass( me.className + '_panel' );
231 
232 					me.setState( CKEDITOR.TRISTATE_ON );
233 
234 					list.focus( !me.multiSelect && me.getValue() );
235 
236 					me._.on = 1;
237 
238 					if ( me.onOpen )
239 						me.onOpen();
240 				};
241 
242 			panel.onHide = function()
243 				{
244 					if ( me.className )
245 						this.element.getFirst().removeClass( me.className + '_panel' );
246 
247 					me.setState( CKEDITOR.TRISTATE_OFF );
248 
249 					me._.on = 0;
250 
251 					if ( me.onClose )
252 						me.onClose();
253 				};
254 
255 			panel.onEscape = function()
256 				{
257 					panel.hide();
258 					me.document.getById( 'cke_' + me.id ).getFirst().getNext().focus();
259 				};
260 
261 			list.onClick = function( value, marked )
262 				{
263 					// Move the focus to the main windows, otherwise it will stay
264 					// into the floating panel, even if invisible, and Safari and
265 					// Opera will go a bit crazy.
266 					me.document.getWindow().focus();
267 
268 					if ( me.onClick )
269 						me.onClick.call( me, value, marked );
270 
271 					if ( marked )
272 						me.setValue( value, me._.items[ value ] );
273 					else
274 						me.setValue( '' );
275 
276 					panel.hide();
277 				};
278 
279 			this._.panel = panel;
280 			this._.list = list;
281 
282 			panel.getBlock( this.id ).onHide = function()
283 				{
284 					me._.on = 0;
285 					me.setState( CKEDITOR.TRISTATE_OFF );
286 				};
287 
288 			if ( this.init )
289 				this.init();
290 		},
291 
292 		setValue : function( value, text )
293 		{
294 			this._.value = value;
295 
296 			var textElement = this.document.getById( 'cke_' + this.id + '_text' );
297 
298 			if ( !( value || text ) )
299 			{
300 				text = this.label;
301 				textElement.addClass( 'cke_inline_label' );
302 			}
303 			else
304 				textElement.removeClass( 'cke_inline_label' );
305 
306 			textElement.setHtml( typeof text != 'undefined' ? text : value );
307 		},
308 
309 		getValue : function()
310 		{
311 			return this._.value || '';
312 		},
313 
314 		unmarkAll : function()
315 		{
316 			this._.list.unmarkAll();
317 		},
318 
319 		mark : function( value )
320 		{
321 			this._.list.mark( value );
322 		},
323 
324 		hideItem : function( value )
325 		{
326 			this._.list.hideItem( value );
327 		},
328 
329 		hideGroup : function( groupTitle )
330 		{
331 			this._.list.hideGroup( groupTitle );
332 		},
333 
334 		showAll : function()
335 		{
336 			this._.list.showAll();
337 		},
338 
339 		add : function( value, html, text )
340 		{
341 			this._.items[ value ] = text || value;
342 			this._.list.add( value, html, text );
343 		},
344 
345 		startGroup : function( title )
346 		{
347 			this._.list.startGroup( title );
348 		},
349 
350 		commit : function()
351 		{
352 			this._.list.commit();
353 		},
354 
355 		setState : function( state )
356 		{
357 			if ( this._.state == state )
358 				return;
359 
360 			this.document.getById( 'cke_' + this.id ).setState( state );
361 
362 			this._.state = state;
363 		}
364 	}
365 });
366 
367 CKEDITOR.ui.prototype.addRichCombo = function( name, definition )
368 {
369 	this.add( name, CKEDITOR.UI_RICHCOMBO, definition );
370 };
371