d24a80fc512164ee7e3483b8f644dcedbc9e2a18
[ckeditor.git] / _source / plugins / dialog / plugin.js
1 /*
2 Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
3 For licensing, see LICENSE.html or http://ckeditor.com/license
4 */
5
6 /**
7 * @fileOverview The floating dialog plugin.
8 */
9
10 /**
11 * No resize for this dialog.
12 * @constant
13 */
14 CKEDITOR.DIALOG_RESIZE_NONE = 0;
15
16 /**
17 * Only allow horizontal resizing for this dialog, disable vertical resizing.
18 * @constant
19 */
20 CKEDITOR.DIALOG_RESIZE_WIDTH = 1;
21
22 /**
23 * Only allow vertical resizing for this dialog, disable horizontal resizing.
24 * @constant
25 */
26 CKEDITOR.DIALOG_RESIZE_HEIGHT = 2;
27
28 /*
29 * Allow the dialog to be resized in both directions.
30 * @constant
31 */
32 CKEDITOR.DIALOG_RESIZE_BOTH = 3;
33
34 (function()
35 {
36 var cssLength = CKEDITOR.tools.cssLength;
37 function isTabVisible( tabId )
38 {
39 return !!this._.tabs[ tabId ][ 0 ].$.offsetHeight;
40 }
41
42 function getPreviousVisibleTab()
43 {
44 var tabId = this._.currentTabId,
45 length = this._.tabIdList.length,
46 tabIndex = CKEDITOR.tools.indexOf( this._.tabIdList, tabId ) + length;
47
48 for ( var i = tabIndex - 1 ; i > tabIndex - length ; i-- )
49 {
50 if ( isTabVisible.call( this, this._.tabIdList[ i % length ] ) )
51 return this._.tabIdList[ i % length ];
52 }
53
54 return null;
55 }
56
57 function getNextVisibleTab()
58 {
59 var tabId = this._.currentTabId,
60 length = this._.tabIdList.length,
61 tabIndex = CKEDITOR.tools.indexOf( this._.tabIdList, tabId );
62
63 for ( var i = tabIndex + 1 ; i < tabIndex + length ; i++ )
64 {
65 if ( isTabVisible.call( this, this._.tabIdList[ i % length ] ) )
66 return this._.tabIdList[ i % length ];
67 }
68
69 return null;
70 }
71
72
73 function clearOrRecoverTextInputValue( container, isRecover )
74 {
75 var inputs = container.$.getElementsByTagName( 'input' );
76 for ( var i = 0, length = inputs.length; i < length ; i++ )
77 {
78 var item = new CKEDITOR.dom.element( inputs[ i ] );
79
80 if ( item.getAttribute( 'type' ).toLowerCase() == 'text' )
81 {
82 if ( isRecover )
83 {
84 item.setAttribute( 'value', item.getCustomData( 'fake_value' ) || '' );
85 item.removeCustomData( 'fake_value' );
86 }
87 else
88 {
89 item.setCustomData( 'fake_value', item.getAttribute( 'value' ) );
90 item.setAttribute( 'value', '' );
91 }
92 }
93 }
94 }
95
96 // Handle dialog element validation state UI changes.
97 function handleFieldValidated( isValid, msg )
98 {
99 var input = this.getInputElement();
100 if ( input )
101 {
102 isValid ? input.removeAttribute( 'aria-invalid' )
103 : input.setAttribute( 'aria-invalid', true );
104 }
105
106 if ( !isValid )
107 {
108 if ( this.select )
109 this.select();
110 else
111 this.focus();
112 }
113
114 msg && alert( msg );
115
116 this.fire( 'validated', { valid : isValid, msg : msg } );
117 }
118
119 function resetField()
120 {
121 var input = this.getInputElement();
122 input && input.removeAttribute( 'aria-invalid' );
123 }
124
125
126 /**
127 * This is the base class for runtime dialog objects. An instance of this
128 * class represents a single named dialog for a single editor instance.
129 * @param {Object} editor The editor which created the dialog.
130 * @param {String} dialogName The dialog's registered name.
131 * @constructor
132 * @example
133 * var dialogObj = new CKEDITOR.dialog( editor, 'smiley' );
134 */
135 CKEDITOR.dialog = function( editor, dialogName )
136 {
137 // Load the dialog definition.
138 var definition = CKEDITOR.dialog._.dialogDefinitions[ dialogName ],
139 defaultDefinition = CKEDITOR.tools.clone( defaultDialogDefinition ),
140 buttonsOrder = editor.config.dialog_buttonsOrder || 'OS',
141 dir = editor.lang.dir;
142
143 if ( ( buttonsOrder == 'OS' && CKEDITOR.env.mac ) || // The buttons in MacOS Apps are in reverse order (#4750)
144 ( buttonsOrder == 'rtl' && dir == 'ltr' ) ||
145 ( buttonsOrder == 'ltr' && dir == 'rtl' ) )
146 defaultDefinition.buttons.reverse();
147
148
149 // Completes the definition with the default values.
150 definition = CKEDITOR.tools.extend( definition( editor ), defaultDefinition );
151
152 // Clone a functionally independent copy for this dialog.
153 definition = CKEDITOR.tools.clone( definition );
154
155 // Create a complex definition object, extending it with the API
156 // functions.
157 definition = new definitionObject( this, definition );
158
159 var doc = CKEDITOR.document;
160
161 var themeBuilt = editor.theme.buildDialog( editor );
162
163 // Initialize some basic parameters.
164 this._ =
165 {
166 editor : editor,
167 element : themeBuilt.element,
168 name : dialogName,
169 contentSize : { width : 0, height : 0 },
170 size : { width : 0, height : 0 },
171 contents : {},
172 buttons : {},
173 accessKeyMap : {},
174
175 // Initialize the tab and page map.
176 tabs : {},
177 tabIdList : [],
178 currentTabId : null,
179 currentTabIndex : null,
180 pageCount : 0,
181 lastTab : null,
182 tabBarMode : false,
183
184 // Initialize the tab order array for input widgets.
185 focusList : [],
186 currentFocusIndex : 0,
187 hasFocus : false
188 };
189
190 this.parts = themeBuilt.parts;
191
192 CKEDITOR.tools.setTimeout( function()
193 {
194 editor.fire( 'ariaWidget', this.parts.contents );
195 },
196 0, this );
197
198 // Set the startup styles for the dialog, avoiding it enlarging the
199 // page size on the dialog creation.
200 var startStyles = {
201 position : CKEDITOR.env.ie6Compat ? 'absolute' : 'fixed',
202 top : 0,
203 visibility : 'hidden'
204 };
205
206 startStyles[ dir == 'rtl' ? 'right' : 'left' ] = 0;
207 this.parts.dialog.setStyles( startStyles );
208
209
210 // Call the CKEDITOR.event constructor to initialize this instance.
211 CKEDITOR.event.call( this );
212
213 // Fire the "dialogDefinition" event, making it possible to customize
214 // the dialog definition.
215 this.definition = definition = CKEDITOR.fire( 'dialogDefinition',
216 {
217 name : dialogName,
218 definition : definition
219 }
220 , editor ).definition;
221
222 var tabsToRemove = {};
223 // Cache tabs that should be removed.
224 if ( !( 'removeDialogTabs' in editor._ ) && editor.config.removeDialogTabs )
225 {
226 var removeContents = editor.config.removeDialogTabs.split( ';' );
227
228 for ( i = 0; i < removeContents.length; i++ )
229 {
230 var parts = removeContents[ i ].split( ':' );
231 if ( parts.length == 2 )
232 {
233 var removeDialogName = parts[ 0 ];
234 if ( !tabsToRemove[ removeDialogName ] )
235 tabsToRemove[ removeDialogName ] = [];
236 tabsToRemove[ removeDialogName ].push( parts[ 1 ] );
237 }
238 }
239 editor._.removeDialogTabs = tabsToRemove;
240 }
241
242 // Remove tabs of this dialog.
243 if ( editor._.removeDialogTabs && ( tabsToRemove = editor._.removeDialogTabs[ dialogName ] ) )
244 {
245 for ( i = 0; i < tabsToRemove.length; i++ )
246 definition.removeContents( tabsToRemove[ i ] );
247 }
248
249 // Initialize load, show, hide, ok and cancel events.
250 if ( definition.onLoad )
251 this.on( 'load', definition.onLoad );
252
253 if ( definition.onShow )
254 this.on( 'show', definition.onShow );
255
256 if ( definition.onHide )
257 this.on( 'hide', definition.onHide );
258
259 if ( definition.onOk )
260 {
261 this.on( 'ok', function( evt )
262 {
263 // Dialog confirm might probably introduce content changes (#5415).
264 editor.fire( 'saveSnapshot' );
265 setTimeout( function () { editor.fire( 'saveSnapshot' ); }, 0 );
266 if ( definition.onOk.call( this, evt ) === false )
267 evt.data.hide = false;
268 });
269 }
270
271 if ( definition.onCancel )
272 {
273 this.on( 'cancel', function( evt )
274 {
275 if ( definition.onCancel.call( this, evt ) === false )
276 evt.data.hide = false;
277 });
278 }
279
280 var me = this;
281
282 // Iterates over all items inside all content in the dialog, calling a
283 // function for each of them.
284 var iterContents = function( func )
285 {
286 var contents = me._.contents,
287 stop = false;
288
289 for ( var i in contents )
290 {
291 for ( var j in contents[i] )
292 {
293 stop = func.call( this, contents[i][j] );
294 if ( stop )
295 return;
296 }
297 }
298 };
299
300 this.on( 'ok', function( evt )
301 {
302 iterContents( function( item )
303 {
304 if ( item.validate )
305 {
306 var retval = item.validate( this ),
307 invalid = typeof ( retval ) == 'string' || retval === false;
308
309 if ( invalid )
310 {
311 evt.data.hide = false;
312 evt.stop();
313 }
314
315 handleFieldValidated.call( item, !invalid, typeof retval == 'string' ? retval : undefined );
316 return invalid;
317 }
318 });
319 }, this, null, 0 );
320
321 this.on( 'cancel', function( evt )
322 {
323 iterContents( function( item )
324 {
325 if ( item.isChanged() )
326 {
327 if ( !confirm( editor.lang.common.confirmCancel ) )
328 evt.data.hide = false;
329 return true;
330 }
331 });
332 }, this, null, 0 );
333
334 this.parts.close.on( 'click', function( evt )
335 {
336 if ( this.fire( 'cancel', { hide : true } ).hide !== false )
337 this.hide();
338 evt.data.preventDefault();
339 }, this );
340
341 // Sort focus list according to tab order definitions.
342 function setupFocus()
343 {
344 var focusList = me._.focusList;
345 focusList.sort( function( a, b )
346 {
347 // Mimics browser tab order logics;
348 if ( a.tabIndex != b.tabIndex )
349 return b.tabIndex - a.tabIndex;
350 // Sort is not stable in some browsers,
351 // fall-back the comparator to 'focusIndex';
352 else
353 return a.focusIndex - b.focusIndex;
354 });
355
356 var size = focusList.length;
357 for ( var i = 0; i < size; i++ )
358 focusList[ i ].focusIndex = i;
359 }
360
361 function changeFocus( forward )
362 {
363 var focusList = me._.focusList,
364 offset = forward ? 1 : -1;
365 if ( focusList.length < 1 )
366 return;
367
368 var current = me._.currentFocusIndex;
369
370 // Trigger the 'blur' event of any input element before anything,
371 // since certain UI updates may depend on it.
372 try
373 {
374 focusList[ current ].getInputElement().$.blur();
375 }
376 catch( e ){}
377
378 var startIndex = ( current + offset + focusList.length ) % focusList.length,
379 currentIndex = startIndex;
380 while ( !focusList[ currentIndex ].isFocusable() )
381 {
382 currentIndex = ( currentIndex + offset + focusList.length ) % focusList.length;
383 if ( currentIndex == startIndex )
384 break;
385 }
386 focusList[ currentIndex ].focus();
387
388 // Select whole field content.
389 if ( focusList[ currentIndex ].type == 'text' )
390 focusList[ currentIndex ].select();
391 }
392
393 this.changeFocus = changeFocus;
394
395 var processed;
396
397 function focusKeydownHandler( evt )
398 {
399 // If I'm not the top dialog, ignore.
400 if ( me != CKEDITOR.dialog._.currentTop )
401 return;
402
403 var keystroke = evt.data.getKeystroke(),
404 rtl = editor.lang.dir == 'rtl';
405
406 processed = 0;
407 if ( keystroke == 9 || keystroke == CKEDITOR.SHIFT + 9 )
408 {
409 var shiftPressed = ( keystroke == CKEDITOR.SHIFT + 9 );
410
411 // Handling Tab and Shift-Tab.
412 if ( me._.tabBarMode )
413 {
414 // Change tabs.
415 var nextId = shiftPressed ? getPreviousVisibleTab.call( me ) : getNextVisibleTab.call( me );
416 me.selectPage( nextId );
417 me._.tabs[ nextId ][ 0 ].focus();
418 }
419 else
420 {
421 // Change the focus of inputs.
422 changeFocus( !shiftPressed );
423 }
424
425 processed = 1;
426 }
427 else if ( keystroke == CKEDITOR.ALT + 121 && !me._.tabBarMode && me.getPageCount() > 1 )
428 {
429 // Alt-F10 puts focus into the current tab item in the tab bar.
430 me._.tabBarMode = true;
431 me._.tabs[ me._.currentTabId ][ 0 ].focus();
432 processed = 1;
433 }
434 else if ( ( keystroke == 37 || keystroke == 39 ) && me._.tabBarMode )
435 {
436 // Arrow keys - used for changing tabs.
437 nextId = ( keystroke == ( rtl ? 39 : 37 ) ? getPreviousVisibleTab.call( me ) : getNextVisibleTab.call( me ) );
438 me.selectPage( nextId );
439 me._.tabs[ nextId ][ 0 ].focus();
440 processed = 1;
441 }
442 else if ( ( keystroke == 13 || keystroke == 32 ) && me._.tabBarMode )
443 {
444 this.selectPage( this._.currentTabId );
445 this._.tabBarMode = false;
446 this._.currentFocusIndex = -1;
447 changeFocus( true );
448 processed = 1;
449 }
450
451 if ( processed )
452 {
453 evt.stop();
454 evt.data.preventDefault();
455 }
456 }
457
458 function focusKeyPressHandler( evt )
459 {
460 processed && evt.data.preventDefault();
461 }
462
463 var dialogElement = this._.element;
464 // Add the dialog keyboard handlers.
465 this.on( 'show', function()
466 {
467 dialogElement.on( 'keydown', focusKeydownHandler, this, null, 0 );
468 // Some browsers instead, don't cancel key events in the keydown, but in the
469 // keypress. So we must do a longer trip in those cases. (#4531)
470 if ( CKEDITOR.env.opera || ( CKEDITOR.env.gecko && CKEDITOR.env.mac ) )
471 dialogElement.on( 'keypress', focusKeyPressHandler, this );
472
473 } );
474 this.on( 'hide', function()
475 {
476 dialogElement.removeListener( 'keydown', focusKeydownHandler );
477 if ( CKEDITOR.env.opera || ( CKEDITOR.env.gecko && CKEDITOR.env.mac ) )
478 dialogElement.removeListener( 'keypress', focusKeyPressHandler );
479
480 // Reset fields state when closing dialog.
481 iterContents( function( item ) { resetField.apply( item ); } );
482 } );
483 this.on( 'iframeAdded', function( evt )
484 {
485 var doc = new CKEDITOR.dom.document( evt.data.iframe.$.contentWindow.document );
486 doc.on( 'keydown', focusKeydownHandler, this, null, 0 );
487 } );
488
489 // Auto-focus logic in dialog.
490 this.on( 'show', function()
491 {
492 // Setup tabIndex on showing the dialog instead of on loading
493 // to allow dynamic tab order happen in dialog definition.
494 setupFocus();
495
496 if ( editor.config.dialog_startupFocusTab
497 && me._.pageCount > 1 )
498 {
499 me._.tabBarMode = true;
500 me._.tabs[ me._.currentTabId ][ 0 ].focus();
501 }
502 else if ( !this._.hasFocus )
503 {
504 this._.currentFocusIndex = -1;
505
506 // Decide where to put the initial focus.
507 if ( definition.onFocus )
508 {
509 var initialFocus = definition.onFocus.call( this );
510 // Focus the field that the user specified.
511 initialFocus && initialFocus.focus();
512 }
513 // Focus the first field in layout order.
514 else
515 changeFocus( true );
516
517 /*
518 * IE BUG: If the initial focus went into a non-text element (e.g. button),
519 * then IE would still leave the caret inside the editing area.
520 */
521 if ( this._.editor.mode == 'wysiwyg' && CKEDITOR.env.ie )
522 {
523 var $selection = editor.document.$.selection,
524 $range = $selection.createRange();
525
526 if ( $range )
527 {
528 if ( $range.parentElement && $range.parentElement().ownerDocument == editor.document.$
529 || $range.item && $range.item( 0 ).ownerDocument == editor.document.$ )
530 {
531 var $myRange = document.body.createTextRange();
532 $myRange.moveToElementText( this.getElement().getFirst().$ );
533 $myRange.collapse( true );
534 $myRange.select();
535 }
536 }
537 }
538 }
539 }, this, null, 0xffffffff );
540
541 // IE6 BUG: Text fields and text areas are only half-rendered the first time the dialog appears in IE6 (#2661).
542 // This is still needed after [2708] and [2709] because text fields in hidden TR tags are still broken.
543 if ( CKEDITOR.env.ie6Compat )
544 {
545 this.on( 'load', function( evt )
546 {
547 var outer = this.getElement(),
548 inner = outer.getFirst();
549 inner.remove();
550 inner.appendTo( outer );
551 }, this );
552 }
553
554 initDragAndDrop( this );
555 initResizeHandles( this );
556
557 // Insert the title.
558 ( new CKEDITOR.dom.text( definition.title, CKEDITOR.document ) ).appendTo( this.parts.title );
559
560 // Insert the tabs and contents.
561 for ( var i = 0 ; i < definition.contents.length ; i++ )
562 {
563 var page = definition.contents[i];
564 page && this.addPage( page );
565 }
566
567 this.parts[ 'tabs' ].on( 'click', function( evt )
568 {
569 var target = evt.data.getTarget();
570 // If we aren't inside a tab, bail out.
571 if ( target.hasClass( 'cke_dialog_tab' ) )
572 {
573 // Get the ID of the tab, without the 'cke_' prefix and the unique number suffix.
574 var id = target.$.id;
575 this.selectPage( id.substring( 4, id.lastIndexOf( '_' ) ) );
576
577 if ( this._.tabBarMode )
578 {
579 this._.tabBarMode = false;
580 this._.currentFocusIndex = -1;
581 changeFocus( true );
582 }
583 evt.data.preventDefault();
584 }
585 }, this );
586
587 // Insert buttons.
588 var buttonsHtml = [],
589 buttons = CKEDITOR.dialog._.uiElementBuilders.hbox.build( this,
590 {
591 type : 'hbox',
592 className : 'cke_dialog_footer_buttons',
593 widths : [],
594 children : definition.buttons
595 }, buttonsHtml ).getChild();
596 this.parts.footer.setHtml( buttonsHtml.join( '' ) );
597
598 for ( i = 0 ; i < buttons.length ; i++ )
599 this._.buttons[ buttons[i].id ] = buttons[i];
600 };
601
602 // Focusable interface. Use it via dialog.addFocusable.
603 function Focusable( dialog, element, index )
604 {
605 this.element = element;
606 this.focusIndex = index;
607 // TODO: support tabIndex for focusables.
608 this.tabIndex = 0;
609 this.isFocusable = function()
610 {
611 return !element.getAttribute( 'disabled' ) && element.isVisible();
612 };
613 this.focus = function()
614 {
615 dialog._.currentFocusIndex = this.focusIndex;
616 this.element.focus();
617 };
618 // Bind events
619 element.on( 'keydown', function( e )
620 {
621 if ( e.data.getKeystroke() in { 32:1, 13:1 } )
622 this.fire( 'click' );
623 } );
624 element.on( 'focus', function()
625 {
626 this.fire( 'mouseover' );
627 } );
628 element.on( 'blur', function()
629 {
630 this.fire( 'mouseout' );
631 } );
632 }
633
634 CKEDITOR.dialog.prototype =
635 {
636 destroy : function()
637 {
638 this.hide();
639 this._.element.remove();
640 },
641
642 /**
643 * Resizes the dialog.
644 * @param {Number} width The width of the dialog in pixels.
645 * @param {Number} height The height of the dialog in pixels.
646 * @function
647 * @example
648 * dialogObj.resize( 800, 640 );
649 */
650 resize : (function()
651 {
652 return function( width, height )
653 {
654 if ( this._.contentSize && this._.contentSize.width == width && this._.contentSize.height == height )
655 return;
656
657 CKEDITOR.dialog.fire( 'resize',
658 {
659 dialog : this,
660 skin : this._.editor.skinName,
661 width : width,
662 height : height
663 }, this._.editor );
664
665 this.fire( 'resize',
666 {
667 skin : this._.editor.skinName,
668 width : width,
669 height : height
670 }, this._.editor );
671
672 // Update dialog position when dimension get changed in RTL.
673 if ( this._.editor.lang.dir == 'rtl' && this._.position )
674 this._.position.x = CKEDITOR.document.getWindow().getViewPaneSize().width -
675 this._.contentSize.width - parseInt( this._.element.getFirst().getStyle( 'right' ), 10 );
676
677 this._.contentSize = { width : width, height : height };
678 };
679 })(),
680
681 /**
682 * Gets the current size of the dialog in pixels.
683 * @returns {Object} An object with "width" and "height" properties.
684 * @example
685 * var width = dialogObj.getSize().width;
686 */
687 getSize : function()
688 {
689 var element = this._.element.getFirst();
690 return { width : element.$.offsetWidth || 0, height : element.$.offsetHeight || 0};
691 },
692
693 /**
694 * Moves the dialog to an (x, y) coordinate relative to the window.
695 * @function
696 * @param {Number} x The target x-coordinate.
697 * @param {Number} y The target y-coordinate.
698 * @param {Boolean} save Flag indicate whether the dialog position should be remembered on next open up.
699 * @example
700 * dialogObj.move( 10, 40 );
701 */
702 move : (function()
703 {
704 var isFixed;
705 return function( x, y, save )
706 {
707 // The dialog may be fixed positioned or absolute positioned. Ask the
708 // browser what is the current situation first.
709 var element = this._.element.getFirst(),
710 rtl = this._.editor.lang.dir == 'rtl';
711
712 if ( isFixed === undefined )
713 isFixed = element.getComputedStyle( 'position' ) == 'fixed';
714
715 if ( isFixed && this._.position && this._.position.x == x && this._.position.y == y )
716 return;
717
718 // Save the current position.
719 this._.position = { x : x, y : y };
720
721 // If not fixed positioned, add scroll position to the coordinates.
722 if ( !isFixed )
723 {
724 var scrollPosition = CKEDITOR.document.getWindow().getScrollPosition();
725 x += scrollPosition.x;
726 y += scrollPosition.y;
727 }
728
729 // Translate coordinate for RTL.
730 if ( rtl )
731 {
732 var dialogSize = this.getSize(),
733 viewPaneSize = CKEDITOR.document.getWindow().getViewPaneSize();
734 x = viewPaneSize.width - dialogSize.width - x;
735 }
736
737 var styles = { 'top' : ( y > 0 ? y : 0 ) + 'px' };
738 styles[ rtl ? 'right' : 'left' ] = ( x > 0 ? x : 0 ) + 'px';
739
740 element.setStyles( styles );
741
742 save && ( this._.moved = 1 );
743 };
744 })(),
745
746 /**
747 * Gets the dialog's position in the window.
748 * @returns {Object} An object with "x" and "y" properties.
749 * @example
750 * var dialogX = dialogObj.getPosition().x;
751 */
752 getPosition : function(){ return CKEDITOR.tools.extend( {}, this._.position ); },
753
754 /**
755 * Shows the dialog box.
756 * @example
757 * dialogObj.show();
758 */
759 show : function()
760 {
761 // Insert the dialog's element to the root document.
762 var element = this._.element;
763 var definition = this.definition;
764 if ( !( element.getParent() && element.getParent().equals( CKEDITOR.document.getBody() ) ) )
765 element.appendTo( CKEDITOR.document.getBody() );
766 else
767 element.setStyle( 'display', 'block' );
768
769 // FIREFOX BUG: Fix vanishing caret for Firefox 2 or Gecko 1.8.
770 if ( CKEDITOR.env.gecko && CKEDITOR.env.version < 10900 )
771 {
772 var dialogElement = this.parts.dialog;
773 dialogElement.setStyle( 'position', 'absolute' );
774 setTimeout( function()
775 {
776 dialogElement.setStyle( 'position', 'fixed' );
777 }, 0 );
778 }
779
780
781 // First, set the dialog to an appropriate size.
782 this.resize( this._.contentSize && this._.contentSize.width || definition.width || definition.minWidth,
783 this._.contentSize && this._.contentSize.height || definition.height || definition.minHeight );
784
785 // Reset all inputs back to their default value.
786 this.reset();
787
788 // Select the first tab by default.
789 this.selectPage( this.definition.contents[0].id );
790
791 // Set z-index.
792 if ( CKEDITOR.dialog._.currentZIndex === null )
793 CKEDITOR.dialog._.currentZIndex = this._.editor.config.baseFloatZIndex;
794 this._.element.getFirst().setStyle( 'z-index', CKEDITOR.dialog._.currentZIndex += 10 );
795
796 // Maintain the dialog ordering and dialog cover.
797 // Also register key handlers if first dialog.
798 if ( CKEDITOR.dialog._.currentTop === null )
799 {
800 CKEDITOR.dialog._.currentTop = this;
801 this._.parentDialog = null;
802 showCover( this._.editor );
803
804 element.on( 'keydown', accessKeyDownHandler );
805 element.on( CKEDITOR.env.opera ? 'keypress' : 'keyup', accessKeyUpHandler );
806
807 // Prevent some keys from bubbling up. (#4269)
808 for ( var event in { keyup :1, keydown :1, keypress :1 } )
809 element.on( event, preventKeyBubbling );
810 }
811 else
812 {
813 this._.parentDialog = CKEDITOR.dialog._.currentTop;
814 var parentElement = this._.parentDialog.getElement().getFirst();
815 parentElement.$.style.zIndex -= Math.floor( this._.editor.config.baseFloatZIndex / 2 );
816 CKEDITOR.dialog._.currentTop = this;
817 }
818
819 // Register the Esc hotkeys.
820 registerAccessKey( this, this, '\x1b', null, function()
821 {
822 this.getButton( 'cancel' ) && this.getButton( 'cancel' ).click();
823 } );
824
825 // Reset the hasFocus state.
826 this._.hasFocus = false;
827
828 CKEDITOR.tools.setTimeout( function()
829 {
830 this.layout();
831 this.parts.dialog.setStyle( 'visibility', '' );
832
833 // Execute onLoad for the first show.
834 this.fireOnce( 'load', {} );
835 CKEDITOR.ui.fire( 'ready', this );
836
837 this.fire( 'show', {} );
838 this._.editor.fire( 'dialogShow', this );
839
840 // Save the initial values of the dialog.
841 this.foreach( function( contentObj ) { contentObj.setInitValue && contentObj.setInitValue(); } );
842
843 },
844 100, this );
845 },
846
847 /**
848 * Rearrange the dialog to its previous position or the middle of the window.
849 * @since 3.5
850 */
851 layout : function()
852 {
853 var viewSize = CKEDITOR.document.getWindow().getViewPaneSize(),
854 dialogSize = this.getSize();
855
856 this.move( this._.moved ? this._.position.x : ( viewSize.width - dialogSize.width ) / 2,
857 this._.moved ? this._.position.y : ( viewSize.height - dialogSize.height ) / 2 );
858 },
859
860 /**
861 * Executes a function for each UI element.
862 * @param {Function} fn Function to execute for each UI element.
863 * @returns {CKEDITOR.dialog} The current dialog object.
864 */
865 foreach : function( fn )
866 {
867 for ( var i in this._.contents )
868 {
869 for ( var j in this._.contents[i] )
870 fn.call( this, this._.contents[i][j] );
871 }
872 return this;
873 },
874
875 /**
876 * Resets all input values in the dialog.
877 * @example
878 * dialogObj.reset();
879 * @returns {CKEDITOR.dialog} The current dialog object.
880 */
881 reset : (function()
882 {
883 var fn = function( widget ){ if ( widget.reset ) widget.reset( 1 ); };
884 return function(){ this.foreach( fn ); return this; };
885 })(),
886
887
888 /**
889 * Calls the {@link CKEDITOR.dialog.definition.uiElement#setup} method of each of the UI elements, with the arguments passed through it.
890 * It is usually being called when the dialog is opened, to put the initial value inside the field.
891 * @example
892 * dialogObj.setupContent();
893 * @example
894 * var timestamp = ( new Date() ).valueOf();
895 * dialogObj.setupContent( timestamp );
896 */
897 setupContent : function()
898 {
899 var args = arguments;
900 this.foreach( function( widget )
901 {
902 if ( widget.setup )
903 widget.setup.apply( widget, args );
904 });
905 },
906
907 /**
908 * Calls the {@link CKEDITOR.dialog.definition.uiElement#commit} method of each of the UI elements, with the arguments passed through it.
909 * It is usually being called when the user confirms the dialog, to process the values.
910 * @example
911 * dialogObj.commitContent();
912 * @example
913 * var timestamp = ( new Date() ).valueOf();
914 * dialogObj.commitContent( timestamp );
915 */
916 commitContent : function()
917 {
918 var args = arguments;
919 this.foreach( function( widget )
920 {
921 // Make sure IE triggers "change" event on last focused input before closing the dialog. (#7915)
922 if ( CKEDITOR.env.ie && this._.currentFocusIndex == widget.focusIndex )
923 widget.getInputElement().$.blur();
924
925 if ( widget.commit )
926 widget.commit.apply( widget, args );
927 });
928 },
929
930 /**
931 * Hides the dialog box.
932 * @example
933 * dialogObj.hide();
934 */
935 hide : function()
936 {
937 if ( !this.parts.dialog.isVisible() )
938 return;
939
940 this.fire( 'hide', {} );
941 this._.editor.fire( 'dialogHide', this );
942 var element = this._.element;
943 element.setStyle( 'display', 'none' );
944 this.parts.dialog.setStyle( 'visibility', 'hidden' );
945 // Unregister all access keys associated with this dialog.
946 unregisterAccessKey( this );
947
948 // Close any child(top) dialogs first.
949 while( CKEDITOR.dialog._.currentTop != this )
950 CKEDITOR.dialog._.currentTop.hide();
951
952 // Maintain dialog ordering and remove cover if needed.
953 if ( !this._.parentDialog )
954 hideCover();
955 else
956 {
957 var parentElement = this._.parentDialog.getElement().getFirst();
958 parentElement.setStyle( 'z-index', parseInt( parentElement.$.style.zIndex, 10 ) + Math.floor( this._.editor.config.baseFloatZIndex / 2 ) );
959 }
960 CKEDITOR.dialog._.currentTop = this._.parentDialog;
961
962 // Deduct or clear the z-index.
963 if ( !this._.parentDialog )
964 {
965 CKEDITOR.dialog._.currentZIndex = null;
966
967 // Remove access key handlers.
968 element.removeListener( 'keydown', accessKeyDownHandler );
969 element.removeListener( CKEDITOR.env.opera ? 'keypress' : 'keyup', accessKeyUpHandler );
970
971 // Remove bubbling-prevention handler. (#4269)
972 for ( var event in { keyup :1, keydown :1, keypress :1 } )
973 element.removeListener( event, preventKeyBubbling );
974
975 var editor = this._.editor;
976 editor.focus();
977
978 if ( editor.mode == 'wysiwyg' && CKEDITOR.env.ie )
979 {
980 var selection = editor.getSelection();
981 selection && selection.unlock( true );
982 }
983 }
984 else
985 CKEDITOR.dialog._.currentZIndex -= 10;
986
987 delete this._.parentDialog;
988 // Reset the initial values of the dialog.
989 this.foreach( function( contentObj ) { contentObj.resetInitValue && contentObj.resetInitValue(); } );
990 },
991
992 /**
993 * Adds a tabbed page into the dialog.
994 * @param {Object} contents Content definition.
995 * @example
996 */
997 addPage : function( contents )
998 {
999 var pageHtml = [],
1000 titleHtml = contents.label ? ' title="' + CKEDITOR.tools.htmlEncode( contents.label ) + '"' : '',
1001 elements = contents.elements,
1002 vbox = CKEDITOR.dialog._.uiElementBuilders.vbox.build( this,
1003 {
1004 type : 'vbox',
1005 className : 'cke_dialog_page_contents',
1006 children : contents.elements,
1007 expand : !!contents.expand,
1008 padding : contents.padding,
1009 style : contents.style || 'width: 100%;height:100%'
1010 }, pageHtml );
1011
1012 // Create the HTML for the tab and the content block.
1013 var page = CKEDITOR.dom.element.createFromHtml( pageHtml.join( '' ) );
1014 page.setAttribute( 'role', 'tabpanel' );
1015
1016 var env = CKEDITOR.env;
1017 var tabId = 'cke_' + contents.id + '_' + CKEDITOR.tools.getNextNumber(),
1018 tab = CKEDITOR.dom.element.createFromHtml( [
1019 '<a class="cke_dialog_tab"',
1020 ( this._.pageCount > 0 ? ' cke_last' : 'cke_first' ),
1021 titleHtml,
1022 ( !!contents.hidden ? ' style="display:none"' : '' ),
1023 ' id="', tabId, '"',
1024 env.gecko && env.version >= 10900 && !env.hc ? '' : ' href="javascript:void(0)"',
1025 ' tabIndex="-1"',
1026 ' hidefocus="true"',
1027 ' role="tab">',
1028 contents.label,
1029 '</a>'
1030 ].join( '' ) );
1031
1032 page.setAttribute( 'aria-labelledby', tabId );
1033
1034 // Take records for the tabs and elements created.
1035 this._.tabs[ contents.id ] = [ tab, page ];
1036 this._.tabIdList.push( contents.id );
1037 !contents.hidden && this._.pageCount++;
1038 this._.lastTab = tab;
1039 this.updateStyle();
1040
1041 var contentMap = this._.contents[ contents.id ] = {},
1042 cursor,
1043 children = vbox.getChild();
1044
1045 while ( ( cursor = children.shift() ) )
1046 {
1047 contentMap[ cursor.id ] = cursor;
1048 if ( typeof( cursor.getChild ) == 'function' )
1049 children.push.apply( children, cursor.getChild() );
1050 }
1051
1052 // Attach the DOM nodes.
1053
1054 page.setAttribute( 'name', contents.id );
1055 page.appendTo( this.parts.contents );
1056
1057 tab.unselectable();
1058 this.parts.tabs.append( tab );
1059
1060 // Add access key handlers if access key is defined.
1061 if ( contents.accessKey )
1062 {
1063 registerAccessKey( this, this, 'CTRL+' + contents.accessKey,
1064 tabAccessKeyDown, tabAccessKeyUp );
1065 this._.accessKeyMap[ 'CTRL+' + contents.accessKey ] = contents.id;
1066 }
1067 },
1068
1069 /**
1070 * Activates a tab page in the dialog by its id.
1071 * @param {String} id The id of the dialog tab to be activated.
1072 * @example
1073 * dialogObj.selectPage( 'tab_1' );
1074 */
1075 selectPage : function( id )
1076 {
1077 if ( this._.currentTabId == id )
1078 return;
1079
1080 // Returning true means that the event has been canceled
1081 if ( this.fire( 'selectPage', { page : id, currentPage : this._.currentTabId } ) === true )
1082 return;
1083
1084 // Hide the non-selected tabs and pages.
1085 for ( var i in this._.tabs )
1086 {
1087 var tab = this._.tabs[i][0],
1088 page = this._.tabs[i][1];
1089 if ( i != id )
1090 {
1091 tab.removeClass( 'cke_dialog_tab_selected' );
1092 page.hide();
1093 }
1094 page.setAttribute( 'aria-hidden', i != id );
1095 }
1096
1097 var selected = this._.tabs[ id ];
1098 selected[ 0 ].addClass( 'cke_dialog_tab_selected' );
1099
1100 // [IE] an invisible input[type='text'] will enlarge it's width
1101 // if it's value is long when it shows, so we clear it's value
1102 // before it shows and then recover it (#5649)
1103 if ( CKEDITOR.env.ie6Compat || CKEDITOR.env.ie7Compat )
1104 {
1105 clearOrRecoverTextInputValue( selected[ 1 ] );
1106 selected[ 1 ].show();
1107 setTimeout( function()
1108 {
1109 clearOrRecoverTextInputValue( selected[ 1 ], 1 );
1110 }, 0 );
1111 }
1112 else
1113 selected[ 1 ].show();
1114
1115 this._.currentTabId = id;
1116 this._.currentTabIndex = CKEDITOR.tools.indexOf( this._.tabIdList, id );
1117 },
1118
1119 // Dialog state-specific style updates.
1120 updateStyle : function()
1121 {
1122 // If only a single page shown, a different style is used in the central pane.
1123 this.parts.dialog[ ( this._.pageCount === 1 ? 'add' : 'remove' ) + 'Class' ]( 'cke_single_page' );
1124 },
1125
1126 /**
1127 * Hides a page's tab away from the dialog.
1128 * @param {String} id The page's Id.
1129 * @example
1130 * dialog.hidePage( 'tab_3' );
1131 */
1132 hidePage : function( id )
1133 {
1134 var tab = this._.tabs[id] && this._.tabs[id][0];
1135 if ( !tab || this._.pageCount == 1 || !tab.isVisible() )
1136 return;
1137 // Switch to other tab first when we're hiding the active tab.
1138 else if ( id == this._.currentTabId )
1139 this.selectPage( getPreviousVisibleTab.call( this ) );
1140
1141 tab.hide();
1142 this._.pageCount--;
1143 this.updateStyle();
1144 },
1145
1146 /**
1147 * Unhides a page's tab.
1148 * @param {String} id The page's Id.
1149 * @example
1150 * dialog.showPage( 'tab_2' );
1151 */
1152 showPage : function( id )
1153 {
1154 var tab = this._.tabs[id] && this._.tabs[id][0];
1155 if ( !tab )
1156 return;
1157 tab.show();
1158 this._.pageCount++;
1159 this.updateStyle();
1160 },
1161
1162 /**
1163 * Gets the root DOM element of the dialog.
1164 * @returns {CKEDITOR.dom.element} The &lt;span&gt; element containing this dialog.
1165 * @example
1166 * var dialogElement = dialogObj.getElement().getFirst();
1167 * dialogElement.setStyle( 'padding', '5px' );
1168 */
1169 getElement : function()
1170 {
1171 return this._.element;
1172 },
1173
1174 /**
1175 * Gets the name of the dialog.
1176 * @returns {String} The name of this dialog.
1177 * @example
1178 * var dialogName = dialogObj.getName();
1179 */
1180 getName : function()
1181 {
1182 return this._.name;
1183 },
1184
1185 /**
1186 * Gets a dialog UI element object from a dialog page.
1187 * @param {String} pageId id of dialog page.
1188 * @param {String} elementId id of UI element.
1189 * @example
1190 * dialogObj.getContentElement( 'tabId', 'elementId' ).setValue( 'Example' );
1191 * @returns {CKEDITOR.ui.dialog.uiElement} The dialog UI element.
1192 */
1193 getContentElement : function( pageId, elementId )
1194 {
1195 var page = this._.contents[ pageId ];
1196 return page && page[ elementId ];
1197 },
1198
1199 /**
1200 * Gets the value of a dialog UI element.
1201 * @param {String} pageId id of dialog page.
1202 * @param {String} elementId id of UI element.
1203 * @example
1204 * alert( dialogObj.getValueOf( 'tabId', 'elementId' ) );
1205 * @returns {Object} The value of the UI element.
1206 */
1207 getValueOf : function( pageId, elementId )
1208 {
1209 return this.getContentElement( pageId, elementId ).getValue();
1210 },
1211
1212 /**
1213 * Sets the value of a dialog UI element.
1214 * @param {String} pageId id of the dialog page.
1215 * @param {String} elementId id of the UI element.
1216 * @param {Object} value The new value of the UI element.
1217 * @example
1218 * dialogObj.setValueOf( 'tabId', 'elementId', 'Example' );
1219 */
1220 setValueOf : function( pageId, elementId, value )
1221 {
1222 return this.getContentElement( pageId, elementId ).setValue( value );
1223 },
1224
1225 /**
1226 * Gets the UI element of a button in the dialog's button row.
1227 * @param {String} id The id of the button.
1228 * @example
1229 * @returns {CKEDITOR.ui.dialog.button} The button object.
1230 */
1231 getButton : function( id )
1232 {
1233 return this._.buttons[ id ];
1234 },
1235
1236 /**
1237 * Simulates a click to a dialog button in the dialog's button row.
1238 * @param {String} id The id of the button.
1239 * @example
1240 * @returns The return value of the dialog's "click" event.
1241 */
1242 click : function( id )
1243 {
1244 return this._.buttons[ id ].click();
1245 },
1246
1247 /**
1248 * Disables a dialog button.
1249 * @param {String} id The id of the button.
1250 * @example
1251 */
1252 disableButton : function( id )
1253 {
1254 return this._.buttons[ id ].disable();
1255 },
1256
1257 /**
1258 * Enables a dialog button.
1259 * @param {String} id The id of the button.
1260 * @example
1261 */
1262 enableButton : function( id )
1263 {
1264 return this._.buttons[ id ].enable();
1265 },
1266
1267 /**
1268 * Gets the number of pages in the dialog.
1269 * @returns {Number} Page count.
1270 */
1271 getPageCount : function()
1272 {
1273 return this._.pageCount;
1274 },
1275
1276 /**
1277 * Gets the editor instance which opened this dialog.
1278 * @returns {CKEDITOR.editor} Parent editor instances.
1279 */
1280 getParentEditor : function()
1281 {
1282 return this._.editor;
1283 },
1284
1285 /**
1286 * Gets the element that was selected when opening the dialog, if any.
1287 * @returns {CKEDITOR.dom.element} The element that was selected, or null.
1288 */
1289 getSelectedElement : function()
1290 {
1291 return this.getParentEditor().getSelection().getSelectedElement();
1292 },
1293
1294 /**
1295 * Adds element to dialog's focusable list.
1296 *
1297 * @param {CKEDITOR.dom.element} element
1298 * @param {Number} [index]
1299 */
1300 addFocusable: function( element, index ) {
1301 if ( typeof index == 'undefined' )
1302 {
1303 index = this._.focusList.length;
1304 this._.focusList.push( new Focusable( this, element, index ) );
1305 }
1306 else
1307 {
1308 this._.focusList.splice( index, 0, new Focusable( this, element, index ) );
1309 for ( var i = index + 1 ; i < this._.focusList.length ; i++ )
1310 this._.focusList[ i ].focusIndex++;
1311 }
1312 }
1313 };
1314
1315 CKEDITOR.tools.extend( CKEDITOR.dialog,
1316 /**
1317 * @lends CKEDITOR.dialog
1318 */
1319 {
1320 /**
1321 * Registers a dialog.
1322 * @param {String} name The dialog's name.
1323 * @param {Function|String} dialogDefinition
1324 * A function returning the dialog's definition, or the URL to the .js file holding the function.
1325 * The function should accept an argument "editor" which is the current editor instance, and
1326 * return an object conforming to {@link CKEDITOR.dialog.definition}.
1327 * @see CKEDITOR.dialog.definition
1328 * @example
1329 * // Full sample plugin, which does not only register a dialog window but also adds an item to the context menu.
1330 * // To open the dialog window, choose "Open dialog" in the context menu.
1331 * CKEDITOR.plugins.add( 'myplugin',
1332 * {
1333 * init: function( editor )
1334 * {
1335 * editor.addCommand( 'mydialog',new CKEDITOR.dialogCommand( 'mydialog' ) );
1336 *
1337 * if ( editor.contextMenu )
1338 * {
1339 * editor.addMenuGroup( 'mygroup', 10 );
1340 * editor.addMenuItem( 'My Dialog',
1341 * {
1342 * label : 'Open dialog',
1343 * command : 'mydialog',
1344 * group : 'mygroup'
1345 * });
1346 * editor.contextMenu.addListener( function( element )
1347 * {
1348 * return { 'My Dialog' : CKEDITOR.TRISTATE_OFF };
1349 * });
1350 * }
1351 *
1352 * <strong>CKEDITOR.dialog.add</strong>( 'mydialog', function( api )
1353 * {
1354 * // CKEDITOR.dialog.definition
1355 * var <strong>dialogDefinition</strong> =
1356 * {
1357 * title : 'Sample dialog',
1358 * minWidth : 390,
1359 * minHeight : 130,
1360 * contents : [
1361 * {
1362 * id : 'tab1',
1363 * label : 'Label',
1364 * title : 'Title',
1365 * expand : true,
1366 * padding : 0,
1367 * elements :
1368 * [
1369 * {
1370 * type : 'html',
1371 * html : '&lt;p&gt;This is some sample HTML content.&lt;/p&gt;'
1372 * },
1373 * {
1374 * type : 'textarea',
1375 * id : 'textareaId',
1376 * rows : 4,
1377 * cols : 40
1378 * }
1379 * ]
1380 * }
1381 * ],
1382 * buttons : [ CKEDITOR.dialog.okButton, CKEDITOR.dialog.cancelButton ],
1383 * onOk : function() {
1384 * // "this" is now a CKEDITOR.dialog object.
1385 * // Accessing dialog elements:
1386 * var textareaObj = this.<strong>getContentElement</strong>( 'tab1', 'textareaId' );
1387 * alert( "You have entered: " + textareaObj.getValue() );
1388 * }
1389 * };
1390 *
1391 * return dialogDefinition;
1392 * } );
1393 * }
1394 * } );
1395 *
1396 * CKEDITOR.replace( 'editor1', { extraPlugins : 'myplugin' } );
1397 */
1398 add : function( name, dialogDefinition )
1399 {
1400 // Avoid path registration from multiple instances override definition.
1401 if ( !this._.dialogDefinitions[name]
1402 || typeof dialogDefinition == 'function' )
1403 this._.dialogDefinitions[name] = dialogDefinition;
1404 },
1405
1406 exists : function( name )
1407 {
1408 return !!this._.dialogDefinitions[ name ];
1409 },
1410
1411 getCurrent : function()
1412 {
1413 return CKEDITOR.dialog._.currentTop;
1414 },
1415
1416 /**
1417 * The default OK button for dialogs. Fires the "ok" event and closes the dialog if the event succeeds.
1418 * @static
1419 * @field
1420 * @example
1421 * @type Function
1422 */
1423 okButton : (function()
1424 {
1425 var retval = function( editor, override )
1426 {
1427 override = override || {};
1428 return CKEDITOR.tools.extend( {
1429 id : 'ok',
1430 type : 'button',
1431 label : editor.lang.common.ok,
1432 'class' : 'cke_dialog_ui_button_ok',
1433 onClick : function( evt )
1434 {
1435 var dialog = evt.data.dialog;
1436 if ( dialog.fire( 'ok', { hide : true } ).hide !== false )
1437 dialog.hide();
1438 }
1439 }, override, true );
1440 };
1441 retval.type = 'button';
1442 retval.override = function( override )
1443 {
1444 return CKEDITOR.tools.extend( function( editor ){ return retval( editor, override ); },
1445 { type : 'button' }, true );
1446 };
1447 return retval;
1448 })(),
1449
1450 /**
1451 * The default cancel button for dialogs. Fires the "cancel" event and closes the dialog if no UI element value changed.
1452 * @static
1453 * @field
1454 * @example
1455 * @type Function
1456 */
1457 cancelButton : (function()
1458 {
1459 var retval = function( editor, override )
1460 {
1461 override = override || {};
1462 return CKEDITOR.tools.extend( {
1463 id : 'cancel',
1464 type : 'button',
1465 label : editor.lang.common.cancel,
1466 'class' : 'cke_dialog_ui_button_cancel',
1467 onClick : function( evt )
1468 {
1469 var dialog = evt.data.dialog;
1470 if ( dialog.fire( 'cancel', { hide : true } ).hide !== false )
1471 dialog.hide();
1472 }
1473 }, override, true );
1474 };
1475 retval.type = 'button';
1476 retval.override = function( override )
1477 {
1478 return CKEDITOR.tools.extend( function( editor ){ return retval( editor, override ); },
1479 { type : 'button' }, true );
1480 };
1481 return retval;
1482 })(),
1483
1484 /**
1485 * Registers a dialog UI element.
1486 * @param {String} typeName The name of the UI element.
1487 * @param {Function} builder The function to build the UI element.
1488 * @example
1489 */
1490 addUIElement : function( typeName, builder )
1491 {
1492 this._.uiElementBuilders[ typeName ] = builder;
1493 }
1494 });
1495
1496 CKEDITOR.dialog._ =
1497 {
1498 uiElementBuilders : {},
1499
1500 dialogDefinitions : {},
1501
1502 currentTop : null,
1503
1504 currentZIndex : null
1505 };
1506
1507 // "Inherit" (copy actually) from CKEDITOR.event.
1508 CKEDITOR.event.implementOn( CKEDITOR.dialog );
1509 CKEDITOR.event.implementOn( CKEDITOR.dialog.prototype, true );
1510
1511 var defaultDialogDefinition =
1512 {
1513 resizable : CKEDITOR.DIALOG_RESIZE_BOTH,
1514 minWidth : 600,
1515 minHeight : 400,
1516 buttons : [ CKEDITOR.dialog.okButton, CKEDITOR.dialog.cancelButton ]
1517 };
1518
1519 // Tool function used to return an item from an array based on its id
1520 // property.
1521 var getById = function( array, id, recurse )
1522 {
1523 for ( var i = 0, item ; ( item = array[ i ] ) ; i++ )
1524 {
1525 if ( item.id == id )
1526 return item;
1527 if ( recurse && item[ recurse ] )
1528 {
1529 var retval = getById( item[ recurse ], id, recurse ) ;
1530 if ( retval )
1531 return retval;
1532 }
1533 }
1534 return null;
1535 };
1536
1537 // Tool function used to add an item into an array.
1538 var addById = function( array, newItem, nextSiblingId, recurse, nullIfNotFound )
1539 {
1540 if ( nextSiblingId )
1541 {
1542 for ( var i = 0, item ; ( item = array[ i ] ) ; i++ )
1543 {
1544 if ( item.id == nextSiblingId )
1545 {
1546 array.splice( i, 0, newItem );
1547 return newItem;
1548 }
1549
1550 if ( recurse && item[ recurse ] )
1551 {
1552 var retval = addById( item[ recurse ], newItem, nextSiblingId, recurse, true );
1553 if ( retval )
1554 return retval;
1555 }
1556 }
1557
1558 if ( nullIfNotFound )
1559 return null;
1560 }
1561
1562 array.push( newItem );
1563 return newItem;
1564 };
1565
1566 // Tool function used to remove an item from an array based on its id.
1567 var removeById = function( array, id, recurse )
1568 {
1569 for ( var i = 0, item ; ( item = array[ i ] ) ; i++ )
1570 {
1571 if ( item.id == id )
1572 return array.splice( i, 1 );
1573 if ( recurse && item[ recurse ] )
1574 {
1575 var retval = removeById( item[ recurse ], id, recurse );
1576 if ( retval )
1577 return retval;
1578 }
1579 }
1580 return null;
1581 };
1582
1583 /**
1584 * This class is not really part of the API. It is the "definition" property value
1585 * passed to "dialogDefinition" event handlers.
1586 * @constructor
1587 * @name CKEDITOR.dialog.definitionObject
1588 * @extends CKEDITOR.dialog.definition
1589 * @example
1590 * CKEDITOR.on( 'dialogDefinition', function( evt )
1591 * {
1592 * var definition = evt.data.definition;
1593 * var content = definition.getContents( 'page1' );
1594 * ...
1595 * } );
1596 */
1597 var definitionObject = function( dialog, dialogDefinition )
1598 {
1599 // TODO : Check if needed.
1600 this.dialog = dialog;
1601
1602 // Transform the contents entries in contentObjects.
1603 var contents = dialogDefinition.contents;
1604 for ( var i = 0, content ; ( content = contents[i] ) ; i++ )
1605 contents[ i ] = content && new contentObject( dialog, content );
1606
1607 CKEDITOR.tools.extend( this, dialogDefinition );
1608 };
1609
1610 definitionObject.prototype =
1611 /** @lends CKEDITOR.dialog.definitionObject.prototype */
1612 {
1613 /**
1614 * Gets a content definition.
1615 * @param {String} id The id of the content definition.
1616 * @returns {CKEDITOR.dialog.definition.content} The content definition
1617 * matching id.
1618 */
1619 getContents : function( id )
1620 {
1621 return getById( this.contents, id );
1622 },
1623
1624 /**
1625 * Gets a button definition.
1626 * @param {String} id The id of the button definition.
1627 * @returns {CKEDITOR.dialog.definition.button} The button definition
1628 * matching id.
1629 */
1630 getButton : function( id )
1631 {
1632 return getById( this.buttons, id );
1633 },
1634
1635 /**
1636 * Adds a content definition object under this dialog definition.
1637 * @param {CKEDITOR.dialog.definition.content} contentDefinition The
1638 * content definition.
1639 * @param {String} [nextSiblingId] The id of an existing content
1640 * definition which the new content definition will be inserted
1641 * before. Omit if the new content definition is to be inserted as
1642 * the last item.
1643 * @returns {CKEDITOR.dialog.definition.content} The inserted content
1644 * definition.
1645 */
1646 addContents : function( contentDefinition, nextSiblingId )
1647 {
1648 return addById( this.contents, contentDefinition, nextSiblingId );
1649 },
1650
1651 /**
1652 * Adds a button definition object under this dialog definition.
1653 * @param {CKEDITOR.dialog.definition.button} buttonDefinition The
1654 * button definition.
1655 * @param {String} [nextSiblingId] The id of an existing button
1656 * definition which the new button definition will be inserted
1657 * before. Omit if the new button definition is to be inserted as
1658 * the last item.
1659 * @returns {CKEDITOR.dialog.definition.button} The inserted button
1660 * definition.
1661 */
1662 addButton : function( buttonDefinition, nextSiblingId )
1663 {
1664 return addById( this.buttons, buttonDefinition, nextSiblingId );
1665 },
1666
1667 /**
1668 * Removes a content definition from this dialog definition.
1669 * @param {String} id The id of the content definition to be removed.
1670 * @returns {CKEDITOR.dialog.definition.content} The removed content
1671 * definition.
1672 */
1673 removeContents : function( id )
1674 {
1675 removeById( this.contents, id );
1676 },
1677
1678 /**
1679 * Removes a button definition from the dialog definition.
1680 * @param {String} id The id of the button definition to be removed.
1681 * @returns {CKEDITOR.dialog.definition.button} The removed button
1682 * definition.
1683 */
1684 removeButton : function( id )
1685 {
1686 removeById( this.buttons, id );
1687 }
1688 };
1689
1690 /**
1691 * This class is not really part of the API. It is the template of the
1692 * objects representing content pages inside the
1693 * CKEDITOR.dialog.definitionObject.
1694 * @constructor
1695 * @name CKEDITOR.dialog.definition.contentObject
1696 * @example
1697 * CKEDITOR.on( 'dialogDefinition', function( evt )
1698 * {
1699 * var definition = evt.data.definition;
1700 * var content = definition.getContents( 'page1' );
1701 * content.remove( 'textInput1' );
1702 * ...
1703 * } );
1704 */
1705 function contentObject( dialog, contentDefinition )
1706 {
1707 this._ =
1708 {
1709 dialog : dialog
1710 };
1711
1712 CKEDITOR.tools.extend( this, contentDefinition );
1713 }
1714
1715 contentObject.prototype =
1716 /** @lends CKEDITOR.dialog.definition.contentObject.prototype */
1717 {
1718 /**
1719 * Gets a UI element definition under the content definition.
1720 * @param {String} id The id of the UI element definition.
1721 * @returns {CKEDITOR.dialog.definition.uiElement}
1722 */
1723 get : function( id )
1724 {
1725 return getById( this.elements, id, 'children' );
1726 },
1727
1728 /**
1729 * Adds a UI element definition to the content definition.
1730 * @param {CKEDITOR.dialog.definition.uiElement} elementDefinition The
1731 * UI elemnet definition to be added.
1732 * @param {String} nextSiblingId The id of an existing UI element
1733 * definition which the new UI element definition will be inserted
1734 * before. Omit if the new button definition is to be inserted as
1735 * the last item.
1736 * @returns {CKEDITOR.dialog.definition.uiElement} The element
1737 * definition inserted.
1738 */
1739 add : function( elementDefinition, nextSiblingId )
1740 {
1741 return addById( this.elements, elementDefinition, nextSiblingId, 'children' );
1742 },
1743
1744 /**
1745 * Removes a UI element definition from the content definition.
1746 * @param {String} id The id of the UI element definition to be
1747 * removed.
1748 * @returns {CKEDITOR.dialog.definition.uiElement} The element
1749 * definition removed.
1750 * @example
1751 */
1752 remove : function( id )
1753 {
1754 removeById( this.elements, id, 'children' );
1755 }
1756 };
1757
1758 function initDragAndDrop( dialog )
1759 {
1760 var lastCoords = null,
1761 abstractDialogCoords = null,
1762 element = dialog.getElement().getFirst(),
1763 editor = dialog.getParentEditor(),
1764 magnetDistance = editor.config.dialog_magnetDistance,
1765 margins = editor.skin.margins || [ 0, 0, 0, 0 ];
1766
1767 if ( typeof magnetDistance == 'undefined' )
1768 magnetDistance = 20;
1769
1770 function mouseMoveHandler( evt )
1771 {
1772 var dialogSize = dialog.getSize(),
1773 viewPaneSize = CKEDITOR.document.getWindow().getViewPaneSize(),
1774 x = evt.data.$.screenX,
1775 y = evt.data.$.screenY,
1776 dx = x - lastCoords.x,
1777 dy = y - lastCoords.y,
1778 realX, realY;
1779
1780 lastCoords = { x : x, y : y };
1781 abstractDialogCoords.x += dx;
1782 abstractDialogCoords.y += dy;
1783
1784 if ( abstractDialogCoords.x + margins[3] < magnetDistance )
1785 realX = - margins[3];
1786 else if ( abstractDialogCoords.x - margins[1] > viewPaneSize.width - dialogSize.width - magnetDistance )
1787 realX = viewPaneSize.width - dialogSize.width + ( editor.lang.dir == 'rtl' ? 0 : margins[1] );
1788 else
1789 realX = abstractDialogCoords.x;
1790
1791 if ( abstractDialogCoords.y + margins[0] < magnetDistance )
1792 realY = - margins[0];
1793 else if ( abstractDialogCoords.y - margins[2] > viewPaneSize.height - dialogSize.height - magnetDistance )
1794 realY = viewPaneSize.height - dialogSize.height + margins[2];
1795 else
1796 realY = abstractDialogCoords.y;
1797
1798 dialog.move( realX, realY, 1 );
1799
1800 evt.data.preventDefault();
1801 }
1802
1803 function mouseUpHandler( evt )
1804 {
1805 CKEDITOR.document.removeListener( 'mousemove', mouseMoveHandler );
1806 CKEDITOR.document.removeListener( 'mouseup', mouseUpHandler );
1807
1808 if ( CKEDITOR.env.ie6Compat )
1809 {
1810 var coverDoc = currentCover.getChild( 0 ).getFrameDocument();
1811 coverDoc.removeListener( 'mousemove', mouseMoveHandler );
1812 coverDoc.removeListener( 'mouseup', mouseUpHandler );
1813 }
1814 }
1815
1816 dialog.parts.title.on( 'mousedown', function( evt )
1817 {
1818 lastCoords = { x : evt.data.$.screenX, y : evt.data.$.screenY };
1819
1820 CKEDITOR.document.on( 'mousemove', mouseMoveHandler );
1821 CKEDITOR.document.on( 'mouseup', mouseUpHandler );
1822 abstractDialogCoords = dialog.getPosition();
1823
1824 if ( CKEDITOR.env.ie6Compat )
1825 {
1826 var coverDoc = currentCover.getChild( 0 ).getFrameDocument();
1827 coverDoc.on( 'mousemove', mouseMoveHandler );
1828 coverDoc.on( 'mouseup', mouseUpHandler );
1829 }
1830
1831 evt.data.preventDefault();
1832 }, dialog );
1833 }
1834
1835 function initResizeHandles( dialog )
1836 {
1837 var def = dialog.definition,
1838 resizable = def.resizable;
1839
1840 if ( resizable == CKEDITOR.DIALOG_RESIZE_NONE )
1841 return;
1842
1843 var editor = dialog.getParentEditor();
1844 var wrapperWidth, wrapperHeight,
1845 viewSize, origin, startSize,
1846 dialogCover;
1847
1848 var mouseDownFn = CKEDITOR.tools.addFunction( function( $event )
1849 {
1850 startSize = dialog.getSize();
1851
1852 var content = dialog.parts.contents,
1853 iframeDialog = content.$.getElementsByTagName( 'iframe' ).length;
1854
1855 // Shim to help capturing "mousemove" over iframe.
1856 if ( iframeDialog )
1857 {
1858 dialogCover = CKEDITOR.dom.element.createFromHtml( '<div class="cke_dialog_resize_cover" style="height: 100%; position: absolute; width: 100%;"></div>' );
1859 content.append( dialogCover );
1860 }
1861
1862 // Calculate the offset between content and chrome size.
1863 wrapperHeight = startSize.height - dialog.parts.contents.getSize( 'height', ! ( CKEDITOR.env.gecko || CKEDITOR.env.opera || CKEDITOR.env.ie && CKEDITOR.env.quirks ) );
1864 wrapperWidth = startSize.width - dialog.parts.contents.getSize( 'width', 1 );
1865
1866 origin = { x : $event.screenX, y : $event.screenY };
1867
1868 viewSize = CKEDITOR.document.getWindow().getViewPaneSize();
1869
1870 CKEDITOR.document.on( 'mousemove', mouseMoveHandler );
1871 CKEDITOR.document.on( 'mouseup', mouseUpHandler );
1872
1873 if ( CKEDITOR.env.ie6Compat )
1874 {
1875 var coverDoc = currentCover.getChild( 0 ).getFrameDocument();
1876 coverDoc.on( 'mousemove', mouseMoveHandler );
1877 coverDoc.on( 'mouseup', mouseUpHandler );
1878 }
1879
1880 $event.preventDefault && $event.preventDefault();
1881 });
1882
1883 // Prepend the grip to the dialog.
1884 dialog.on( 'load', function()
1885 {
1886 var direction = '';
1887 if ( resizable == CKEDITOR.DIALOG_RESIZE_WIDTH )
1888 direction = ' cke_resizer_horizontal';
1889 else if ( resizable == CKEDITOR.DIALOG_RESIZE_HEIGHT )
1890 direction = ' cke_resizer_vertical';
1891 var resizer = CKEDITOR.dom.element.createFromHtml( '<div' +
1892 ' class="cke_resizer' + direction + ' cke_resizer_' + editor.lang.dir + '"' +
1893 ' title="' + CKEDITOR.tools.htmlEncode( editor.lang.resize ) + '"' +
1894 ' onmousedown="CKEDITOR.tools.callFunction(' + mouseDownFn + ', event )"></div>' );
1895 dialog.parts.footer.append( resizer, 1 );
1896 });
1897 editor.on( 'destroy', function() { CKEDITOR.tools.removeFunction( mouseDownFn ); } );
1898
1899 function mouseMoveHandler( evt )
1900 {
1901 var rtl = editor.lang.dir == 'rtl',
1902 dx = ( evt.data.$.screenX - origin.x ) * ( rtl ? -1 : 1 ),
1903 dy = evt.data.$.screenY - origin.y,
1904 width = startSize.width,
1905 height = startSize.height,
1906 internalWidth = width + dx * ( dialog._.moved ? 1 : 2 ),
1907 internalHeight = height + dy * ( dialog._.moved ? 1 : 2 ),
1908 element = dialog._.element.getFirst(),
1909 right = rtl && element.getComputedStyle( 'right' ),
1910 position = dialog.getPosition();
1911
1912 if ( position.y + internalHeight > viewSize.height )
1913 internalHeight = viewSize.height - position.y;
1914
1915 if ( ( rtl ? right : position.x ) + internalWidth > viewSize.width )
1916 internalWidth = viewSize.width - ( rtl ? right : position.x );
1917
1918 // Make sure the dialog will not be resized to the wrong side when it's in the leftmost position for RTL.
1919 if ( ( resizable == CKEDITOR.DIALOG_RESIZE_WIDTH || resizable == CKEDITOR.DIALOG_RESIZE_BOTH ) )
1920 width = Math.max( def.minWidth || 0, internalWidth - wrapperWidth );
1921
1922 if ( resizable == CKEDITOR.DIALOG_RESIZE_HEIGHT || resizable == CKEDITOR.DIALOG_RESIZE_BOTH )
1923 height = Math.max( def.minHeight || 0, internalHeight - wrapperHeight );
1924
1925 dialog.resize( width, height );
1926
1927 if ( !dialog._.moved )
1928 dialog.layout();
1929
1930 evt.data.preventDefault();
1931 }
1932
1933 function mouseUpHandler()
1934 {
1935 CKEDITOR.document.removeListener( 'mouseup', mouseUpHandler );
1936 CKEDITOR.document.removeListener( 'mousemove', mouseMoveHandler );
1937
1938 if ( dialogCover )
1939 {
1940 dialogCover.remove();
1941 dialogCover = null;
1942 }
1943
1944 if ( CKEDITOR.env.ie6Compat )
1945 {
1946 var coverDoc = currentCover.getChild( 0 ).getFrameDocument();
1947 coverDoc.removeListener( 'mouseup', mouseUpHandler );
1948 coverDoc.removeListener( 'mousemove', mouseMoveHandler );
1949 }
1950 }
1951 }
1952
1953 var resizeCover;
1954 // Caching resuable covers and allowing only one cover
1955 // on screen.
1956 var covers = {},
1957 currentCover;
1958
1959 function cancelEvent( ev )
1960 {
1961 ev.data.preventDefault(1);
1962 }
1963
1964 function showCover( editor )
1965 {
1966 var win = CKEDITOR.document.getWindow();
1967 var config = editor.config,
1968 backgroundColorStyle = config.dialog_backgroundCoverColor || 'white',
1969 backgroundCoverOpacity = config.dialog_backgroundCoverOpacity,
1970 baseFloatZIndex = config.baseFloatZIndex,
1971 coverKey = CKEDITOR.tools.genKey(
1972 backgroundColorStyle,
1973 backgroundCoverOpacity,
1974 baseFloatZIndex ),
1975 coverElement = covers[ coverKey ];
1976
1977 if ( !coverElement )
1978 {
1979 var html = [
1980 '<div tabIndex="-1" style="position: ', ( CKEDITOR.env.ie6Compat ? 'absolute' : 'fixed' ),
1981 '; z-index: ', baseFloatZIndex,
1982 '; top: 0px; left: 0px; ',
1983 ( !CKEDITOR.env.ie6Compat ? 'background-color: ' + backgroundColorStyle : '' ),
1984 '" class="cke_dialog_background_cover">'
1985 ];
1986
1987 if ( CKEDITOR.env.ie6Compat )
1988 {
1989 // Support for custom document.domain in IE.
1990 var isCustomDomain = CKEDITOR.env.isCustomDomain(),
1991 iframeHtml = '<html><body style=\\\'background-color:' + backgroundColorStyle + ';\\\'></body></html>';
1992
1993 html.push(
1994 '<iframe' +
1995 ' hidefocus="true"' +
1996 ' frameborder="0"' +
1997 ' id="cke_dialog_background_iframe"' +
1998 ' src="javascript:' );
1999
2000 html.push( 'void((function(){' +
2001 'document.open();' +
2002 ( isCustomDomain ? 'document.domain=\'' + document.domain + '\';' : '' ) +
2003 'document.write( \'' + iframeHtml + '\' );' +
2004 'document.close();' +
2005 '})())' );
2006
2007 html.push(
2008 '"' +
2009 ' style="' +
2010 'position:absolute;' +
2011 'left:0;' +
2012 'top:0;' +
2013 'width:100%;' +
2014 'height: 100%;' +
2015 'progid:DXImageTransform.Microsoft.Alpha(opacity=0)">' +
2016 '</iframe>' );
2017 }
2018
2019 html.push( '</div>' );
2020
2021 coverElement = CKEDITOR.dom.element.createFromHtml( html.join( '' ) );
2022 coverElement.setOpacity( backgroundCoverOpacity != undefined ? backgroundCoverOpacity : 0.5 );
2023
2024 coverElement.on( 'keydown', cancelEvent );
2025 coverElement.on( 'keypress', cancelEvent );
2026 coverElement.on( 'keyup', cancelEvent );
2027
2028 coverElement.appendTo( CKEDITOR.document.getBody() );
2029 covers[ coverKey ] = coverElement;
2030 }
2031 else
2032 coverElement. show();
2033
2034 currentCover = coverElement;
2035 var resizeFunc = function()
2036 {
2037 var size = win.getViewPaneSize();
2038 coverElement.setStyles(
2039 {
2040 width : size.width + 'px',
2041 height : size.height + 'px'
2042 } );
2043 };
2044
2045 var scrollFunc = function()
2046 {
2047 var pos = win.getScrollPosition(),
2048 cursor = CKEDITOR.dialog._.currentTop;
2049 coverElement.setStyles(
2050 {
2051 left : pos.x + 'px',
2052 top : pos.y + 'px'
2053 });
2054
2055 if ( cursor )
2056 {
2057 do
2058 {
2059 var dialogPos = cursor.getPosition();
2060 cursor.move( dialogPos.x, dialogPos.y );
2061 } while ( ( cursor = cursor._.parentDialog ) );
2062 }
2063 };
2064
2065 resizeCover = resizeFunc;
2066 win.on( 'resize', resizeFunc );
2067 resizeFunc();
2068 // Using Safari/Mac, focus must be kept where it is (#7027)
2069 if ( !( CKEDITOR.env.mac && CKEDITOR.env.webkit ) )
2070 coverElement.focus();
2071
2072 if ( CKEDITOR.env.ie6Compat )
2073 {
2074 // IE BUG: win.$.onscroll assignment doesn't work.. it must be window.onscroll.
2075 // So we need to invent a really funny way to make it work.
2076 var myScrollHandler = function()
2077 {
2078 scrollFunc();
2079 arguments.callee.prevScrollHandler.apply( this, arguments );
2080 };
2081 win.$.setTimeout( function()
2082 {
2083 myScrollHandler.prevScrollHandler = window.onscroll || function(){};
2084 window.onscroll = myScrollHandler;
2085 }, 0 );
2086 scrollFunc();
2087 }
2088 }
2089
2090 function hideCover()
2091 {
2092 if ( !currentCover )
2093 return;
2094
2095 var win = CKEDITOR.document.getWindow();
2096 currentCover.hide();
2097 win.removeListener( 'resize', resizeCover );
2098
2099 if ( CKEDITOR.env.ie6Compat )
2100 {
2101 win.$.setTimeout( function()
2102 {
2103 var prevScrollHandler = window.onscroll && window.onscroll.prevScrollHandler;
2104 window.onscroll = prevScrollHandler || null;
2105 }, 0 );
2106 }
2107 resizeCover = null;
2108 }
2109
2110 function removeCovers()
2111 {
2112 for ( var coverId in covers )
2113 covers[ coverId ].remove();
2114 covers = {};
2115 }
2116
2117 var accessKeyProcessors = {};
2118
2119 var accessKeyDownHandler = function( evt )
2120 {
2121 var ctrl = evt.data.$.ctrlKey || evt.data.$.metaKey,
2122 alt = evt.data.$.altKey,
2123 shift = evt.data.$.shiftKey,
2124 key = String.fromCharCode( evt.data.$.keyCode ),
2125 keyProcessor = accessKeyProcessors[( ctrl ? 'CTRL+' : '' ) + ( alt ? 'ALT+' : '') + ( shift ? 'SHIFT+' : '' ) + key];
2126
2127 if ( !keyProcessor || !keyProcessor.length )
2128 return;
2129
2130 keyProcessor = keyProcessor[keyProcessor.length - 1];
2131 keyProcessor.keydown && keyProcessor.keydown.call( keyProcessor.uiElement, keyProcessor.dialog, keyProcessor.key );
2132 evt.data.preventDefault();
2133 };
2134
2135 var accessKeyUpHandler = function( evt )
2136 {
2137 var ctrl = evt.data.$.ctrlKey || evt.data.$.metaKey,
2138 alt = evt.data.$.altKey,
2139 shift = evt.data.$.shiftKey,
2140 key = String.fromCharCode( evt.data.$.keyCode ),
2141 keyProcessor = accessKeyProcessors[( ctrl ? 'CTRL+' : '' ) + ( alt ? 'ALT+' : '') + ( shift ? 'SHIFT+' : '' ) + key];
2142
2143 if ( !keyProcessor || !keyProcessor.length )
2144 return;
2145
2146 keyProcessor = keyProcessor[keyProcessor.length - 1];
2147 if ( keyProcessor.keyup )
2148 {
2149 keyProcessor.keyup.call( keyProcessor.uiElement, keyProcessor.dialog, keyProcessor.key );
2150 evt.data.preventDefault();
2151 }
2152 };
2153
2154 var registerAccessKey = function( uiElement, dialog, key, downFunc, upFunc )
2155 {
2156 var procList = accessKeyProcessors[key] || ( accessKeyProcessors[key] = [] );
2157 procList.push( {
2158 uiElement : uiElement,
2159 dialog : dialog,
2160 key : key,
2161 keyup : upFunc || uiElement.accessKeyUp,
2162 keydown : downFunc || uiElement.accessKeyDown
2163 } );
2164 };
2165
2166 var unregisterAccessKey = function( obj )
2167 {
2168 for ( var i in accessKeyProcessors )
2169 {
2170 var list = accessKeyProcessors[i];
2171 for ( var j = list.length - 1 ; j >= 0 ; j-- )
2172 {
2173 if ( list[j].dialog == obj || list[j].uiElement == obj )
2174 list.splice( j, 1 );
2175 }
2176 if ( list.length === 0 )
2177 delete accessKeyProcessors[i];
2178 }
2179 };
2180
2181 var tabAccessKeyUp = function( dialog, key )
2182 {
2183 if ( dialog._.accessKeyMap[key] )
2184 dialog.selectPage( dialog._.accessKeyMap[key] );
2185 };
2186
2187 var tabAccessKeyDown = function( dialog, key )
2188 {
2189 };
2190
2191 // ESC, ENTER
2192 var preventKeyBubblingKeys = { 27 :1, 13 :1 };
2193 var preventKeyBubbling = function( e )
2194 {
2195 if ( e.data.getKeystroke() in preventKeyBubblingKeys )
2196 e.data.stopPropagation();
2197 };
2198
2199 (function()
2200 {
2201 CKEDITOR.ui.dialog =
2202 {
2203 /**
2204 * The base class of all dialog UI elements.
2205 * @constructor
2206 * @param {CKEDITOR.dialog} dialog Parent dialog object.
2207 * @param {CKEDITOR.dialog.definition.uiElement} elementDefinition Element
2208 * definition. Accepted fields:
2209 * <ul>
2210 * <li><strong>id</strong> (Required) The id of the UI element. See {@link
2211 * CKEDITOR.dialog#getContentElement}</li>
2212 * <li><strong>type</strong> (Required) The type of the UI element. The
2213 * value to this field specifies which UI element class will be used to
2214 * generate the final widget.</li>
2215 * <li><strong>title</strong> (Optional) The popup tooltip for the UI
2216 * element.</li>
2217 * <li><strong>hidden</strong> (Optional) A flag that tells if the element
2218 * should be initially visible.</li>
2219 * <li><strong>className</strong> (Optional) Additional CSS class names
2220 * to add to the UI element. Separated by space.</li>
2221 * <li><strong>style</strong> (Optional) Additional CSS inline styles
2222 * to add to the UI element. A semicolon (;) is required after the last
2223 * style declaration.</li>
2224 * <li><strong>accessKey</strong> (Optional) The alphanumeric access key
2225 * for this element. Access keys are automatically prefixed by CTRL.</li>
2226 * <li><strong>on*</strong> (Optional) Any UI element definition field that
2227 * starts with <em>on</em> followed immediately by a capital letter and
2228 * probably more letters is an event handler. Event handlers may be further
2229 * divided into registered event handlers and DOM event handlers. Please
2230 * refer to {@link CKEDITOR.ui.dialog.uiElement#registerEvents} and
2231 * {@link CKEDITOR.ui.dialog.uiElement#eventProcessors} for more
2232 * information.</li>
2233 * </ul>
2234 * @param {Array} htmlList
2235 * List of HTML code to be added to the dialog's content area.
2236 * @param {Function|String} nodeNameArg
2237 * A function returning a string, or a simple string for the node name for
2238 * the root DOM node. Default is 'div'.
2239 * @param {Function|Object} stylesArg
2240 * A function returning an object, or a simple object for CSS styles applied
2241 * to the DOM node. Default is empty object.
2242 * @param {Function|Object} attributesArg
2243 * A fucntion returning an object, or a simple object for attributes applied
2244 * to the DOM node. Default is empty object.
2245 * @param {Function|String} contentsArg
2246 * A function returning a string, or a simple string for the HTML code inside
2247 * the root DOM node. Default is empty string.
2248 * @example
2249 */
2250 uiElement : function( dialog, elementDefinition, htmlList, nodeNameArg, stylesArg, attributesArg, contentsArg )
2251 {
2252 if ( arguments.length < 4 )
2253 return;
2254
2255 var nodeName = ( nodeNameArg.call ? nodeNameArg( elementDefinition ) : nodeNameArg ) || 'div',
2256 html = [ '<', nodeName, ' ' ],
2257 styles = ( stylesArg && stylesArg.call ? stylesArg( elementDefinition ) : stylesArg ) || {},
2258 attributes = ( attributesArg && attributesArg.call ? attributesArg( elementDefinition ) : attributesArg ) || {},
2259 innerHTML = ( contentsArg && contentsArg.call ? contentsArg.call( this, dialog, elementDefinition ) : contentsArg ) || '',
2260 domId = this.domId = attributes.id || CKEDITOR.tools.getNextId() + '_uiElement',
2261 id = this.id = elementDefinition.id,
2262 i;
2263
2264 // Set the id, a unique id is required for getElement() to work.
2265 attributes.id = domId;
2266
2267 // Set the type and definition CSS class names.
2268 var classes = {};
2269 if ( elementDefinition.type )
2270 classes[ 'cke_dialog_ui_' + elementDefinition.type ] = 1;
2271 if ( elementDefinition.className )
2272 classes[ elementDefinition.className ] = 1;
2273 if ( elementDefinition.disabled )
2274 classes[ 'cke_disabled' ] = 1;
2275
2276 var attributeClasses = ( attributes['class'] && attributes['class'].split ) ? attributes['class'].split( ' ' ) : [];
2277 for ( i = 0 ; i < attributeClasses.length ; i++ )
2278 {
2279 if ( attributeClasses[i] )
2280 classes[ attributeClasses[i] ] = 1;
2281 }
2282 var finalClasses = [];
2283 for ( i in classes )
2284 finalClasses.push( i );
2285 attributes['class'] = finalClasses.join( ' ' );
2286
2287 // Set the popup tooltop.
2288 if ( elementDefinition.title )
2289 attributes.title = elementDefinition.title;
2290
2291 // Write the inline CSS styles.
2292 var styleStr = ( elementDefinition.style || '' ).split( ';' );
2293
2294 // Element alignment support.
2295 if ( elementDefinition.align )
2296 {
2297 var align = elementDefinition.align;
2298 styles[ 'margin-left' ] = align == 'left' ? 0 : 'auto';
2299 styles[ 'margin-right' ] = align == 'right' ? 0 : 'auto';
2300 }
2301
2302 for ( i in styles )
2303 styleStr.push( i + ':' + styles[i] );
2304 if ( elementDefinition.hidden )
2305 styleStr.push( 'display:none' );
2306 for ( i = styleStr.length - 1 ; i >= 0 ; i-- )
2307 {
2308 if ( styleStr[i] === '' )
2309 styleStr.splice( i, 1 );
2310 }
2311 if ( styleStr.length > 0 )
2312 attributes.style = ( attributes.style ? ( attributes.style + '; ' ) : '' ) + styleStr.join( '; ' );
2313
2314 // Write the attributes.
2315 for ( i in attributes )
2316 html.push( i + '="' + CKEDITOR.tools.htmlEncode( attributes[i] ) + '" ');
2317
2318 // Write the content HTML.
2319 html.push( '>', innerHTML, '</', nodeName, '>' );
2320
2321 // Add contents to the parent HTML array.
2322 htmlList.push( html.join( '' ) );
2323
2324 ( this._ || ( this._ = {} ) ).dialog = dialog;
2325
2326 // Override isChanged if it is defined in element definition.
2327 if ( typeof( elementDefinition.isChanged ) == 'boolean' )
2328 this.isChanged = function(){ return elementDefinition.isChanged; };
2329 if ( typeof( elementDefinition.isChanged ) == 'function' )
2330 this.isChanged = elementDefinition.isChanged;
2331
2332 // Overload 'get(set)Value' on definition.
2333 if ( typeof( elementDefinition.setValue ) == 'function' )
2334 {
2335 this.setValue = CKEDITOR.tools.override( this.setValue, function( org )
2336 {
2337 return function( val ){ org.call( this, elementDefinition.setValue.call( this, val ) ); };
2338 } );
2339 }
2340
2341 if ( typeof( elementDefinition.getValue ) == 'function' )
2342 {
2343 this.getValue = CKEDITOR.tools.override( this.getValue, function( org )
2344 {
2345 return function(){ return elementDefinition.getValue.call( this, org.call( this ) ); };
2346 } );
2347 }
2348
2349 // Add events.
2350 CKEDITOR.event.implementOn( this );
2351
2352 this.registerEvents( elementDefinition );
2353 if ( this.accessKeyUp && this.accessKeyDown && elementDefinition.accessKey )
2354 registerAccessKey( this, dialog, 'CTRL+' + elementDefinition.accessKey );
2355
2356 var me = this;
2357 dialog.on( 'load', function()
2358 {
2359 if ( me.getInputElement() )
2360 {
2361 me.getInputElement().on( 'focus', function()
2362 {
2363 dialog._.tabBarMode = false;
2364 dialog._.hasFocus = true;
2365 me.fire( 'focus' );
2366 }, me );
2367 }
2368 } );
2369
2370 // Register the object as a tab focus if it can be included.
2371 if ( this.keyboardFocusable )
2372 {
2373 this.tabIndex = elementDefinition.tabIndex || 0;
2374
2375 this.focusIndex = dialog._.focusList.push( this ) - 1;
2376 this.on( 'focus', function()
2377 {
2378 dialog._.currentFocusIndex = me.focusIndex;
2379 } );
2380 }
2381
2382 // Completes this object with everything we have in the
2383 // definition.
2384 CKEDITOR.tools.extend( this, elementDefinition );
2385 },
2386
2387 /**
2388 * Horizontal layout box for dialog UI elements, auto-expends to available width of container.
2389 * @constructor
2390 * @extends CKEDITOR.ui.dialog.uiElement
2391 * @param {CKEDITOR.dialog} dialog
2392 * Parent dialog object.
2393 * @param {Array} childObjList
2394 * Array of {@link CKEDITOR.ui.dialog.uiElement} objects inside this
2395 * container.
2396 * @param {Array} childHtmlList
2397 * Array of HTML code that correspond to the HTML output of all the
2398 * objects in childObjList.
2399 * @param {Array} htmlList
2400 * Array of HTML code that this element will output to.
2401 * @param {CKEDITOR.dialog.definition.uiElement} elementDefinition
2402 * The element definition. Accepted fields:
2403 * <ul>
2404 * <li><strong>widths</strong> (Optional) The widths of child cells.</li>
2405 * <li><strong>height</strong> (Optional) The height of the layout.</li>
2406 * <li><strong>padding</strong> (Optional) The padding width inside child
2407 * cells.</li>
2408 * <li><strong>align</strong> (Optional) The alignment of the whole layout
2409 * </li>
2410 * </ul>
2411 * @example
2412 */
2413 hbox : function( dialog, childObjList, childHtmlList, htmlList, elementDefinition )
2414 {
2415 if ( arguments.length < 4 )
2416 return;
2417
2418 this._ || ( this._ = {} );
2419
2420 var children = this._.children = childObjList,
2421 widths = elementDefinition && elementDefinition.widths || null,
2422 height = elementDefinition && elementDefinition.height || null,
2423 styles = {},
2424 i;
2425 /** @ignore */
2426 var innerHTML = function()
2427 {
2428 var html = [ '<tbody><tr class="cke_dialog_ui_hbox">' ];
2429 for ( i = 0 ; i < childHtmlList.length ; i++ )
2430 {
2431 var className = 'cke_dialog_ui_hbox_child',
2432 styles = [];
2433 if ( i === 0 )
2434 className = 'cke_dialog_ui_hbox_first';
2435 if ( i == childHtmlList.length - 1 )
2436 className = 'cke_dialog_ui_hbox_last';
2437 html.push( '<td class="', className, '" role="presentation" ' );
2438 if ( widths )
2439 {
2440 if ( widths[i] )
2441 styles.push( 'width:' + cssLength( widths[i] ) );
2442 }
2443 else
2444 styles.push( 'width:' + Math.floor( 100 / childHtmlList.length ) + '%' );
2445 if ( height )
2446 styles.push( 'height:' + cssLength( height ) );
2447 if ( elementDefinition && elementDefinition.padding != undefined )
2448 styles.push( 'padding:' + cssLength( elementDefinition.padding ) );
2449 // In IE Quirks alignment has to be done on table cells. (#7324)
2450 if ( CKEDITOR.env.ie && CKEDITOR.env.quirks && children[ i ].align )
2451 styles.push( 'text-align:' + children[ i ].align );
2452 if ( styles.length > 0 )
2453 html.push( 'style="' + styles.join('; ') + '" ' );
2454 html.push( '>', childHtmlList[i], '</td>' );
2455 }
2456 html.push( '</tr></tbody>' );
2457 return html.join( '' );
2458 };
2459
2460 var attribs = { role : 'presentation' };
2461 elementDefinition && elementDefinition.align && ( attribs.align = elementDefinition.align );
2462
2463 CKEDITOR.ui.dialog.uiElement.call(
2464 this,
2465 dialog,
2466 elementDefinition || { type : 'hbox' },
2467 htmlList,
2468 'table',
2469 styles,
2470 attribs,
2471 innerHTML );
2472 },
2473
2474 /**
2475 * Vertical layout box for dialog UI elements.
2476 * @constructor
2477 * @extends CKEDITOR.ui.dialog.hbox
2478 * @param {CKEDITOR.dialog} dialog
2479 * Parent dialog object.
2480 * @param {Array} childObjList
2481 * Array of {@link CKEDITOR.ui.dialog.uiElement} objects inside this
2482 * container.
2483 * @param {Array} childHtmlList
2484 * Array of HTML code that correspond to the HTML output of all the
2485 * objects in childObjList.
2486 * @param {Array} htmlList
2487 * Array of HTML code that this element will output to.
2488 * @param {CKEDITOR.dialog.definition.uiElement} elementDefinition
2489 * The element definition. Accepted fields:
2490 * <ul>
2491 * <li><strong>width</strong> (Optional) The width of the layout.</li>
2492 * <li><strong>heights</strong> (Optional) The heights of individual cells.
2493 * </li>
2494 * <li><strong>align</strong> (Optional) The alignment of the layout.</li>
2495 * <li><strong>padding</strong> (Optional) The padding width inside child
2496 * cells.</li>
2497 * <li><strong>expand</strong> (Optional) Whether the layout should expand
2498 * vertically to fill its container.</li>
2499 * </ul>
2500 * @example
2501 */
2502 vbox : function( dialog, childObjList, childHtmlList, htmlList, elementDefinition )
2503 {
2504 if ( arguments.length < 3 )
2505 return;
2506
2507 this._ || ( this._ = {} );
2508
2509 var children = this._.children = childObjList,
2510 width = elementDefinition && elementDefinition.width || null,
2511 heights = elementDefinition && elementDefinition.heights || null;
2512 /** @ignore */
2513 var innerHTML = function()
2514 {
2515 var html = [ '<table role="presentation" cellspacing="0" border="0" ' ];
2516 html.push( 'style="' );
2517 if ( elementDefinition && elementDefinition.expand )
2518 html.push( 'height:100%;' );
2519 html.push( 'width:' + cssLength( width || '100%' ), ';' );
2520 html.push( '"' );
2521 html.push( 'align="', CKEDITOR.tools.htmlEncode(
2522 ( elementDefinition && elementDefinition.align ) || ( dialog.getParentEditor().lang.dir == 'ltr' ? 'left' : 'right' ) ), '" ' );
2523
2524 html.push( '><tbody>' );
2525 for ( var i = 0 ; i < childHtmlList.length ; i++ )
2526 {
2527 var styles = [];
2528 html.push( '<tr><td role="presentation" ' );
2529 if ( width )
2530 styles.push( 'width:' + cssLength( width || '100%' ) );
2531 if ( heights )
2532 styles.push( 'height:' + cssLength( heights[i] ) );
2533 else if ( elementDefinition && elementDefinition.expand )
2534 styles.push( 'height:' + Math.floor( 100 / childHtmlList.length ) + '%' );
2535 if ( elementDefinition && elementDefinition.padding != undefined )
2536 styles.push( 'padding:' + cssLength( elementDefinition.padding ) );
2537 // In IE Quirks alignment has to be done on table cells. (#7324)
2538 if ( CKEDITOR.env.ie && CKEDITOR.env.quirks && children[ i ].align )
2539 styles.push( 'text-align:' + children[ i ].align );
2540 if ( styles.length > 0 )
2541 html.push( 'style="', styles.join( '; ' ), '" ' );
2542 html.push( ' class="cke_dialog_ui_vbox_child">', childHtmlList[i], '</td></tr>' );
2543 }
2544 html.push( '</tbody></table>' );
2545 return html.join( '' );
2546 };
2547 CKEDITOR.ui.dialog.uiElement.call( this, dialog, elementDefinition || { type : 'vbox' }, htmlList, 'div', null, { role : 'presentation' }, innerHTML );
2548 }
2549 };
2550 })();
2551
2552 CKEDITOR.ui.dialog.uiElement.prototype =
2553 {
2554 /**
2555 * Gets the root DOM element of this dialog UI object.
2556 * @returns {CKEDITOR.dom.element} Root DOM element of UI object.
2557 * @example
2558 * uiElement.getElement().hide();
2559 */
2560 getElement : function()
2561 {
2562 return CKEDITOR.document.getById( this.domId );
2563 },
2564
2565 /**
2566 * Gets the DOM element that the user inputs values.
2567 * This function is used by setValue(), getValue() and focus(). It should
2568 * be overrided in child classes where the input element isn't the root
2569 * element.
2570 * @returns {CKEDITOR.dom.element} The element where the user input values.
2571 * @example
2572 * var rawValue = textInput.getInputElement().$.value;
2573 */
2574 getInputElement : function()
2575 {
2576 return this.getElement();
2577 },
2578
2579 /**
2580 * Gets the parent dialog object containing this UI element.
2581 * @returns {CKEDITOR.dialog} Parent dialog object.
2582 * @example
2583 * var dialog = uiElement.getDialog();
2584 */
2585 getDialog : function()
2586 {
2587 return this._.dialog;
2588 },
2589
2590 /**
2591 * Sets the value of this dialog UI object.
2592 * @param {Object} value The new value.
2593 * @param {Boolean} noChangeEvent Internal commit, to supress 'change' event on this element.
2594 * @returns {CKEDITOR.dialog.uiElement} The current UI element.
2595 * @example
2596 * uiElement.setValue( 'Dingo' );
2597 */
2598 setValue : function( value, noChangeEvent )
2599 {
2600 this.getInputElement().setValue( value );
2601 !noChangeEvent && this.fire( 'change', { value : value } );
2602 return this;
2603 },
2604
2605 /**
2606 * Gets the current value of this dialog UI object.
2607 * @returns {Object} The current value.
2608 * @example
2609 * var myValue = uiElement.getValue();
2610 */
2611 getValue : function()
2612 {
2613 return this.getInputElement().getValue();
2614 },
2615
2616 /**
2617 * Tells whether the UI object's value has changed.
2618 * @returns {Boolean} true if changed, false if not changed.
2619 * @example
2620 * if ( uiElement.isChanged() )
2621 * &nbsp;&nbsp;confirm( 'Value changed! Continue?' );
2622 */
2623 isChanged : function()
2624 {
2625 // Override in input classes.
2626 return false;
2627 },
2628
2629 /**
2630 * Selects the parent tab of this element. Usually called by focus() or overridden focus() methods.
2631 * @returns {CKEDITOR.dialog.uiElement} The current UI element.
2632 * @example
2633 * focus : function()
2634 * {
2635 * this.selectParentTab();
2636 * // do something else.
2637 * }
2638 */
2639 selectParentTab : function()
2640 {
2641 var element = this.getInputElement(),
2642 cursor = element,
2643 tabId;
2644 while ( ( cursor = cursor.getParent() ) && cursor.$.className.search( 'cke_dialog_page_contents' ) == -1 )
2645 { /*jsl:pass*/ }
2646
2647 // Some widgets don't have parent tabs (e.g. OK and Cancel buttons).
2648 if ( !cursor )
2649 return this;
2650
2651 tabId = cursor.getAttribute( 'name' );
2652 // Avoid duplicate select.
2653 if ( this._.dialog._.currentTabId != tabId )
2654 this._.dialog.selectPage( tabId );
2655 return this;
2656 },
2657
2658 /**
2659 * Puts the focus to the UI object. Switches tabs if the UI object isn't in the active tab page.
2660 * @returns {CKEDITOR.dialog.uiElement} The current UI element.
2661 * @example
2662 * uiElement.focus();
2663 */
2664 focus : function()
2665 {
2666 this.selectParentTab().getInputElement().focus();
2667 return this;
2668 },
2669
2670 /**
2671 * Registers the on* event handlers defined in the element definition.
2672 * The default behavior of this function is:
2673 * <ol>
2674 * <li>
2675 * If the on* event is defined in the class's eventProcesors list,
2676 * then the registration is delegated to the corresponding function
2677 * in the eventProcessors list.
2678 * </li>
2679 * <li>
2680 * If the on* event is not defined in the eventProcessors list, then
2681 * register the event handler under the corresponding DOM event of
2682 * the UI element's input DOM element (as defined by the return value
2683 * of {@link CKEDITOR.ui.dialog.uiElement#getInputElement}).
2684 * </li>
2685 * </ol>
2686 * This function is only called at UI element instantiation, but can
2687 * be overridded in child classes if they require more flexibility.
2688 * @param {CKEDITOR.dialog.definition.uiElement} definition The UI element
2689 * definition.
2690 * @returns {CKEDITOR.dialog.uiElement} The current UI element.
2691 * @example
2692 */
2693 registerEvents : function( definition )
2694 {
2695 var regex = /^on([A-Z]\w+)/,
2696 match;
2697
2698 var registerDomEvent = function( uiElement, dialog, eventName, func )
2699 {
2700 dialog.on( 'load', function()
2701 {
2702 uiElement.getInputElement().on( eventName, func, uiElement );
2703 });
2704 };
2705
2706 for ( var i in definition )
2707 {
2708 if ( !( match = i.match( regex ) ) )
2709 continue;
2710 if ( this.eventProcessors[i] )
2711 this.eventProcessors[i].call( this, this._.dialog, definition[i] );
2712 else
2713 registerDomEvent( this, this._.dialog, match[1].toLowerCase(), definition[i] );
2714 }
2715
2716 return this;
2717 },
2718
2719 /**
2720 * The event processor list used by
2721 * {@link CKEDITOR.ui.dialog.uiElement#getInputElement} at UI element
2722 * instantiation. The default list defines three on* events:
2723 * <ol>
2724 * <li>onLoad - Called when the element's parent dialog opens for the
2725 * first time</li>
2726 * <li>onShow - Called whenever the element's parent dialog opens.</li>
2727 * <li>onHide - Called whenever the element's parent dialog closes.</li>
2728 * </ol>
2729 * @field
2730 * @type Object
2731 * @example
2732 * // This connects the 'click' event in CKEDITOR.ui.dialog.button to onClick
2733 * // handlers in the UI element's definitions.
2734 * CKEDITOR.ui.dialog.button.eventProcessors = CKEDITOR.tools.extend( {},
2735 * &nbsp;&nbsp;CKEDITOR.ui.dialog.uiElement.prototype.eventProcessors,
2736 * &nbsp;&nbsp;{ onClick : function( dialog, func ) { this.on( 'click', func ); } },
2737 * &nbsp;&nbsp;true );
2738 */
2739 eventProcessors :
2740 {
2741 onLoad : function( dialog, func )
2742 {
2743 dialog.on( 'load', func, this );
2744 },
2745
2746 onShow : function( dialog, func )
2747 {
2748 dialog.on( 'show', func, this );
2749 },
2750
2751 onHide : function( dialog, func )
2752 {
2753 dialog.on( 'hide', func, this );
2754 }
2755 },
2756
2757 /**
2758 * The default handler for a UI element's access key down event, which
2759 * tries to put focus to the UI element.<br />
2760 * Can be overridded in child classes for more sophisticaed behavior.
2761 * @param {CKEDITOR.dialog} dialog The parent dialog object.
2762 * @param {String} key The key combination pressed. Since access keys
2763 * are defined to always include the CTRL key, its value should always
2764 * include a 'CTRL+' prefix.
2765 * @example
2766 */
2767 accessKeyDown : function( dialog, key )
2768 {
2769 this.focus();
2770 },
2771
2772 /**
2773 * The default handler for a UI element's access key up event, which
2774 * does nothing.<br />
2775 * Can be overridded in child classes for more sophisticated behavior.
2776 * @param {CKEDITOR.dialog} dialog The parent dialog object.
2777 * @param {String} key The key combination pressed. Since access keys
2778 * are defined to always include the CTRL key, its value should always
2779 * include a 'CTRL+' prefix.
2780 * @example
2781 */
2782 accessKeyUp : function( dialog, key )
2783 {
2784 },
2785
2786 /**
2787 * Disables a UI element.
2788 * @example
2789 */
2790 disable : function()
2791 {
2792 var element = this.getElement(),
2793 input = this.getInputElement();
2794 input.setAttribute( 'disabled', 'true' );
2795 element.addClass( 'cke_disabled' );
2796 },
2797
2798 /**
2799 * Enables a UI element.
2800 * @example
2801 */
2802 enable : function()
2803 {
2804 var element = this.getElement(),
2805 input = this.getInputElement();
2806 input.removeAttribute( 'disabled' );
2807 element.removeClass( 'cke_disabled' );
2808 },
2809
2810 /**
2811 * Determines whether an UI element is enabled or not.
2812 * @returns {Boolean} Whether the UI element is enabled.
2813 * @example
2814 */
2815 isEnabled : function()
2816 {
2817 return !this.getElement().hasClass( 'cke_disabled' );
2818 },
2819
2820 /**
2821 * Determines whether an UI element is visible or not.
2822 * @returns {Boolean} Whether the UI element is visible.
2823 * @example
2824 */
2825 isVisible : function()
2826 {
2827 return this.getInputElement().isVisible();
2828 },
2829
2830 /**
2831 * Determines whether an UI element is focus-able or not.
2832 * Focus-able is defined as being both visible and enabled.
2833 * @returns {Boolean} Whether the UI element can be focused.
2834 * @example
2835 */
2836 isFocusable : function()
2837 {
2838 if ( !this.isEnabled() || !this.isVisible() )
2839 return false;
2840 return true;
2841 }
2842 };
2843
2844 CKEDITOR.ui.dialog.hbox.prototype = CKEDITOR.tools.extend( new CKEDITOR.ui.dialog.uiElement,
2845 /**
2846 * @lends CKEDITOR.ui.dialog.hbox.prototype
2847 */
2848 {
2849 /**
2850 * Gets a child UI element inside this container.
2851 * @param {Array|Number} indices An array or a single number to indicate the child's
2852 * position in the container's descendant tree. Omit to get all the children in an array.
2853 * @returns {Array|CKEDITOR.ui.dialog.uiElement} Array of all UI elements in the container
2854 * if no argument given, or the specified UI element if indices is given.
2855 * @example
2856 * var checkbox = hbox.getChild( [0,1] );
2857 * checkbox.setValue( true );
2858 */
2859 getChild : function( indices )
2860 {
2861 // If no arguments, return a clone of the children array.
2862 if ( arguments.length < 1 )
2863 return this._.children.concat();
2864
2865 // If indices isn't array, make it one.
2866 if ( !indices.splice )
2867 indices = [ indices ];
2868
2869 // Retrieve the child element according to tree position.
2870 if ( indices.length < 2 )
2871 return this._.children[ indices[0] ];
2872 else
2873 return ( this._.children[ indices[0] ] && this._.children[ indices[0] ].getChild ) ?
2874 this._.children[ indices[0] ].getChild( indices.slice( 1, indices.length ) ) :
2875 null;
2876 }
2877 }, true );
2878
2879 CKEDITOR.ui.dialog.vbox.prototype = new CKEDITOR.ui.dialog.hbox();
2880
2881
2882
2883 (function()
2884 {
2885 var commonBuilder = {
2886 build : function( dialog, elementDefinition, output )
2887 {
2888 var children = elementDefinition.children,
2889 child,
2890 childHtmlList = [],
2891 childObjList = [];
2892 for ( var i = 0 ; ( i < children.length && ( child = children[i] ) ) ; i++ )
2893 {
2894 var childHtml = [];
2895 childHtmlList.push( childHtml );
2896 childObjList.push( CKEDITOR.dialog._.uiElementBuilders[ child.type ].build( dialog, child, childHtml ) );
2897 }
2898 return new CKEDITOR.ui.dialog[elementDefinition.type]( dialog, childObjList, childHtmlList, output, elementDefinition );
2899 }
2900 };
2901
2902 CKEDITOR.dialog.addUIElement( 'hbox', commonBuilder );
2903 CKEDITOR.dialog.addUIElement( 'vbox', commonBuilder );
2904 })();
2905
2906 /**
2907 * Generic dialog command. It opens a specific dialog when executed.
2908 * @constructor
2909 * @augments CKEDITOR.commandDefinition
2910 * @param {string} dialogName The name of the dialog to open when executing
2911 * this command.
2912 * @example
2913 * // Register the "link" command, which opens the "link" dialog.
2914 * editor.addCommand( 'link', <b>new CKEDITOR.dialogCommand( 'link' )</b> );
2915 */
2916 CKEDITOR.dialogCommand = function( dialogName )
2917 {
2918 this.dialogName = dialogName;
2919 };
2920
2921 CKEDITOR.dialogCommand.prototype =
2922 {
2923 /** @ignore */
2924 exec : function( editor )
2925 {
2926 editor.openDialog( this.dialogName );
2927 },
2928
2929 // Dialog commands just open a dialog ui, thus require no undo logic,
2930 // undo support should dedicate to specific dialog implementation.
2931 canUndo: false,
2932
2933 editorFocus : CKEDITOR.env.ie || CKEDITOR.env.webkit
2934 };
2935
2936 (function()
2937 {
2938 var notEmptyRegex = /^([a]|[^a])+$/,
2939 integerRegex = /^\d*$/,
2940 numberRegex = /^\d*(?:\.\d+)?$/,
2941 htmlLengthRegex = /^(((\d*(\.\d+))|(\d*))(px|\%)?)?$/,
2942 cssLengthRegex = /^(((\d*(\.\d+))|(\d*))(px|em|ex|in|cm|mm|pt|pc|\%)?)?$/i;
2943
2944 CKEDITOR.VALIDATE_OR = 1;
2945 CKEDITOR.VALIDATE_AND = 2;
2946
2947 CKEDITOR.dialog.validate =
2948 {
2949 functions : function()
2950 {
2951 var args = arguments;
2952 return function()
2953 {
2954 /**
2955 * It's important for validate functions to be able to accept the value
2956 * as argument in addition to this.getValue(), so that it is possible to
2957 * combine validate functions together to make more sophisticated
2958 * validators.
2959 */
2960 var value = this && this.getValue ? this.getValue() : args[ 0 ];
2961
2962 var msg = undefined,
2963 relation = CKEDITOR.VALIDATE_AND,
2964 functions = [], i;
2965
2966 for ( i = 0 ; i < args.length ; i++ )
2967 {
2968 if ( typeof( args[i] ) == 'function' )
2969 functions.push( args[i] );
2970 else
2971 break;
2972 }
2973
2974 if ( i < args.length && typeof( args[i] ) == 'string' )
2975 {
2976 msg = args[i];
2977 i++;
2978 }
2979
2980 if ( i < args.length && typeof( args[i]) == 'number' )
2981 relation = args[i];
2982
2983 var passed = ( relation == CKEDITOR.VALIDATE_AND ? true : false );
2984 for ( i = 0 ; i < functions.length ; i++ )
2985 {
2986 if ( relation == CKEDITOR.VALIDATE_AND )
2987 passed = passed && functions[i]( value );
2988 else
2989 passed = passed || functions[i]( value );
2990 }
2991
2992 return !passed ? msg : true;
2993 };
2994 },
2995
2996 regex : function( regex, msg )
2997 {
2998 /*
2999 * Can be greatly shortened by deriving from functions validator if code size
3000 * turns out to be more important than performance.
3001 */
3002 return function()
3003 {
3004 var value = this && this.getValue ? this.getValue() : arguments[0];
3005 return !regex.test( value ) ? msg : true;
3006 };
3007 },
3008
3009 notEmpty : function( msg )
3010 {
3011 return this.regex( notEmptyRegex, msg );
3012 },
3013
3014 integer : function( msg )
3015 {
3016 return this.regex( integerRegex, msg );
3017 },
3018
3019 'number' : function( msg )
3020 {
3021 return this.regex( numberRegex, msg );
3022 },
3023
3024 'cssLength' : function( msg )
3025 {
3026 return this.functions( function( val ){ return cssLengthRegex.test( CKEDITOR.tools.trim( val ) ); }, msg );
3027 },
3028
3029 'htmlLength' : function( msg )
3030 {
3031 return this.functions( function( val ){ return htmlLengthRegex.test( CKEDITOR.tools.trim( val ) ); }, msg );
3032 },
3033
3034 equals : function( value, msg )
3035 {
3036 return this.functions( function( val ){ return val == value; }, msg );
3037 },
3038
3039 notEqual : function( value, msg )
3040 {
3041 return this.functions( function( val ){ return val != value; }, msg );
3042 }
3043 };
3044
3045 CKEDITOR.on( 'instanceDestroyed', function( evt )
3046 {
3047 // Remove dialog cover on last instance destroy.
3048 if ( CKEDITOR.tools.isEmpty( CKEDITOR.instances ) )
3049 {
3050 var currentTopDialog;
3051 while ( ( currentTopDialog = CKEDITOR.dialog._.currentTop ) )
3052 currentTopDialog.hide();
3053 removeCovers();
3054 }
3055
3056 var dialogs = evt.editor._.storedDialogs;
3057 for ( var name in dialogs )
3058 dialogs[ name ].destroy();
3059
3060 });
3061
3062 })();
3063
3064 // Extend the CKEDITOR.editor class with dialog specific functions.
3065 CKEDITOR.tools.extend( CKEDITOR.editor.prototype,
3066 /** @lends CKEDITOR.editor.prototype */
3067 {
3068 /**
3069 * Loads and opens a registered dialog.
3070 * @param {String} dialogName The registered name of the dialog.
3071 * @param {Function} callback The function to be invoked after dialog instance created.
3072 * @see CKEDITOR.dialog.add
3073 * @example
3074 * CKEDITOR.instances.editor1.openDialog( 'smiley' );
3075 * @returns {CKEDITOR.dialog} The dialog object corresponding to the dialog displayed. null if the dialog name is not registered.
3076 */
3077 openDialog : function( dialogName, callback )
3078 {
3079 if ( this.mode == 'wysiwyg' && CKEDITOR.env.ie )
3080 {
3081 var selection = this.getSelection();
3082 selection && selection.lock();
3083 }
3084
3085 var dialogDefinitions = CKEDITOR.dialog._.dialogDefinitions[ dialogName ],
3086 dialogSkin = this.skin.dialog;
3087
3088 if ( CKEDITOR.dialog._.currentTop === null )
3089 showCover( this );
3090
3091 // If the dialogDefinition is already loaded, open it immediately.
3092 if ( typeof dialogDefinitions == 'function' && dialogSkin._isLoaded )
3093 {
3094 var storedDialogs = this._.storedDialogs ||
3095 ( this._.storedDialogs = {} );
3096
3097 var dialog = storedDialogs[ dialogName ] ||
3098 ( storedDialogs[ dialogName ] = new CKEDITOR.dialog( this, dialogName ) );
3099
3100 callback && callback.call( dialog, dialog );
3101 dialog.show();
3102
3103 return dialog;
3104 }
3105 else if ( dialogDefinitions == 'failed' )
3106 {
3107 hideCover();
3108 throw new Error( '[CKEDITOR.dialog.openDialog] Dialog "' + dialogName + '" failed when loading definition.' );
3109 }
3110
3111 var me = this;
3112
3113 function onDialogFileLoaded( success )
3114 {
3115 var dialogDefinition = CKEDITOR.dialog._.dialogDefinitions[ dialogName ],
3116 skin = me.skin.dialog;
3117
3118 // Check if both skin part and definition is loaded.
3119 if ( !skin._isLoaded || loadDefinition && typeof success == 'undefined' )
3120 return;
3121
3122 // In case of plugin error, mark it as loading failed.
3123 if ( typeof dialogDefinition != 'function' )
3124 CKEDITOR.dialog._.dialogDefinitions[ dialogName ] = 'failed';
3125
3126 me.openDialog( dialogName, callback );
3127 }
3128
3129 if ( typeof dialogDefinitions == 'string' )
3130 {
3131 var loadDefinition = 1;
3132 CKEDITOR.scriptLoader.load( CKEDITOR.getUrl( dialogDefinitions ), onDialogFileLoaded, null, 0, 1 );
3133 }
3134
3135 CKEDITOR.skins.load( this, 'dialog', onDialogFileLoaded );
3136
3137 return null;
3138 }
3139 });
3140 })();
3141
3142 CKEDITOR.plugins.add( 'dialog',
3143 {
3144 requires : [ 'dialogui' ]
3145 });
3146
3147 // Dialog related configurations.
3148
3149 /**
3150 * The color of the dialog background cover. It should be a valid CSS color
3151 * string.
3152 * @name CKEDITOR.config.dialog_backgroundCoverColor
3153 * @type String
3154 * @default 'white'
3155 * @example
3156 * config.dialog_backgroundCoverColor = 'rgb(255, 254, 253)';
3157 */
3158
3159 /**
3160 * The opacity of the dialog background cover. It should be a number within the
3161 * range [0.0, 1.0].
3162 * @name CKEDITOR.config.dialog_backgroundCoverOpacity
3163 * @type Number
3164 * @default 0.5
3165 * @example
3166 * config.dialog_backgroundCoverOpacity = 0.7;
3167 */
3168
3169 /**
3170 * If the dialog has more than one tab, put focus into the first tab as soon as dialog is opened.
3171 * @name CKEDITOR.config.dialog_startupFocusTab
3172 * @type Boolean
3173 * @default false
3174 * @example
3175 * config.dialog_startupFocusTab = true;
3176 */
3177
3178 /**
3179 * The distance of magnetic borders used in moving and resizing dialogs,
3180 * measured in pixels.
3181 * @name CKEDITOR.config.dialog_magnetDistance
3182 * @type Number
3183 * @default 20
3184 * @example
3185 * config.dialog_magnetDistance = 30;
3186 */
3187
3188 /**
3189 * The guideline to follow when generating the dialog buttons. There are 3 possible options:
3190 * <ul>
3191 * <li>'OS' - the buttons will be displayed in the default order of the user's OS;</li>
3192 * <li>'ltr' - for Left-To-Right order;</li>
3193 * <li>'rtl' - for Right-To-Left order.</li>
3194 * </ul>
3195 * @name CKEDITOR.config.dialog_buttonsOrder
3196 * @type String
3197 * @default 'OS'
3198 * @since 3.5
3199 * @example
3200 * config.dialog_buttonsOrder = 'rtl';
3201 */
3202
3203 /**
3204 * The dialog contents to removed. It's a string composed by dialog name and tab name with a colon between them.
3205 * Separate each pair with semicolon (see example).
3206 * <b>Note: All names are case-sensitive.</b>
3207 * <b>Note: Be cautious when specifying dialog tabs that are mandatory, like "info", dialog functionality might be broken because of this!</b>
3208 * @name CKEDITOR.config.removeDialogTabs
3209 * @type String
3210 * @since 3.5
3211 * @default ''
3212 * @example
3213 * config.removeDialogTabs = 'flash:advanced;image:Link';
3214 */
3215
3216 /**
3217 * Fired when a dialog definition is about to be used to create a dialog into
3218 * an editor instance. This event makes it possible to customize the definition
3219 * before creating it.
3220 * <p>Note that this event is called only the first time a specific dialog is
3221 * opened. Successive openings will use the cached dialog, and this event will
3222 * not get fired.</p>
3223 * @name CKEDITOR#dialogDefinition
3224 * @event
3225 * @param {CKEDITOR.dialog.definition} data The dialog defination that
3226 * is being loaded.
3227 * @param {CKEDITOR.editor} editor The editor instance that will use the
3228 * dialog.
3229 */
3230
3231 /**
3232 * Fired when a tab is going to be selected in a dialog
3233 * @name CKEDITOR.dialog#selectPage
3234 * @event
3235 * @param {String} page The id of the page that it's gonna be selected.
3236 * @param {String} currentPage The id of the current page.
3237 */
3238
3239 /**
3240 * Fired when the user tries to dismiss a dialog
3241 * @name CKEDITOR.dialog#cancel
3242 * @event
3243 * @param {Boolean} hide Whether the event should proceed or not.
3244 */
3245
3246 /**
3247 * Fired when the user tries to confirm a dialog
3248 * @name CKEDITOR.dialog#ok
3249 * @event
3250 * @param {Boolean} hide Whether the event should proceed or not.
3251 */
3252
3253 /**
3254 * Fired when a dialog is shown
3255 * @name CKEDITOR.dialog#show
3256 * @event
3257 */
3258
3259 /**
3260 * Fired when a dialog is shown
3261 * @name CKEDITOR.editor#dialogShow
3262 * @event
3263 */
3264
3265 /**
3266 * Fired when a dialog is hidden
3267 * @name CKEDITOR.dialog#hide
3268 * @event
3269 */
3270
3271 /**
3272 * Fired when a dialog is hidden
3273 * @name CKEDITOR.editor#dialogHide
3274 * @event
3275 */
3276
3277 /**
3278 * Fired when a dialog is being resized. The event is fired on
3279 * both the 'CKEDITOR.dialog' object and the dialog instance
3280 * since 3.5.3, previously it's available only in the global object.
3281 * @name CKEDITOR.dialog#resize
3282 * @since 3.5
3283 * @event
3284 * @param {CKEDITOR.dialog} dialog The dialog being resized (if
3285 * it's fired on the dialog itself, this parameter isn't sent).
3286 * @param {String} skin The skin name.
3287 * @param {Number} width The new width.
3288 * @param {Number} height The new height.
3289 */