]> git.immae.eu Git - perso/Immae/Projets/packagist/ludivine-ckeditor-component.git/blob - sources/plugins/widget/plugin.js
Update to 4.7.3
[perso/Immae/Projets/packagist/ludivine-ckeditor-component.git] / sources / plugins / widget / plugin.js
1 /**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6 /**
7 * @fileOverview [Widget](http://ckeditor.com/addon/widget) plugin.
8 */
9
10 'use strict';
11
12 ( function() {
13 var DRAG_HANDLER_SIZE = 15;
14
15 CKEDITOR.plugins.add( 'widget', {
16 // jscs:disable maximumLineLength
17 lang: 'af,ar,az,bg,ca,cs,cy,da,de,de-ch,el,en,en-gb,eo,es,es-mx,eu,fa,fi,fr,gl,he,hr,hu,id,it,ja,km,ko,ku,lv,nb,nl,no,oc,pl,pt,pt-br,ru,sk,sl,sq,sv,tr,tt,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
18 // jscs:enable maximumLineLength
19 requires: 'lineutils,clipboard,widgetselection',
20 onLoad: function() {
21 CKEDITOR.addCss(
22 '.cke_widget_wrapper{' +
23 'position:relative;' +
24 'outline:none' +
25 '}' +
26 '.cke_widget_inline{' +
27 'display:inline-block' +
28 '}' +
29 '.cke_widget_wrapper:hover>.cke_widget_element{' +
30 'outline:2px solid yellow;' +
31 'cursor:default' +
32 '}' +
33 '.cke_widget_wrapper:hover .cke_widget_editable{' +
34 'outline:2px solid yellow' +
35 '}' +
36 '.cke_widget_wrapper.cke_widget_focused>.cke_widget_element,' +
37 // We need higher specificity than hover style.
38 '.cke_widget_wrapper .cke_widget_editable.cke_widget_editable_focused{' +
39 'outline:2px solid #ace' +
40 '}' +
41 '.cke_widget_editable{' +
42 'cursor:text' +
43 '}' +
44 '.cke_widget_drag_handler_container{' +
45 'position:absolute;' +
46 'width:' + DRAG_HANDLER_SIZE + 'px;' +
47 'height:0;' +
48 // Initially drag handler should not be visible, until its position will be
49 // calculated (http://dev.ckeditor.com/ticket/11177).
50 // We need to hide unpositined handlers, so they don't extend
51 // widget's outline far to the left (http://dev.ckeditor.com/ticket/12024).
52 'display:none;' +
53 'opacity:0.75;' +
54 'transition:height 0s 0.2s;' + // Delay hiding drag handler.
55 // Prevent drag handler from being misplaced (http://dev.ckeditor.com/ticket/11198).
56 'line-height:0' +
57 '}' +
58 '.cke_widget_wrapper:hover>.cke_widget_drag_handler_container{' +
59 'height:' + DRAG_HANDLER_SIZE + 'px;' +
60 'transition:none' +
61 '}' +
62 '.cke_widget_drag_handler_container:hover{' +
63 'opacity:1' +
64 '}' +
65 'img.cke_widget_drag_handler{' +
66 'cursor:move;' +
67 'width:' + DRAG_HANDLER_SIZE + 'px;' +
68 'height:' + DRAG_HANDLER_SIZE + 'px;' +
69 'display:inline-block' +
70 '}' +
71 '.cke_widget_mask{' +
72 'position:absolute;' +
73 'top:0;' +
74 'left:0;' +
75 'width:100%;' +
76 'height:100%;' +
77 'display:block' +
78 '}' +
79 '.cke_editable.cke_widget_dragging, .cke_editable.cke_widget_dragging *{' +
80 'cursor:move !important' +
81 '}'
82 );
83 },
84
85 beforeInit: function( editor ) {
86 /**
87 * An instance of widget repository. It contains all
88 * {@link CKEDITOR.plugins.widget.repository#registered registered widget definitions} and
89 * {@link CKEDITOR.plugins.widget.repository#instances initialized instances}.
90 *
91 * editor.widgets.add( 'someName', {
92 * // Widget definition...
93 * } );
94 *
95 * editor.widgets.registered.someName; // -> Widget definition
96 *
97 * @since 4.3
98 * @readonly
99 * @property {CKEDITOR.plugins.widget.repository} widgets
100 * @member CKEDITOR.editor
101 */
102 editor.widgets = new Repository( editor );
103 },
104
105 afterInit: function( editor ) {
106 addWidgetButtons( editor );
107 setupContextMenu( editor );
108 }
109 } );
110
111 /**
112 * Widget repository. It keeps track of all {@link #registered registered widget definitions} and
113 * {@link #instances initialized instances}. An instance of the repository is available under
114 * the {@link CKEDITOR.editor#widgets} property.
115 *
116 * @class CKEDITOR.plugins.widget.repository
117 * @mixins CKEDITOR.event
118 * @constructor Creates a widget repository instance. Note that the widget plugin automatically
119 * creates a repository instance which is available under the {@link CKEDITOR.editor#widgets} property.
120 * @param {CKEDITOR.editor} editor The editor instance for which the repository will be created.
121 */
122 function Repository( editor ) {
123 /**
124 * The editor instance for which this repository was created.
125 *
126 * @readonly
127 * @property {CKEDITOR.editor} editor
128 */
129 this.editor = editor;
130
131 /**
132 * A hash of registered widget definitions (definition name => {@link CKEDITOR.plugins.widget.definition}).
133 *
134 * To register a definition use the {@link #add} method.
135 *
136 * @readonly
137 */
138 this.registered = {};
139
140 /**
141 * An object containing initialized widget instances (widget id => {@link CKEDITOR.plugins.widget}).
142 *
143 * @readonly
144 */
145 this.instances = {};
146
147 /**
148 * An array of selected widget instances.
149 *
150 * @readonly
151 * @property {CKEDITOR.plugins.widget[]} selected
152 */
153 this.selected = [];
154
155 /**
156 * The focused widget instance. See also {@link CKEDITOR.plugins.widget#event-focus}
157 * and {@link CKEDITOR.plugins.widget#event-blur} events.
158 *
159 * editor.on( 'selectionChange', function() {
160 * if ( editor.widgets.focused ) {
161 * // Do something when a widget is focused...
162 * }
163 * } );
164 *
165 * @readonly
166 * @property {CKEDITOR.plugins.widget} focused
167 */
168 this.focused = null;
169
170 /**
171 * The widget instance that contains the nested editable which is currently focused.
172 *
173 * @readonly
174 * @property {CKEDITOR.plugins.widget} widgetHoldingFocusedEditable
175 */
176 this.widgetHoldingFocusedEditable = null;
177
178 this._ = {
179 nextId: 0,
180 upcasts: [],
181 upcastCallbacks: [],
182 filters: {}
183 };
184
185 setupWidgetsLifecycle( this );
186 setupSelectionObserver( this );
187 setupMouseObserver( this );
188 setupKeyboardObserver( this );
189 setupDragAndDrop( this );
190 setupNativeCutAndCopy( this );
191 }
192
193 Repository.prototype = {
194 /**
195 * Minimum interval between selection checks.
196 *
197 * @private
198 */
199 MIN_SELECTION_CHECK_INTERVAL: 500,
200
201 /**
202 * Adds a widget definition to the repository. Fires the {@link CKEDITOR.editor#widgetDefinition} event
203 * which allows to modify the widget definition which is going to be registered.
204 *
205 * @param {String} name The name of the widget definition.
206 * @param {CKEDITOR.plugins.widget.definition} widgetDef Widget definition.
207 * @returns {CKEDITOR.plugins.widget.definition}
208 */
209 add: function( name, widgetDef ) {
210 // Create prototyped copy of original widget definition, so we won't modify it.
211 widgetDef = CKEDITOR.tools.prototypedCopy( widgetDef );
212 widgetDef.name = name;
213
214 widgetDef._ = widgetDef._ || {};
215
216 this.editor.fire( 'widgetDefinition', widgetDef );
217
218 if ( widgetDef.template )
219 widgetDef.template = new CKEDITOR.template( widgetDef.template );
220
221 addWidgetCommand( this.editor, widgetDef );
222 addWidgetProcessors( this, widgetDef );
223
224 this.registered[ name ] = widgetDef;
225
226 return widgetDef;
227 },
228
229 /**
230 * Adds a callback for element upcasting. Each callback will be executed
231 * for every element which is later tested by upcast methods. If a callback
232 * returns `false`, the element will not be upcasted.
233 *
234 * // Images with the "banner" class will not be upcasted (e.g. to the image widget).
235 * editor.widgets.addUpcastCallback( function( element ) {
236 * if ( element.name == 'img' && element.hasClass( 'banner' ) )
237 * return false;
238 * } );
239 *
240 * @param {Function} callback
241 * @param {CKEDITOR.htmlParser.element} callback.element
242 */
243 addUpcastCallback: function( callback ) {
244 this._.upcastCallbacks.push( callback );
245 },
246
247 /**
248 * Checks the selection to update widget states (selection and focus).
249 *
250 * This method is triggered by the {@link #event-checkSelection} event.
251 */
252 checkSelection: function() {
253 var sel = this.editor.getSelection(),
254 selectedElement = sel.getSelectedElement(),
255 updater = stateUpdater( this ),
256 widget;
257
258 // Widget is focused so commit and finish checking.
259 if ( selectedElement && ( widget = this.getByElement( selectedElement, true ) ) )
260 return updater.focus( widget ).select( widget ).commit();
261
262 var range = sel.getRanges()[ 0 ];
263
264 // No ranges or collapsed range mean that nothing is selected, so commit and finish checking.
265 if ( !range || range.collapsed )
266 return updater.commit();
267
268 // Range is not empty, so create walker checking for wrappers.
269 var walker = new CKEDITOR.dom.walker( range ),
270 wrapper;
271
272 walker.evaluator = Widget.isDomWidgetWrapper;
273
274 while ( ( wrapper = walker.next() ) )
275 updater.select( this.getByElement( wrapper ) );
276
277 updater.commit();
278 },
279
280 /**
281 * Checks if all widget instances are still present in the DOM.
282 * Destroys those instances that are not present.
283 * Reinitializes widgets on widget wrappers for which widget instances
284 * cannot be found. Takes nested widgets into account, too.
285 *
286 * This method triggers the {@link #event-checkWidgets} event whose listeners
287 * can cancel the method's execution or modify its options.
288 *
289 * @param [options] The options object.
290 * @param {Boolean} [options.initOnlyNew] Initializes widgets only on newly wrapped
291 * widget elements (those which still have the `cke_widget_new` class). When this option is
292 * set to `true`, widgets which were invalidated (e.g. by replacing with a cloned DOM structure)
293 * will not be reinitialized. This makes the check faster.
294 * @param {Boolean} [options.focusInited] If only one widget is initialized by
295 * the method, it will be focused.
296 */
297 checkWidgets: function( options ) {
298 this.fire( 'checkWidgets', CKEDITOR.tools.copy( options || {} ) );
299 },
300
301 /**
302 * Removes the widget from the editor and moves the selection to the closest
303 * editable position if the widget was focused before.
304 *
305 * @param {CKEDITOR.plugins.widget} widget The widget instance to be deleted.
306 */
307 del: function( widget ) {
308 if ( this.focused === widget ) {
309 var editor = widget.editor,
310 range = editor.createRange(),
311 found;
312
313 // If haven't found place for caret on the default side,
314 // try to find it on the other side.
315 if ( !( found = range.moveToClosestEditablePosition( widget.wrapper, true ) ) )
316 found = range.moveToClosestEditablePosition( widget.wrapper, false );
317
318 if ( found )
319 editor.getSelection().selectRanges( [ range ] );
320 }
321
322 widget.wrapper.remove();
323 this.destroy( widget, true );
324 },
325
326 /**
327 * Destroys the widget instance and all its nested widgets (widgets inside its nested editables).
328 *
329 * @param {CKEDITOR.plugins.widget} widget The widget instance to be destroyed.
330 * @param {Boolean} [offline] Whether the widget is offline (detached from the DOM tree) —
331 * in this case the DOM (attributes, classes, etc.) will not be cleaned up.
332 */
333 destroy: function( widget, offline ) {
334 if ( this.widgetHoldingFocusedEditable === widget )
335 setFocusedEditable( this, widget, null, offline );
336
337 widget.destroy( offline );
338 delete this.instances[ widget.id ];
339 this.fire( 'instanceDestroyed', widget );
340 },
341
342 /**
343 * Destroys all widget instances.
344 *
345 * @param {Boolean} [offline] Whether the widgets are offline (detached from the DOM tree) —
346 * in this case the DOM (attributes, classes, etc.) will not be cleaned up.
347 * @param {CKEDITOR.dom.element} [container] The container within widgets will be destroyed.
348 * This option will be ignored if the `offline` flag was set to `true`, because in such case
349 * it is not possible to find widgets within the passed block.
350 */
351 destroyAll: function( offline, container ) {
352 var widget,
353 id,
354 instances = this.instances;
355
356 if ( container && !offline ) {
357 var wrappers = container.find( '.cke_widget_wrapper' ),
358 l = wrappers.count(),
359 i = 0;
360
361 // Length is constant, because this is not a live node list.
362 // Note: since querySelectorAll returns nodes in document order,
363 // outer widgets are always placed before their nested widgets and therefore
364 // are destroyed before them.
365 for ( ; i < l; ++i ) {
366 widget = this.getByElement( wrappers.getItem( i ), true );
367 // Widget might not be found, because it could be a nested widget,
368 // which would be destroyed when destroying its parent.
369 if ( widget )
370 this.destroy( widget );
371 }
372
373 return;
374 }
375
376 for ( id in instances ) {
377 widget = instances[ id ];
378 this.destroy( widget, offline );
379 }
380 },
381
382 /**
383 * Finalizes a process of widget creation. This includes:
384 *
385 * * inserting widget element into editor,
386 * * marking widget instance as ready (see {@link CKEDITOR.plugins.widget#event-ready}),
387 * * focusing widget instance.
388 *
389 * This method is used by the default widget's command and is called
390 * after widget's dialog (if set) is closed. It may also be used in a
391 * customized process of widget creation and insertion.
392 *
393 * widget.once( 'edit', function() {
394 * // Finalize creation only of not ready widgets.
395 * if ( widget.isReady() )
396 * return;
397 *
398 * // Cancel edit event to prevent automatic widget insertion.
399 * evt.cancel();
400 *
401 * CustomDialog.open( widget.data, function saveCallback( savedData ) {
402 * // Cache the container, because widget may be destroyed while saving data,
403 * // if this process will require some deep transformations.
404 * var container = widget.wrapper.getParent();
405 *
406 * widget.setData( savedData );
407 *
408 * // Widget will be retrieved from container and inserted into editor.
409 * editor.widgets.finalizeCreation( container );
410 * } );
411 * } );
412 *
413 * @param {CKEDITOR.dom.element/CKEDITOR.dom.documentFragment} container The element
414 * or document fragment which contains widget wrapper. The container is used, so before
415 * finalizing creation the widget can be freely transformed (even destroyed and reinitialized).
416 */
417 finalizeCreation: function( container ) {
418 var wrapper = container.getFirst();
419 if ( wrapper && Widget.isDomWidgetWrapper( wrapper ) ) {
420 this.editor.insertElement( wrapper );
421
422 var widget = this.getByElement( wrapper );
423 // Fire postponed #ready event.
424 widget.ready = true;
425 widget.fire( 'ready' );
426 widget.focus();
427 }
428 },
429
430 /**
431 * Finds a widget instance which contains a given element. The element will be the {@link CKEDITOR.plugins.widget#wrapper wrapper}
432 * of the returned widget or a descendant of this {@link CKEDITOR.plugins.widget#wrapper wrapper}.
433 *
434 * editor.widgets.getByElement( someWidget.wrapper ); // -> someWidget
435 * editor.widgets.getByElement( someWidget.parts.caption ); // -> someWidget
436 *
437 * // Check wrapper only:
438 * editor.widgets.getByElement( someWidget.wrapper, true ); // -> someWidget
439 * editor.widgets.getByElement( someWidget.parts.caption, true ); // -> null
440 *
441 * @param {CKEDITOR.dom.element} element The element to be checked.
442 * @param {Boolean} [checkWrapperOnly] If set to `true`, the method will not check wrappers' descendants.
443 * @returns {CKEDITOR.plugins.widget} The widget instance or `null`.
444 */
445 getByElement: ( function() {
446 var validWrapperElements = { div: 1, span: 1 };
447 function getWidgetId( element ) {
448 return element.is( validWrapperElements ) && element.data( 'cke-widget-id' );
449 }
450
451 return function( element, checkWrapperOnly ) {
452 if ( !element )
453 return null;
454
455 var id = getWidgetId( element );
456
457 // There's no need to check element parents if element is a wrapper.
458 if ( !checkWrapperOnly && !id ) {
459 var limit = this.editor.editable();
460
461 // Try to find a closest ascendant which is a widget wrapper.
462 do {
463 element = element.getParent();
464 } while ( element && !element.equals( limit ) && !( id = getWidgetId( element ) ) );
465 }
466
467 return this.instances[ id ] || null;
468 };
469 } )(),
470
471 /**
472 * Initializes a widget on a given element if the widget has not been initialized on it yet.
473 *
474 * @param {CKEDITOR.dom.element} element The future widget element.
475 * @param {String/CKEDITOR.plugins.widget.definition} [widgetDef] Name of a widget or a widget definition.
476 * The widget definition should be previously registered by using the
477 * {@link CKEDITOR.plugins.widget.repository#add} method.
478 * @param [startupData] Widget startup data (has precedence over default one).
479 * @returns {CKEDITOR.plugins.widget} The widget instance or `null` if a widget could not be initialized on
480 * a given element.
481 */
482 initOn: function( element, widgetDef, startupData ) {
483 if ( !widgetDef )
484 widgetDef = this.registered[ element.data( 'widget' ) ];
485 else if ( typeof widgetDef == 'string' )
486 widgetDef = this.registered[ widgetDef ];
487
488 if ( !widgetDef )
489 return null;
490
491 // Wrap element if still wasn't wrapped (was added during runtime by method that skips dataProcessor).
492 var wrapper = this.wrapElement( element, widgetDef.name );
493
494 if ( wrapper ) {
495 // Check if widget wrapper is new (widget hasn't been initialized on it yet).
496 // This class will be removed by widget constructor to avoid locking snapshot twice.
497 if ( wrapper.hasClass( 'cke_widget_new' ) ) {
498 var widget = new Widget( this, this._.nextId++, element, widgetDef, startupData );
499
500 // Widget could be destroyed when initializing it.
501 if ( widget.isInited() ) {
502 this.instances[ widget.id ] = widget;
503
504 return widget;
505 } else {
506 return null;
507 }
508 }
509
510 // Widget already has been initialized, so try to get widget by element.
511 // Note - it may happen that other instance will returned than the one created above,
512 // if for example widget was destroyed and reinitialized.
513 return this.getByElement( element );
514 }
515
516 // No wrapper means that there's no widget for this element.
517 return null;
518 },
519
520 /**
521 * Initializes widgets on all elements which were wrapped by {@link #wrapElement} and
522 * have not been initialized yet.
523 *
524 * @param {CKEDITOR.dom.element} [container=editor.editable()] The container which will be checked for not
525 * initialized widgets. Defaults to editor's {@link CKEDITOR.editor#editable editable} element.
526 * @returns {CKEDITOR.plugins.widget[]} Array of widget instances which have been initialized.
527 * Note: Only first-level widgets are returned &mdash; without nested widgets.
528 */
529 initOnAll: function( container ) {
530 var newWidgets = ( container || this.editor.editable() ).find( '.cke_widget_new' ),
531 newInstances = [],
532 instance;
533
534 for ( var i = newWidgets.count(); i--; ) {
535 instance = this.initOn( newWidgets.getItem( i ).getFirst( Widget.isDomWidgetElement ) );
536 if ( instance )
537 newInstances.push( instance );
538 }
539
540 return newInstances;
541 },
542
543 /**
544 * Allows to listen to events on specific types of widgets, even if they are not created yet.
545 *
546 * Please note that this method inherits parameters from the {@link CKEDITOR.event#method-on} method with one
547 * extra parameter at the beginning which is the widget name.
548 *
549 * editor.widgets.onWidget( 'image', 'action', function( evt ) {
550 * // Event `action` occurs on `image` widget.
551 * } );
552 *
553 * @since 4.5
554 * @param {String} widgetName
555 * @param {String} eventName
556 * @param {Function} listenerFunction
557 * @param {Object} [scopeObj]
558 * @param {Object} [listenerData]
559 * @param {Number} [priority=10]
560 */
561 onWidget: function( widgetName ) {
562 var args = Array.prototype.slice.call( arguments );
563
564 args.shift();
565
566 for ( var i in this.instances ) {
567 var instance = this.instances[ i ];
568
569 if ( instance.name == widgetName ) {
570 instance.on.apply( instance, args );
571 }
572 }
573
574 this.on( 'instanceCreated', function( evt ) {
575 var widget = evt.data;
576
577 if ( widget.name == widgetName ) {
578 widget.on.apply( widget, args );
579 }
580 } );
581 },
582
583 /**
584 * Parses element classes string and returns an object
585 * whose keys contain class names. Skips all `cke_*` classes.
586 *
587 * This method is used by the {@link CKEDITOR.plugins.widget#getClasses} method and
588 * may be used when overriding that method.
589 *
590 * @since 4.4
591 * @param {String} classes String (value of `class` attribute).
592 * @returns {Object} Object containing classes or `null` if no classes found.
593 */
594 parseElementClasses: function( classes ) {
595 if ( !classes )
596 return null;
597
598 classes = CKEDITOR.tools.trim( classes ).split( /\s+/ );
599
600 var cl,
601 obj = {},
602 hasClasses = 0;
603
604 while ( ( cl = classes.pop() ) ) {
605 if ( cl.indexOf( 'cke_' ) == -1 )
606 obj[ cl ] = hasClasses = 1;
607 }
608
609 return hasClasses ? obj : null;
610 },
611
612 /**
613 * Wraps an element with a widget's non-editable container.
614 *
615 * If this method is called on an {@link CKEDITOR.htmlParser.element}, then it will
616 * also take care of fixing the DOM after wrapping (the wrapper may not be allowed in element's parent).
617 *
618 * @param {CKEDITOR.dom.element/CKEDITOR.htmlParser.element} element The widget element to be wrapped.
619 * @param {String} [widgetName] The name of the widget definition. Defaults to element's `data-widget`
620 * attribute value.
621 * @returns {CKEDITOR.dom.element/CKEDITOR.htmlParser.element} The wrapper element or `null` if
622 * the widget definition of this name is not registered.
623 */
624 wrapElement: function( element, widgetName ) {
625 var wrapper = null,
626 widgetDef,
627 isInline;
628
629 if ( element instanceof CKEDITOR.dom.element ) {
630 widgetName = widgetName || element.data( 'widget' );
631 widgetDef = this.registered[ widgetName ];
632
633 if ( !widgetDef )
634 return null;
635
636 // Do not wrap already wrapped element.
637 wrapper = element.getParent();
638 if ( wrapper && wrapper.type == CKEDITOR.NODE_ELEMENT && wrapper.data( 'cke-widget-wrapper' ) )
639 return wrapper;
640
641 // If attribute isn't already set (e.g. for pasted widget), set it.
642 if ( !element.hasAttribute( 'data-cke-widget-keep-attr' ) )
643 element.data( 'cke-widget-keep-attr', element.data( 'widget' ) ? 1 : 0 );
644
645 element.data( 'widget', widgetName );
646
647 isInline = isWidgetInline( widgetDef, element.getName() );
648
649 wrapper = new CKEDITOR.dom.element( isInline ? 'span' : 'div' );
650 wrapper.setAttributes( getWrapperAttributes( isInline, widgetName ) );
651
652 wrapper.data( 'cke-display-name', widgetDef.pathName ? widgetDef.pathName : element.getName() );
653
654 // Replace element unless it is a detached one.
655 if ( element.getParent( true ) )
656 wrapper.replace( element );
657 element.appendTo( wrapper );
658 }
659 else if ( element instanceof CKEDITOR.htmlParser.element ) {
660 widgetName = widgetName || element.attributes[ 'data-widget' ];
661 widgetDef = this.registered[ widgetName ];
662
663 if ( !widgetDef )
664 return null;
665
666 wrapper = element.parent;
667 if ( wrapper && wrapper.type == CKEDITOR.NODE_ELEMENT && wrapper.attributes[ 'data-cke-widget-wrapper' ] )
668 return wrapper;
669
670 // If attribute isn't already set (e.g. for pasted widget), set it.
671 if ( !( 'data-cke-widget-keep-attr' in element.attributes ) )
672 element.attributes[ 'data-cke-widget-keep-attr' ] = element.attributes[ 'data-widget' ] ? 1 : 0;
673 if ( widgetName )
674 element.attributes[ 'data-widget' ] = widgetName;
675
676 isInline = isWidgetInline( widgetDef, element.name );
677
678 wrapper = new CKEDITOR.htmlParser.element( isInline ? 'span' : 'div', getWrapperAttributes( isInline, widgetName ) );
679 wrapper.attributes[ 'data-cke-display-name' ] = widgetDef.pathName ? widgetDef.pathName : element.name;
680
681 var parent = element.parent,
682 index;
683
684 // Don't detach already detached element.
685 if ( parent ) {
686 index = element.getIndex();
687 element.remove();
688 }
689
690 wrapper.add( element );
691
692 // Insert wrapper fixing DOM (splitting parents if wrapper is not allowed inside them).
693 parent && insertElement( parent, index, wrapper );
694 }
695
696 return wrapper;
697 },
698
699 // Expose for tests.
700 _tests_createEditableFilter: createEditableFilter
701 };
702
703 CKEDITOR.event.implementOn( Repository.prototype );
704
705 /**
706 * An event fired when a widget instance is created, but before it is fully initialized.
707 *
708 * @event instanceCreated
709 * @param {CKEDITOR.plugins.widget} data The widget instance.
710 */
711
712 /**
713 * An event fired when a widget instance was destroyed.
714 *
715 * See also {@link CKEDITOR.plugins.widget#event-destroy}.
716 *
717 * @event instanceDestroyed
718 * @param {CKEDITOR.plugins.widget} data The widget instance.
719 */
720
721 /**
722 * An event fired to trigger the selection check.
723 *
724 * See the {@link #method-checkSelection} method.
725 *
726 * @event checkSelection
727 */
728
729 /**
730 * An event fired by the the {@link #method-checkWidgets} method.
731 *
732 * It can be canceled in order to stop the {@link #method-checkWidgets}
733 * method execution or the event listener can modify the method's options.
734 *
735 * @event checkWidgets
736 * @param [data]
737 * @param {Boolean} [data.initOnlyNew] Initialize widgets only on newly wrapped
738 * widget elements (those which still have the `cke_widget_new` class). When this option is
739 * set to `true`, widgets which were invalidated (e.g. by replacing with a cloned DOM structure)
740 * will not be reinitialized. This makes the check faster.
741 * @param {Boolean} [data.focusInited] If only one widget is initialized by
742 * the method, it will be focused.
743 */
744
745
746 /**
747 * An instance of a widget. Together with {@link CKEDITOR.plugins.widget.repository} these
748 * two classes constitute the core of the Widget System.
749 *
750 * Note that neither the repository nor the widget instances can be created by using their constructors.
751 * A repository instance is automatically set up by the Widget plugin and is accessible under
752 * {@link CKEDITOR.editor#widgets}, while widget instances are created and destroyed by the repository.
753 *
754 * To create a widget, first you need to {@link CKEDITOR.plugins.widget.repository#add register} its
755 * {@link CKEDITOR.plugins.widget.definition definition}:
756 *
757 * editor.widgets.add( 'simplebox', {
758 * upcast: function( element ) {
759 * // Defines which elements will become widgets.
760 * if ( element.hasClass( 'simplebox' ) )
761 * return true;
762 * },
763 * init: function() {
764 * // ...
765 * }
766 * } );
767 *
768 * Once the widget definition is registered, widgets will be automatically
769 * created when loading data:
770 *
771 * editor.setData( '<div class="simplebox">foo</div>', function() {
772 * console.log( editor.widgets.instances ); // -> An object containing one instance.
773 * } );
774 *
775 * It is also possible to create instances during runtime by using a command
776 * (if a {@link CKEDITOR.plugins.widget.definition#template} property was defined):
777 *
778 * // You can execute an automatically defined command to
779 * // insert a new simplebox widget or edit the one currently focused.
780 * editor.execCommand( 'simplebox' );
781 *
782 * Note: Since CKEditor 4.5 widget's `startupData` can be passed as the command argument:
783 *
784 * editor.execCommand( 'simplebox', {
785 * startupData: {
786 * align: 'left'
787 * }
788 * } );
789 *
790 * A widget can also be created in a completely custom way:
791 *
792 * var element = editor.document.createElement( 'div' );
793 * editor.insertElement( element );
794 * var widget = editor.widgets.initOn( element, 'simplebox' );
795 *
796 * @since 4.3
797 * @class CKEDITOR.plugins.widget
798 * @mixins CKEDITOR.event
799 * @extends CKEDITOR.plugins.widget.definition
800 * @constructor Creates an instance of the widget class. Do not use it directly, but instead initialize widgets
801 * by using the {@link CKEDITOR.plugins.widget.repository#initOn} method or by the upcasting system.
802 * @param {CKEDITOR.plugins.widget.repository} widgetsRepo
803 * @param {Number} id Unique ID of this widget instance.
804 * @param {CKEDITOR.dom.element} element The widget element.
805 * @param {CKEDITOR.plugins.widget.definition} widgetDef Widget's registered definition.
806 * @param [startupData] Initial widget data. This data object will overwrite the default data and
807 * the data loaded from the DOM.
808 */
809 function Widget( widgetsRepo, id, element, widgetDef, startupData ) {
810 var editor = widgetsRepo.editor;
811
812 // Extend this widget with widgetDef-specific methods and properties.
813 CKEDITOR.tools.extend( this, widgetDef, {
814 /**
815 * The editor instance.
816 *
817 * @readonly
818 * @property {CKEDITOR.editor}
819 */
820 editor: editor,
821
822 /**
823 * This widget's unique (per editor instance) ID.
824 *
825 * @readonly
826 * @property {Number}
827 */
828 id: id,
829
830 /**
831 * Whether this widget is an inline widget (based on an inline element unless
832 * forced otherwise by {@link CKEDITOR.plugins.widget.definition#inline}).
833 *
834 * **Note:** This option does not allow to turn a block element into an inline widget.
835 * However, it makes it possible to turn an inline element into a block widget or to
836 * force a correct type in case when automatic recognition fails.
837 *
838 * @readonly
839 * @property {Boolean}
840 */
841 inline: element.getParent().getName() == 'span',
842
843 /**
844 * The widget element &mdash; the element on which the widget was initialized.
845 *
846 * @readonly
847 * @property {CKEDITOR.dom.element} element
848 */
849 element: element,
850
851 /**
852 * Widget's data object.
853 *
854 * The data can only be set by using the {@link #setData} method.
855 * Changes made to the data fire the {@link #event-data} event.
856 *
857 * @readonly
858 */
859 data: CKEDITOR.tools.extend( {}, typeof widgetDef.defaults == 'function' ? widgetDef.defaults() : widgetDef.defaults ),
860
861 /**
862 * Indicates if a widget is data-ready. Set to `true` when data from all sources
863 * ({@link CKEDITOR.plugins.widget.definition#defaults}, set in the
864 * {@link #init} method, loaded from the widget's element and startup data coming from the constructor)
865 * are finally loaded. This is immediately followed by the first {@link #event-data}.
866 *
867 * @readonly
868 */
869 dataReady: false,
870
871 /**
872 * Whether a widget instance was initialized. This means that:
873 *
874 * * An instance was created,
875 * * Its properties were set,
876 * * The `init` method was executed.
877 *
878 * **Note**: The first {@link #event-data} event could not be fired yet which
879 * means that the widget's DOM has not been set up yet. Wait for the {@link #event-ready}
880 * event to be notified when a widget is fully initialized and ready.
881 *
882 * **Note**: Use the {@link #isInited} method to check whether a widget is initialized and
883 * has not been destroyed.
884 *
885 * @readonly
886 */
887 inited: false,
888
889 /**
890 * Whether a widget instance is ready. This means that the widget is {@link #inited} and
891 * that its DOM was finally set up.
892 *
893 * **Note:** Use the {@link #isReady} method to check whether a widget is ready and
894 * has not been destroyed.
895 *
896 * @readonly
897 */
898 ready: false,
899
900 // Revert what widgetDef could override (automatic #edit listener).
901 edit: Widget.prototype.edit,
902
903 /**
904 * The nested editable element which is currently focused.
905 *
906 * @readonly
907 * @property {CKEDITOR.plugins.widget.nestedEditable}
908 */
909 focusedEditable: null,
910
911 /**
912 * The widget definition from which this instance was created.
913 *
914 * @readonly
915 * @property {CKEDITOR.plugins.widget.definition} definition
916 */
917 definition: widgetDef,
918
919 /**
920 * Link to the widget repository which created this instance.
921 *
922 * @readonly
923 * @property {CKEDITOR.plugins.widget.repository} repository
924 */
925 repository: widgetsRepo,
926
927 draggable: widgetDef.draggable !== false,
928
929 // WAAARNING: Overwrite widgetDef's priv object, because otherwise violent unicorn's gonna visit you.
930 _: {
931 downcastFn: ( widgetDef.downcast && typeof widgetDef.downcast == 'string' ) ?
932 widgetDef.downcasts[ widgetDef.downcast ] : widgetDef.downcast
933 }
934 }, true );
935
936 /**
937 * An object of widget component elements.
938 *
939 * For every `partName => selector` pair in {@link CKEDITOR.plugins.widget.definition#parts},
940 * one `partName => element` pair is added to this object during the widget initialization.
941 *
942 * @readonly
943 * @property {Object} parts
944 */
945
946 /**
947 * The template which will be used to create a new widget element (when the widget's command is executed).
948 * It will be populated with {@link #defaults default values}.
949 *
950 * @readonly
951 * @property {CKEDITOR.template} template
952 */
953
954 /**
955 * The widget wrapper &mdash; a non-editable `div` or `span` element (depending on {@link #inline})
956 * which is a parent of the {@link #element} and widget compontents like the drag handler and the {@link #mask}.
957 * It is the outermost widget element.
958 *
959 * @readonly
960 * @property {CKEDITOR.dom.element} wrapper
961 */
962
963 widgetsRepo.fire( 'instanceCreated', this );
964
965 setupWidget( this, widgetDef );
966
967 this.init && this.init();
968
969 // Finally mark widget as inited.
970 this.inited = true;
971
972 setupWidgetData( this, startupData );
973
974 // If at some point (e.g. in #data listener) widget hasn't been destroyed
975 // and widget is already attached to document then fire #ready.
976 if ( this.isInited() && editor.editable().contains( this.wrapper ) ) {
977 this.ready = true;
978 this.fire( 'ready' );
979 }
980 }
981
982 Widget.prototype = {
983 /**
984 * Adds a class to the widget element. This method is used by
985 * the {@link #applyStyle} method and should be overridden by widgets
986 * which should handle classes differently (e.g. add them to other elements).
987 *
988 * Since 4.6.0 this method also adds a corresponding class prefixed with {@link #WRAPPER_CLASS_PREFIX}
989 * to the widget wrapper element.
990 *
991 * **Note**: This method should not be used directly. Use the {@link #setData} method to
992 * set the `classes` property. Read more in the {@link #setData} documentation.
993 *
994 * See also: {@link #removeClass}, {@link #hasClass}, {@link #getClasses}.
995 *
996 * @since 4.4
997 * @param {String} className The class name to be added.
998 */
999 addClass: function( className ) {
1000 this.element.addClass( className );
1001 this.wrapper.addClass( Widget.WRAPPER_CLASS_PREFIX + className );
1002 },
1003
1004 /**
1005 * Applies the specified style to the widget. It is highly recommended to use the
1006 * {@link CKEDITOR.editor#applyStyle} or {@link CKEDITOR.style#apply} methods instead of
1007 * using this method directly, because unlike editor's and style's methods, this one
1008 * does not perform any checks.
1009 *
1010 * By default this method handles only classes defined in the style. It clones existing
1011 * classes which are stored in the {@link #property-data widget data}'s `classes` property,
1012 * adds new classes, and calls the {@link #setData} method if at least one new class was added.
1013 * Then, using the {@link #event-data} event listener widget applies modifications passing
1014 * new classes to the {@link #addClass} method.
1015 *
1016 * If you need to handle classes differently than in the default way, you can override the
1017 * {@link #addClass} and related methods. You can also handle other style properties than `classes`
1018 * by overriding this method.
1019 *
1020 * See also: {@link #checkStyleActive}, {@link #removeStyle}.
1021 *
1022 * @since 4.4
1023 * @param {CKEDITOR.style} style The custom widget style to be applied.
1024 */
1025 applyStyle: function( style ) {
1026 applyRemoveStyle( this, style, 1 );
1027 },
1028
1029 /**
1030 * Checks if the specified style is applied to this widget. It is highly recommended to use the
1031 * {@link CKEDITOR.style#checkActive} method instead of using this method directly,
1032 * because unlike style's method, this one does not perform any checks.
1033 *
1034 * By default this method handles only classes defined in the style and passes
1035 * them to the {@link #hasClass} method. You can override these methods to handle classes
1036 * differently or to handle more of the style properties.
1037 *
1038 * See also: {@link #applyStyle}, {@link #removeStyle}.
1039 *
1040 * @since 4.4
1041 * @param {CKEDITOR.style} style The custom widget style to be checked.
1042 * @returns {Boolean} Whether the style is applied to this widget.
1043 */
1044 checkStyleActive: function( style ) {
1045 var classes = getStyleClasses( style ),
1046 cl;
1047
1048 if ( !classes )
1049 return false;
1050
1051 while ( ( cl = classes.pop() ) ) {
1052 if ( !this.hasClass( cl ) )
1053 return false;
1054 }
1055 return true;
1056 },
1057
1058 /**
1059 * Destroys this widget instance.
1060 *
1061 * Use {@link CKEDITOR.plugins.widget.repository#destroy} when possible instead of this method.
1062 *
1063 * This method fires the {#event-destroy} event.
1064 *
1065 * @param {Boolean} [offline] Whether a widget is offline (detached from the DOM tree) &mdash;
1066 * in this case the DOM (attributes, classes, etc.) will not be cleaned up.
1067 */
1068 destroy: function( offline ) {
1069 this.fire( 'destroy' );
1070
1071 if ( this.editables ) {
1072 for ( var name in this.editables )
1073 this.destroyEditable( name, offline );
1074 }
1075
1076 if ( !offline ) {
1077 if ( this.element.data( 'cke-widget-keep-attr' ) == '0' )
1078 this.element.removeAttribute( 'data-widget' );
1079 this.element.removeAttributes( [ 'data-cke-widget-data', 'data-cke-widget-keep-attr' ] );
1080 this.element.removeClass( 'cke_widget_element' );
1081 this.element.replace( this.wrapper );
1082 }
1083
1084 this.wrapper = null;
1085 },
1086
1087 /**
1088 * Destroys a nested editable and all nested widgets.
1089 *
1090 * @param {String} editableName Nested editable name.
1091 * @param {Boolean} [offline] See {@link #method-destroy} method.
1092 */
1093 destroyEditable: function( editableName, offline ) {
1094 var editable = this.editables[ editableName ];
1095
1096 editable.removeListener( 'focus', onEditableFocus );
1097 editable.removeListener( 'blur', onEditableBlur );
1098 this.editor.focusManager.remove( editable );
1099
1100 if ( !offline ) {
1101 this.repository.destroyAll( false, editable );
1102 editable.removeClass( 'cke_widget_editable' );
1103 editable.removeClass( 'cke_widget_editable_focused' );
1104 editable.removeAttributes( [ 'contenteditable', 'data-cke-widget-editable', 'data-cke-enter-mode' ] );
1105 }
1106
1107 delete this.editables[ editableName ];
1108 },
1109
1110 /**
1111 * Starts widget editing.
1112 *
1113 * This method fires the {@link CKEDITOR.plugins.widget#event-edit} event
1114 * which may be canceled in order to prevent it from opening a dialog window.
1115 *
1116 * The dialog window name is obtained from the event's data `dialog` property or
1117 * from {@link CKEDITOR.plugins.widget.definition#dialog}.
1118 *
1119 * @returns {Boolean} Returns `true` if a dialog window was opened.
1120 */
1121 edit: function() {
1122 var evtData = { dialog: this.dialog },
1123 that = this;
1124
1125 // Edit event was blocked or there's no dialog to be automatically opened.
1126 if ( this.fire( 'edit', evtData ) === false || !evtData.dialog )
1127 return false;
1128
1129 this.editor.openDialog( evtData.dialog, function( dialog ) {
1130 var showListener,
1131 okListener;
1132
1133 // Allow to add a custom dialog handler.
1134 if ( that.fire( 'dialog', dialog ) === false )
1135 return;
1136
1137 showListener = dialog.on( 'show', function() {
1138 dialog.setupContent( that );
1139 } );
1140
1141 okListener = dialog.on( 'ok', function() {
1142 // Commit dialog's fields, but prevent from
1143 // firing data event for every field. Fire only one,
1144 // bulk event at the end.
1145 var dataChanged,
1146 dataListener = that.on( 'data', function( evt ) {
1147 dataChanged = 1;
1148 evt.cancel();
1149 }, null, null, 0 );
1150
1151 // Create snapshot preceeding snapshot with changed widget...
1152 // TODO it should not be required, but it is and I found similar
1153 // code in dialog#ok listener in dialog/plugin.js.
1154 that.editor.fire( 'saveSnapshot' );
1155 dialog.commitContent( that );
1156
1157 dataListener.removeListener();
1158 if ( dataChanged ) {
1159 that.fire( 'data', that.data );
1160 that.editor.fire( 'saveSnapshot' );
1161 }
1162 } );
1163
1164 dialog.once( 'hide', function() {
1165 showListener.removeListener();
1166 okListener.removeListener();
1167 } );
1168 } );
1169
1170 return true;
1171 },
1172
1173 /**
1174 * Returns widget element classes parsed to an object. This method
1175 * is used to populate the `classes` property of widget's {@link #property-data}.
1176 *
1177 * This method reuses {@link CKEDITOR.plugins.widget.repository#parseElementClasses}.
1178 * It should be overriden if a widget should handle classes differently (e.g. on other elements).
1179 *
1180 * See also: {@link #removeClass}, {@link #addClass}, {@link #hasClass}.
1181 *
1182 * @since 4.4
1183 * @returns {Object}
1184 */
1185 getClasses: function() {
1186 return this.repository.parseElementClasses( this.element.getAttribute( 'class' ) );
1187 },
1188
1189 /**
1190 * Checks if the widget element has specified class. This method is used by
1191 * the {@link #checkStyleActive} method and should be overriden by widgets
1192 * which should handle classes differently (e.g. on other elements).
1193 *
1194 * See also: {@link #removeClass}, {@link #addClass}, {@link #getClasses}.
1195 *
1196 * @since 4.4
1197 * @param {String} className The class to be checked.
1198 * @param {Boolean} Whether a widget has specified class.
1199 */
1200 hasClass: function( className ) {
1201 return this.element.hasClass( className );
1202 },
1203
1204 /**
1205 * Initializes a nested editable.
1206 *
1207 * **Note**: Only elements from {@link CKEDITOR.dtd#$editable} may become editables.
1208 *
1209 * @param {String} editableName The nested editable name.
1210 * @param {CKEDITOR.plugins.widget.nestedEditable.definition} definition The definition of the nested editable.
1211 * @returns {Boolean} Whether an editable was successfully initialized.
1212 */
1213 initEditable: function( editableName, definition ) {
1214 // Don't fetch just first element which matched selector but look for a correct one. (http://dev.ckeditor.com/ticket/13334)
1215 var editable = this._findOneNotNested( definition.selector );
1216
1217 if ( editable && editable.is( CKEDITOR.dtd.$editable ) ) {
1218 editable = new NestedEditable( this.editor, editable, {
1219 filter: createEditableFilter.call( this.repository, this.name, editableName, definition )
1220 } );
1221 this.editables[ editableName ] = editable;
1222
1223 editable.setAttributes( {
1224 contenteditable: 'true',
1225 'data-cke-widget-editable': editableName,
1226 'data-cke-enter-mode': editable.enterMode
1227 } );
1228
1229 if ( editable.filter )
1230 editable.data( 'cke-filter', editable.filter.id );
1231
1232 editable.addClass( 'cke_widget_editable' );
1233 // This class may be left when d&ding widget which
1234 // had focused editable. Clean this class here, not in
1235 // cleanUpWidgetElement for performance and code size reasons.
1236 editable.removeClass( 'cke_widget_editable_focused' );
1237
1238 if ( definition.pathName )
1239 editable.data( 'cke-display-name', definition.pathName );
1240
1241 this.editor.focusManager.add( editable );
1242 editable.on( 'focus', onEditableFocus, this );
1243 CKEDITOR.env.ie && editable.on( 'blur', onEditableBlur, this );
1244
1245 // Finally, process editable's data. This data wasn't processed when loading
1246 // editor's data, becuase they need to be processed separately, with its own filters and settings.
1247 editable._.initialSetData = true;
1248 editable.setData( editable.getHtml() );
1249
1250 return true;
1251 }
1252
1253 return false;
1254 },
1255
1256 /**
1257 * Looks inside wrapper element to find a node that
1258 * matches given selector and is not nested in other widget. (http://dev.ckeditor.com/ticket/13334)
1259 *
1260 * @since 4.5
1261 * @private
1262 * @param {String} selector Selector to match.
1263 * @returns {CKEDITOR.dom.element} Matched element or `null` if a node has not been found.
1264 */
1265 _findOneNotNested: function( selector ) {
1266 var matchedElements = this.wrapper.find( selector ),
1267 match,
1268 closestWrapper;
1269
1270 for ( var i = 0; i < matchedElements.count(); i++ ) {
1271 match = matchedElements.getItem( i );
1272 closestWrapper = match.getAscendant( Widget.isDomWidgetWrapper );
1273
1274 // The closest ascendant-wrapper of this match defines to which widget
1275 // this match belongs. If the ascendant is this widget's wrapper
1276 // it means that the match is not nested in other widget.
1277 if ( this.wrapper.equals( closestWrapper ) ) {
1278 return match;
1279 }
1280 }
1281
1282 return null;
1283 },
1284
1285 /**
1286 * Checks if a widget has already been initialized and has not been destroyed yet.
1287 *
1288 * See {@link #inited} for more details.
1289 *
1290 * @returns {Boolean}
1291 */
1292 isInited: function() {
1293 return !!( this.wrapper && this.inited );
1294 },
1295
1296 /**
1297 * Checks if a widget is ready and has not been destroyed yet.
1298 *
1299 * See {@link #property-ready} for more details.
1300 *
1301 * @returns {Boolean}
1302 */
1303 isReady: function() {
1304 return this.isInited() && this.ready;
1305 },
1306
1307 /**
1308 * Focuses a widget by selecting it.
1309 */
1310 focus: function() {
1311 var sel = this.editor.getSelection();
1312
1313 // Fake the selection before focusing editor, to avoid unpreventable viewports scrolling
1314 // on Webkit/Blink/IE which is done because there's no selection or selection was somewhere else than widget.
1315 if ( sel ) {
1316 var isDirty = this.editor.checkDirty();
1317
1318 sel.fake( this.wrapper );
1319
1320 !isDirty && this.editor.resetDirty();
1321 }
1322
1323 // Always focus editor (not only when focusManger.hasFocus is false) (because of http://dev.ckeditor.com/ticket/10483).
1324 this.editor.focus();
1325 },
1326
1327 /**
1328 * Removes a class from the widget element. This method is used by
1329 * the {@link #removeStyle} method and should be overriden by widgets
1330 * which should handle classes differently (e.g. on other elements).
1331 *
1332 * **Note**: This method should not be used directly. Use the {@link #setData} method to
1333 * set the `classes` property. Read more in the {@link #setData} documentation.
1334 *
1335 * See also: {@link #hasClass}, {@link #addClass}.
1336 *
1337 * @since 4.4
1338 * @param {String} className The class to be removed.
1339 */
1340 removeClass: function( className ) {
1341 this.element.removeClass( className );
1342 this.wrapper.removeClass( Widget.WRAPPER_CLASS_PREFIX + className );
1343 },
1344
1345 /**
1346 * Removes the specified style from the widget. It is highly recommended to use the
1347 * {@link CKEDITOR.editor#removeStyle} or {@link CKEDITOR.style#remove} methods instead of
1348 * using this method directly, because unlike editor's and style's methods, this one
1349 * does not perform any checks.
1350 *
1351 * Read more about how applying/removing styles works in the {@link #applyStyle} method documentation.
1352 *
1353 * See also {@link #checkStyleActive}, {@link #applyStyle}, {@link #getClasses}.
1354 *
1355 * @since 4.4
1356 * @param {CKEDITOR.style} style The custom widget style to be removed.
1357 */
1358 removeStyle: function( style ) {
1359 applyRemoveStyle( this, style, 0 );
1360 },
1361
1362 /**
1363 * Sets widget value(s) in the {@link #property-data} object.
1364 * If the given value(s) modifies current ones, the {@link #event-data} event is fired.
1365 *
1366 * this.setData( 'align', 'left' );
1367 * this.data.align; // -> 'left'
1368 *
1369 * this.setData( { align: 'right', opened: false } );
1370 * this.data.align; // -> 'right'
1371 * this.data.opened; // -> false
1372 *
1373 * Set values are stored in {@link #element}'s attribute (`data-cke-widget-data`),
1374 * in a JSON string, therefore {@link #property-data} should contain
1375 * only serializable data.
1376 *
1377 * **Note:** A special data property, `classes`, exists. It contains an object with
1378 * classes which were returned by the {@link #getClasses} method during the widget initialization.
1379 * This property is then used by the {@link #applyStyle} and {@link #removeStyle} methods.
1380 * When it is changed (the reference to object must be changed!), the widget updates its classes by
1381 * using the {@link #addClass} and {@link #removeClass} methods.
1382 *
1383 * // Adding a new class.
1384 * var classes = CKEDITOR.tools.clone( widget.data.classes );
1385 * classes.newClass = 1;
1386 * widget.setData( 'classes', classes );
1387 *
1388 * // Removing a class.
1389 * var classes = CKEDITOR.tools.clone( widget.data.classes );
1390 * delete classes.newClass;
1391 * widget.setData( 'classes', classes );
1392 *
1393 * @param {String/Object} keyOrData
1394 * @param {Object} value
1395 * @chainable
1396 */
1397 setData: function( key, value ) {
1398 var data = this.data,
1399 modified = 0;
1400
1401 if ( typeof key == 'string' ) {
1402 if ( data[ key ] !== value ) {
1403 data[ key ] = value;
1404 modified = 1;
1405 }
1406 }
1407 else {
1408 var newData = key;
1409
1410 for ( key in newData ) {
1411 if ( data[ key ] !== newData[ key ] ) {
1412 modified = 1;
1413 data[ key ] = newData[ key ];
1414 }
1415 }
1416 }
1417
1418 // Block firing data event and overwriting data element before setupWidgetData is executed.
1419 if ( modified && this.dataReady ) {
1420 writeDataToElement( this );
1421 this.fire( 'data', data );
1422 }
1423
1424 return this;
1425 },
1426
1427 /**
1428 * Changes the widget's focus state. This method is executed automatically after
1429 * a widget was focused by the {@link #method-focus} method or the selection was moved
1430 * out of the widget.
1431 *
1432 * This is a low-level method which is not integrated with e.g. the undo manager.
1433 * Use the {@link #method-focus} method instead.
1434 *
1435 * @param {Boolean} selected Whether to select or deselect this widget.
1436 * @chainable
1437 */
1438 setFocused: function( focused ) {
1439 this.wrapper[ focused ? 'addClass' : 'removeClass' ]( 'cke_widget_focused' );
1440 this.fire( focused ? 'focus' : 'blur' );
1441 return this;
1442 },
1443
1444 /**
1445 * Changes the widget's select state. This method is executed automatically after
1446 * a widget was selected by the {@link #method-focus} method or the selection
1447 * was moved out of the widget.
1448 *
1449 * This is a low-level method which is not integrated with e.g. the undo manager.
1450 * Use the {@link #method-focus} method instead or simply change the selection.
1451 *
1452 * @param {Boolean} selected Whether to select or deselect this widget.
1453 * @chainable
1454 */
1455 setSelected: function( selected ) {
1456 this.wrapper[ selected ? 'addClass' : 'removeClass' ]( 'cke_widget_selected' );
1457 this.fire( selected ? 'select' : 'deselect' );
1458 return this;
1459 },
1460
1461 /**
1462 * Repositions drag handler according to the widget's element position. Should be called from events, like mouseover.
1463 */
1464 updateDragHandlerPosition: function() {
1465 var editor = this.editor,
1466 domElement = this.element.$,
1467 oldPos = this._.dragHandlerOffset,
1468 newPos = {
1469 x: domElement.offsetLeft,
1470 y: domElement.offsetTop - DRAG_HANDLER_SIZE
1471 };
1472
1473 if ( oldPos && newPos.x == oldPos.x && newPos.y == oldPos.y )
1474 return;
1475
1476 // We need to make sure that dirty state is not changed (http://dev.ckeditor.com/ticket/11487).
1477 var initialDirty = editor.checkDirty();
1478
1479 editor.fire( 'lockSnapshot' );
1480 this.dragHandlerContainer.setStyles( {
1481 top: newPos.y + 'px',
1482 left: newPos.x + 'px',
1483 display: 'block'
1484 } );
1485 editor.fire( 'unlockSnapshot' );
1486 !initialDirty && editor.resetDirty();
1487
1488 this._.dragHandlerOffset = newPos;
1489 }
1490 };
1491
1492 CKEDITOR.event.implementOn( Widget.prototype );
1493
1494 /**
1495 * Gets the {@link #isDomNestedEditable nested editable}
1496 * (returned as a {@link CKEDITOR.dom.element}, not as a {@link CKEDITOR.plugins.widget.nestedEditable})
1497 * closest to the `node` or the `node` if it is a nested editable itself.
1498 *
1499 * @since 4.5
1500 * @static
1501 * @param {CKEDITOR.dom.element} guard Stop ancestor search on this node (usually editor's editable).
1502 * @param {CKEDITOR.dom.node} node Start the search from this node.
1503 * @returns {CKEDITOR.dom.element/null} Element or `null` if not found.
1504 */
1505 Widget.getNestedEditable = function( guard, node ) {
1506 if ( !node || node.equals( guard ) )
1507 return null;
1508
1509 if ( Widget.isDomNestedEditable( node ) )
1510 return node;
1511
1512 return Widget.getNestedEditable( guard, node.getParent() );
1513 };
1514
1515 /**
1516 * Checks whether the `node` is a widget's drag handle element.
1517 *
1518 * @since 4.5
1519 * @static
1520 * @param {CKEDITOR.dom.node} node
1521 * @returns {Boolean}
1522 */
1523 Widget.isDomDragHandler = function( node ) {
1524 return node.type == CKEDITOR.NODE_ELEMENT && node.hasAttribute( 'data-cke-widget-drag-handler' );
1525 };
1526
1527 /**
1528 * Checks whether the `node` is a container of the widget's drag handle element.
1529 *
1530 * @since 4.5
1531 * @static
1532 * @param {CKEDITOR.dom.node} node
1533 * @returns {Boolean}
1534 */
1535 Widget.isDomDragHandlerContainer = function( node ) {
1536 return node.type == CKEDITOR.NODE_ELEMENT && node.hasClass( 'cke_widget_drag_handler_container' );
1537 };
1538
1539 /**
1540 * Checks whether the `node` is a {@link CKEDITOR.plugins.widget#editables nested editable}.
1541 * Note that this function only checks whether it is the right element, not whether
1542 * the passed `node` is an instance of {@link CKEDITOR.plugins.widget.nestedEditable}.
1543 *
1544 * @since 4.5
1545 * @static
1546 * @param {CKEDITOR.dom.node} node
1547 * @returns {Boolean}
1548 */
1549 Widget.isDomNestedEditable = function( node ) {
1550 return node.type == CKEDITOR.NODE_ELEMENT && node.hasAttribute( 'data-cke-widget-editable' );
1551 };
1552
1553 /**
1554 * Checks whether the `node` is a {@link CKEDITOR.plugins.widget#element widget element}.
1555 *
1556 * @since 4.5
1557 * @static
1558 * @param {CKEDITOR.dom.node} node
1559 * @returns {Boolean}
1560 */
1561 Widget.isDomWidgetElement = function( node ) {
1562 return node.type == CKEDITOR.NODE_ELEMENT && node.hasAttribute( 'data-widget' );
1563 };
1564
1565 /**
1566 * Checks whether the `node` is a {@link CKEDITOR.plugins.widget#wrapper widget wrapper}.
1567 *
1568 * @since 4.5
1569 * @static
1570 * @param {CKEDITOR.dom.element} node
1571 * @returns {Boolean}
1572 */
1573 Widget.isDomWidgetWrapper = function( node ) {
1574 return node.type == CKEDITOR.NODE_ELEMENT && node.hasAttribute( 'data-cke-widget-wrapper' );
1575 };
1576
1577 /**
1578 * Checks whether the `node` is a {@link CKEDITOR.plugins.widget#element widget element}.
1579 *
1580 * @since 4.5
1581 * @static
1582 * @param {CKEDITOR.htmlParser.node} node
1583 * @returns {Boolean}
1584 */
1585 Widget.isParserWidgetElement = function( node ) {
1586 return node.type == CKEDITOR.NODE_ELEMENT && !!node.attributes[ 'data-widget' ];
1587 };
1588
1589 /**
1590 * Checks whether the `node` is a {@link CKEDITOR.plugins.widget#wrapper widget wrapper}.
1591 *
1592 * @since 4.5
1593 * @static
1594 * @param {CKEDITOR.htmlParser.element} node
1595 * @returns {Boolean}
1596 */
1597 Widget.isParserWidgetWrapper = function( node ) {
1598 return node.type == CKEDITOR.NODE_ELEMENT && !!node.attributes[ 'data-cke-widget-wrapper' ];
1599 };
1600
1601 /**
1602 * Prefix added to wrapper classes. Each class added to the widget element by the {@link #addClass}
1603 * method will also be added to the wrapper prefixed with it.
1604 *
1605 * @since 4.6.0
1606 * @static
1607 * @readonly
1608 * @property {String} [='cke_widget_wrapper_']
1609 */
1610 Widget.WRAPPER_CLASS_PREFIX = 'cke_widget_wrapper_';
1611
1612 /**
1613 * An event fired when a widget is ready (fully initialized). This event is fired after:
1614 *
1615 * * {@link #init} is called,
1616 * * The first {@link #event-data} event is fired,
1617 * * A widget is attached to the document.
1618 *
1619 * Therefore, in case of widget creation with a command which opens a dialog window, this event
1620 * will be delayed after the dialog window is closed and the widget is finally inserted into the document.
1621 *
1622 * **Note**: If your widget does not use automatic dialog window binding (i.e. you open the dialog window manually)
1623 * or another situation in which the widget wrapper is not attached to document at the time when it is
1624 * initialized occurs, you need to take care of firing {@link #event-ready} yourself.
1625 *
1626 * See also {@link #property-ready} and {@link #property-inited} properties, and
1627 * {@link #isReady} and {@link #isInited} methods.
1628 *
1629 * @event ready
1630 */
1631
1632 /**
1633 * An event fired when a widget is about to be destroyed, but before it is
1634 * fully torn down.
1635 *
1636 * @event destroy
1637 */
1638
1639 /**
1640 * An event fired when a widget is focused.
1641 *
1642 * Widget can be focused by executing {@link #method-focus}.
1643 *
1644 * @event focus
1645 */
1646
1647 /**
1648 * An event fired when a widget is blurred.
1649 *
1650 * @event blur
1651 */
1652
1653 /**
1654 * An event fired when a widget is selected.
1655 *
1656 * @event select
1657 */
1658
1659 /**
1660 * An event fired when a widget is deselected.
1661 *
1662 * @event deselect
1663 */
1664
1665 /**
1666 * An event fired by the {@link #method-edit} method. It can be canceled
1667 * in order to stop the default action (opening a dialog window and/or
1668 * {@link CKEDITOR.plugins.widget.repository#finalizeCreation finalizing widget creation}).
1669 *
1670 * @event edit
1671 * @param data
1672 * @param {String} data.dialog Defaults to {@link CKEDITOR.plugins.widget.definition#dialog}
1673 * and can be changed or set by the listener.
1674 */
1675
1676 /**
1677 * An event fired when a dialog window for widget editing is opened.
1678 * This event can be canceled in order to handle the editing dialog in a custom manner.
1679 *
1680 * @event dialog
1681 * @param {CKEDITOR.dialog} data The opened dialog window instance.
1682 */
1683
1684 /**
1685 * An event fired when a key is pressed on a focused widget.
1686 * This event is forwarded from the {@link CKEDITOR.editor#key} event and
1687 * has the ability to block editor keystrokes if it is canceled.
1688 *
1689 * @event key
1690 * @param data
1691 * @param {Number} data.keyCode A number representing the key code (or combination).
1692 */
1693
1694 /**
1695 * An event fired when a widget is double clicked.
1696 *
1697 * **Note:** If a default editing action is executed on double click (i.e. a widget has a
1698 * {@link CKEDITOR.plugins.widget.definition#dialog dialog} defined and the {@link #event-doubleclick} event was not
1699 * canceled), this event will be automatically canceled, so a listener added with the default priority (10)
1700 * will not be executed. Use a listener with low priority (e.g. 5) to be sure that it will be executed.
1701 *
1702 * widget.on( 'doubleclick', function( evt ) {
1703 * console.log( 'widget#doubleclick' );
1704 * }, null, null, 5 );
1705 *
1706 * If your widget handles double click in a special way (so the default editing action is not executed),
1707 * make sure you cancel this event, because otherwise it will be propagated to {@link CKEDITOR.editor#doubleclick}
1708 * and another feature may step in (e.g. a Link dialog window may be opened if your widget was inside a link).
1709 *
1710 * @event doubleclick
1711 * @param data
1712 * @param {CKEDITOR.dom.element} data.element The double-clicked element.
1713 */
1714
1715 /**
1716 * An event fired when the context menu is opened for a widget.
1717 *
1718 * @event contextMenu
1719 * @param data The object containing context menu options to be added
1720 * for this widget. See {@link CKEDITOR.plugins.contextMenu#addListener}.
1721 */
1722
1723 /**
1724 * An event fired when the widget data changed. See the {@link #setData} method and the {@link #property-data} property.
1725 *
1726 * @event data
1727 */
1728
1729
1730
1731 /**
1732 * The wrapper class for editable elements inside widgets.
1733 *
1734 * Do not use directly. Use {@link CKEDITOR.plugins.widget.definition#editables} or
1735 * {@link CKEDITOR.plugins.widget#initEditable}.
1736 *
1737 * @class CKEDITOR.plugins.widget.nestedEditable
1738 * @extends CKEDITOR.dom.element
1739 * @constructor
1740 * @param {CKEDITOR.editor} editor
1741 * @param {CKEDITOR.dom.element} element
1742 * @param config
1743 * @param {CKEDITOR.filter} [config.filter]
1744 */
1745 function NestedEditable( editor, element, config ) {
1746 // Call the base constructor.
1747 CKEDITOR.dom.element.call( this, element.$ );
1748 this.editor = editor;
1749 this._ = {};
1750 var filter = this.filter = config.filter;
1751
1752 // If blockless editable - always use BR mode.
1753 if ( !CKEDITOR.dtd[ this.getName() ].p )
1754 this.enterMode = this.shiftEnterMode = CKEDITOR.ENTER_BR;
1755 else {
1756 this.enterMode = filter ? filter.getAllowedEnterMode( editor.enterMode ) : editor.enterMode;
1757 this.shiftEnterMode = filter ? filter.getAllowedEnterMode( editor.shiftEnterMode, true ) : editor.shiftEnterMode;
1758 }
1759 }
1760
1761 NestedEditable.prototype = CKEDITOR.tools.extend( CKEDITOR.tools.prototypedCopy( CKEDITOR.dom.element.prototype ), {
1762 /**
1763 * Sets the editable data. The data will be passed through the {@link CKEDITOR.editor#dataProcessor}
1764 * and the {@link CKEDITOR.editor#filter}. This ensures that the data was filtered and prepared to be
1765 * edited like the {@link CKEDITOR.editor#method-setData editor data}.
1766 *
1767 * Before content is changed, all nested widgets are destroyed. Afterwards, after new content is loaded,
1768 * all nested widgets are initialized.
1769 *
1770 * @param {String} data
1771 */
1772 setData: function( data ) {
1773 // For performance reasons don't call destroyAll when initializing a nested editable,
1774 // because there are no widgets inside.
1775 if ( !this._.initialSetData ) {
1776 // Destroy all nested widgets before setting data.
1777 this.editor.widgets.destroyAll( false, this );
1778 }
1779 this._.initialSetData = false;
1780
1781 data = this.editor.dataProcessor.toHtml( data, {
1782 context: this.getName(),
1783 filter: this.filter,
1784 enterMode: this.enterMode
1785 } );
1786 this.setHtml( data );
1787
1788 this.editor.widgets.initOnAll( this );
1789 },
1790
1791 /**
1792 * Gets the editable data. Like {@link #setData}, this method will process and filter the data.
1793 *
1794 * @returns {String}
1795 */
1796 getData: function() {
1797 return this.editor.dataProcessor.toDataFormat( this.getHtml(), {
1798 context: this.getName(),
1799 filter: this.filter,
1800 enterMode: this.enterMode
1801 } );
1802 }
1803 } );
1804
1805 /**
1806 * The editor instance.
1807 *
1808 * @readonly
1809 * @property {CKEDITOR.editor} editor
1810 */
1811
1812 /**
1813 * The filter instance if allowed content rules were defined.
1814 *
1815 * @readonly
1816 * @property {CKEDITOR.filter} filter
1817 */
1818
1819 /**
1820 * The enter mode active in this editable.
1821 * It is determined from editable's name (whether it is a blockless editable),
1822 * its allowed content rules (if defined) and the default editor's mode.
1823 *
1824 * @readonly
1825 * @property {Number} enterMode
1826 */
1827
1828 /**
1829 * The shift enter move active in this editable.
1830 *
1831 * @readonly
1832 * @property {Number} shiftEnterMode
1833 */
1834
1835
1836 //
1837 // REPOSITORY helpers -----------------------------------------------------
1838 //
1839
1840 function addWidgetButtons( editor ) {
1841 var widgets = editor.widgets.registered,
1842 widget,
1843 widgetName,
1844 widgetButton;
1845
1846 for ( widgetName in widgets ) {
1847 widget = widgets[ widgetName ];
1848
1849 // Create button if defined.
1850 widgetButton = widget.button;
1851 if ( widgetButton && editor.ui.addButton ) {
1852 editor.ui.addButton( CKEDITOR.tools.capitalize( widget.name, true ), {
1853 label: widgetButton,
1854 command: widget.name,
1855 toolbar: 'insert,10'
1856 } );
1857 }
1858 }
1859 }
1860
1861 // Create a command creating and editing widget.
1862 //
1863 // @param editor
1864 // @param {CKEDITOR.plugins.widget.definition} widgetDef
1865 function addWidgetCommand( editor, widgetDef ) {
1866 editor.addCommand( widgetDef.name, {
1867 exec: function( editor, commandData ) {
1868 var focused = editor.widgets.focused;
1869 // If a widget of the same type is focused, start editing.
1870 if ( focused && focused.name == widgetDef.name )
1871 focused.edit();
1872 // Otherwise...
1873 // ... use insert method is was defined.
1874 else if ( widgetDef.insert )
1875 widgetDef.insert();
1876 // ... or create a brand-new widget from template.
1877 else if ( widgetDef.template ) {
1878 var defaults = typeof widgetDef.defaults == 'function' ? widgetDef.defaults() : widgetDef.defaults,
1879 element = CKEDITOR.dom.element.createFromHtml( widgetDef.template.output( defaults ) ),
1880 instance,
1881 wrapper = editor.widgets.wrapElement( element, widgetDef.name ),
1882 temp = new CKEDITOR.dom.documentFragment( wrapper.getDocument() );
1883
1884 // Append wrapper to a temporary document. This will unify the environment
1885 // in which #data listeners work when creating and editing widget.
1886 temp.append( wrapper );
1887 instance = editor.widgets.initOn( element, widgetDef, commandData && commandData.startupData );
1888
1889 // Instance could be destroyed during initialization.
1890 // In this case finalize creation if some new widget
1891 // was left in temporary document fragment.
1892 if ( !instance ) {
1893 finalizeCreation();
1894 return;
1895 }
1896
1897 // Listen on edit to finalize widget insertion.
1898 //
1899 // * If dialog was set, then insert widget after dialog was successfully saved or destroy this
1900 // temporary instance.
1901 // * If dialog wasn't set and edit wasn't canceled, insert widget.
1902 var editListener = instance.once( 'edit', function( evt ) {
1903 if ( evt.data.dialog ) {
1904 instance.once( 'dialog', function( evt ) {
1905 var dialog = evt.data,
1906 okListener,
1907 cancelListener;
1908
1909 // Finalize creation AFTER (20) new data was set.
1910 okListener = dialog.once( 'ok', finalizeCreation, null, null, 20 );
1911
1912 cancelListener = dialog.once( 'cancel', function( evt ) {
1913 if ( !( evt.data && evt.data.hide === false ) ) {
1914 editor.widgets.destroy( instance, true );
1915 }
1916 } );
1917
1918 dialog.once( 'hide', function() {
1919 okListener.removeListener();
1920 cancelListener.removeListener();
1921 } );
1922 } );
1923 } else {
1924 // Dialog hasn't been set, so insert widget now.
1925 finalizeCreation();
1926 }
1927 }, null, null, 999 );
1928
1929 instance.edit();
1930
1931 // Remove listener in case someone canceled it before this
1932 // listener was executed.
1933 editListener.removeListener();
1934 }
1935
1936 function finalizeCreation() {
1937 editor.widgets.finalizeCreation( temp );
1938 }
1939 },
1940
1941 allowedContent: widgetDef.allowedContent,
1942 requiredContent: widgetDef.requiredContent,
1943 contentForms: widgetDef.contentForms,
1944 contentTransformations: widgetDef.contentTransformations
1945 } );
1946 }
1947
1948 function addWidgetProcessors( widgetsRepo, widgetDef ) {
1949 var upcast = widgetDef.upcast,
1950 upcasts,
1951 priority = widgetDef.upcastPriority || 10;
1952
1953 if ( !upcast )
1954 return;
1955
1956 // Multiple upcasts defined in string.
1957 if ( typeof upcast == 'string' ) {
1958 upcasts = upcast.split( ',' );
1959 while ( upcasts.length ) {
1960 addUpcast( widgetDef.upcasts[ upcasts.pop() ], widgetDef.name, priority );
1961 }
1962 }
1963 // Single rule which is automatically activated.
1964 else {
1965 addUpcast( upcast, widgetDef.name, priority );
1966 }
1967
1968 function addUpcast( upcast, name, priority ) {
1969 // Find index of the first higher (in terms of value) priority upcast.
1970 var index = CKEDITOR.tools.getIndex( widgetsRepo._.upcasts, function( element ) {
1971 return element[ 2 ] > priority;
1972 } );
1973 // Add at the end if it is the highest priority so far.
1974 if ( index < 0 ) {
1975 index = widgetsRepo._.upcasts.length;
1976 }
1977
1978 widgetsRepo._.upcasts.splice( index, 0, [ upcast, name, priority ] );
1979 }
1980 }
1981
1982 function blurWidget( widgetsRepo, widget ) {
1983 widgetsRepo.focused = null;
1984
1985 if ( widget.isInited() ) {
1986 var isDirty = widget.editor.checkDirty();
1987
1988 // Widget could be destroyed in the meantime - e.g. data could be set.
1989 widgetsRepo.fire( 'widgetBlurred', { widget: widget } );
1990 widget.setFocused( false );
1991
1992 !isDirty && widget.editor.resetDirty();
1993 }
1994 }
1995
1996 function checkWidgets( evt ) {
1997 var options = evt.data;
1998
1999 if ( this.editor.mode != 'wysiwyg' )
2000 return;
2001
2002 var editable = this.editor.editable(),
2003 instances = this.instances,
2004 newInstances, i, count, wrapper, notYetInitialized;
2005
2006 if ( !editable )
2007 return;
2008
2009 // Remove widgets which have no corresponding elements in DOM.
2010 for ( i in instances ) {
2011 // http://dev.ckeditor.com/ticket/13410 Remove widgets that are ready. This prevents from destroying widgets that are during loading process.
2012 if ( instances[ i ].isReady() && !editable.contains( instances[ i ].wrapper ) )
2013 this.destroy( instances[ i ], true );
2014 }
2015
2016 // Init on all (new) if initOnlyNew option was passed.
2017 if ( options && options.initOnlyNew )
2018 newInstances = this.initOnAll();
2019 else {
2020 var wrappers = editable.find( '.cke_widget_wrapper' );
2021 newInstances = [];
2022
2023 // Create widgets on existing wrappers if they do not exists.
2024 for ( i = 0, count = wrappers.count(); i < count; i++ ) {
2025 wrapper = wrappers.getItem( i );
2026 notYetInitialized = !this.getByElement( wrapper, true );
2027
2028 // Check if:
2029 // * there's no instance for this widget
2030 // * wrapper is not inside some temporary element like copybin (http://dev.ckeditor.com/ticket/11088)
2031 // * it was a nested widget's wrapper which has been detached from DOM,
2032 // when nested editable has been initialized (it overwrites its innerHTML
2033 // and initializes nested widgets).
2034 if ( notYetInitialized && !findParent( wrapper, isDomTemp ) && editable.contains( wrapper ) ) {
2035 // Add cke_widget_new class because otherwise
2036 // widget will not be created on such wrapper.
2037 wrapper.addClass( 'cke_widget_new' );
2038 newInstances.push( this.initOn( wrapper.getFirst( Widget.isDomWidgetElement ) ) );
2039 }
2040 }
2041 }
2042
2043 // If only single widget was initialized and focusInited was passed, focus it.
2044 if ( options && options.focusInited && newInstances.length == 1 )
2045 newInstances[ 0 ].focus();
2046 }
2047
2048 // Unwraps widget element and clean up element.
2049 //
2050 // This function is used to clean up pasted widgets.
2051 // It should have similar result to widget#destroy plus
2052 // some additional adjustments, specific for pasting.
2053 //
2054 // @param {CKEDITOR.htmlParser.element} el
2055 function cleanUpWidgetElement( el ) {
2056 var parent = el.parent;
2057 if ( parent.type == CKEDITOR.NODE_ELEMENT && parent.attributes[ 'data-cke-widget-wrapper' ] )
2058 parent.replaceWith( el );
2059 }
2060
2061 // Similar to cleanUpWidgetElement, but works on DOM and finds
2062 // widget elements by its own.
2063 //
2064 // Unlike cleanUpWidgetElement it will wrap element back.
2065 //
2066 // @param {CKEDITOR.dom.element} container
2067 function cleanUpAllWidgetElements( widgetsRepo, container ) {
2068 var wrappers = container.find( '.cke_widget_wrapper' ),
2069 wrapper, element,
2070 i = 0,
2071 l = wrappers.count();
2072
2073 for ( ; i < l; ++i ) {
2074 wrapper = wrappers.getItem( i );
2075 element = wrapper.getFirst( Widget.isDomWidgetElement );
2076 // If wrapper contains widget element - unwrap it and wrap again.
2077 if ( element.type == CKEDITOR.NODE_ELEMENT && element.data( 'widget' ) ) {
2078 element.replace( wrapper );
2079 widgetsRepo.wrapElement( element );
2080 } else {
2081 // Otherwise - something is wrong... clean this up.
2082 wrapper.remove();
2083 }
2084 }
2085 }
2086
2087 // Creates {@link CKEDITOR.filter} instance for given widget, editable and rules.
2088 //
2089 // Once filter for widget-editable pair is created it is cached, so the same instance
2090 // will be returned when method is executed again.
2091 //
2092 // @param {String} widgetName
2093 // @param {String} editableName
2094 // @param {CKEDITOR.plugins.widget.nestedEditableDefinition} editableDefinition The nested editable definition.
2095 // @returns {CKEDITOR.filter} Filter instance or `null` if rules are not defined.
2096 // @context CKEDITOR.plugins.widget.repository
2097 function createEditableFilter( widgetName, editableName, editableDefinition ) {
2098 if ( !editableDefinition.allowedContent && !editableDefinition.disallowedContent )
2099 return null;
2100
2101 var editables = this._.filters[ widgetName ];
2102
2103 if ( !editables )
2104 this._.filters[ widgetName ] = editables = {};
2105
2106 var filter = editables[ editableName ];
2107
2108 if ( !filter ) {
2109 filter = editableDefinition.allowedContent ? new CKEDITOR.filter( editableDefinition.allowedContent ) : this.editor.filter.clone();
2110
2111 editables[ editableName ] = filter;
2112
2113 if ( editableDefinition.disallowedContent ) {
2114 filter.disallow( editableDefinition.disallowedContent );
2115 }
2116 }
2117
2118 return filter;
2119 }
2120
2121 // Creates an iterator function which when executed on all
2122 // elements in DOM tree will gather elements that should be wrapped
2123 // and initialized as widgets.
2124 function createUpcastIterator( widgetsRepo ) {
2125 var toBeWrapped = [],
2126 upcasts = widgetsRepo._.upcasts,
2127 upcastCallbacks = widgetsRepo._.upcastCallbacks;
2128
2129 return {
2130 toBeWrapped: toBeWrapped,
2131
2132 iterator: function( element ) {
2133 var upcast, upcasted,
2134 data,
2135 i,
2136 upcastsLength,
2137 upcastCallbacksLength;
2138
2139 // Wrapper found - find widget element, add it to be
2140 // cleaned up (unwrapped) and wrapped and stop iterating in this branch.
2141 if ( 'data-cke-widget-wrapper' in element.attributes ) {
2142 element = element.getFirst( Widget.isParserWidgetElement );
2143
2144 if ( element )
2145 toBeWrapped.push( [ element ] );
2146
2147 // Do not iterate over descendants.
2148 return false;
2149 }
2150 // Widget element found - add it to be cleaned up (just in case)
2151 // and wrapped and stop iterating in this branch.
2152 else if ( 'data-widget' in element.attributes ) {
2153 toBeWrapped.push( [ element ] );
2154
2155 // Do not iterate over descendants.
2156 return false;
2157 }
2158 else if ( ( upcastsLength = upcasts.length ) ) {
2159 // Ignore elements with data-cke-widget-upcasted to avoid multiple upcasts (http://dev.ckeditor.com/ticket/11533).
2160 // Do not iterate over descendants.
2161 if ( element.attributes[ 'data-cke-widget-upcasted' ] )
2162 return false;
2163
2164 // Check element with upcast callbacks first.
2165 // If any of them return false abort upcasting.
2166 for ( i = 0, upcastCallbacksLength = upcastCallbacks.length; i < upcastCallbacksLength; ++i ) {
2167 if ( upcastCallbacks[ i ]( element ) === false )
2168 return;
2169 // Return nothing in order to continue iterating over ascendants.
2170 // See http://dev.ckeditor.com/ticket/11186#comment:6
2171 }
2172
2173 for ( i = 0; i < upcastsLength; ++i ) {
2174 upcast = upcasts[ i ];
2175 data = {};
2176
2177 if ( ( upcasted = upcast[ 0 ]( element, data ) ) ) {
2178 // If upcast function returned element, upcast this one.
2179 // It can be e.g. a new element wrapping the original one.
2180 if ( upcasted instanceof CKEDITOR.htmlParser.element )
2181 element = upcasted;
2182
2183 // Set initial data attr with data from upcast method.
2184 element.attributes[ 'data-cke-widget-data' ] = encodeURIComponent( JSON.stringify( data ) );
2185 element.attributes[ 'data-cke-widget-upcasted' ] = 1;
2186
2187 toBeWrapped.push( [ element, upcast[ 1 ] ] );
2188
2189 // Do not iterate over descendants.
2190 return false;
2191 }
2192 }
2193 }
2194 }
2195 };
2196 }
2197
2198 // Finds a first parent that matches query.
2199 //
2200 // @param {CKEDITOR.dom.element} element
2201 // @param {Function} query
2202 function findParent( element, query ) {
2203 var parent = element;
2204
2205 while ( ( parent = parent.getParent() ) ) {
2206 if ( query( parent ) )
2207 return true;
2208 }
2209 return false;
2210 }
2211
2212 function getWrapperAttributes( inlineWidget, name ) {
2213 return {
2214 // tabindex="-1" means that it can receive focus by code.
2215 tabindex: -1,
2216 contenteditable: 'false',
2217 'data-cke-widget-wrapper': 1,
2218 'data-cke-filter': 'off',
2219 // Class cke_widget_new marks widgets which haven't been initialized yet.
2220 'class': 'cke_widget_wrapper cke_widget_new cke_widget_' +
2221 ( inlineWidget ? 'inline' : 'block' ) +
2222 ( name ? ' cke_widget_' + name : '' )
2223 };
2224 }
2225
2226 // Inserts element at given index.
2227 // It will check DTD and split ancestor elements up to the first
2228 // that can contain this element.
2229 //
2230 // @param {CKEDITOR.htmlParser.element} parent
2231 // @param {Number} index
2232 // @param {CKEDITOR.htmlParser.element} element
2233 function insertElement( parent, index, element ) {
2234 // Do not split doc fragment...
2235 if ( parent.type == CKEDITOR.NODE_ELEMENT ) {
2236 var parentAllows = CKEDITOR.dtd[ parent.name ];
2237 // Parent element is known (included in DTD) and cannot contain
2238 // this element.
2239 if ( parentAllows && !parentAllows[ element.name ] ) {
2240 var parent2 = parent.split( index ),
2241 parentParent = parent.parent;
2242
2243 // Element will now be inserted at right parent's index.
2244 index = parent2.getIndex();
2245
2246 // If left part of split is empty - remove it.
2247 if ( !parent.children.length ) {
2248 index -= 1;
2249 parent.remove();
2250 }
2251
2252 // If right part of split is empty - remove it.
2253 if ( !parent2.children.length )
2254 parent2.remove();
2255
2256 // Try inserting as grandpas' children.
2257 return insertElement( parentParent, index, element );
2258 }
2259 }
2260
2261 // Finally we can add this element.
2262 parent.add( element, index );
2263 }
2264
2265 // Checks whether for the given widget definition and element widget should be created in inline or block mode.
2266 //
2267 // See also: {@link CKEDITOR.plugins.widget.definition#inline} and {@link CKEDITOR.plugins.widget#element}.
2268 //
2269 // @param {CKEDITOR.plugins.widget.definition} widgetDef The widget definition.
2270 // @param {String} elementName The name of the widget element.
2271 // @returns {Boolean}
2272 function isWidgetInline( widgetDef, elementName ) {
2273 return typeof widgetDef.inline == 'boolean' ? widgetDef.inline : !!CKEDITOR.dtd.$inline[ elementName ];
2274 }
2275
2276 // @param {CKEDITOR.dom.element}
2277 // @returns {Boolean}
2278 function isDomTemp( element ) {
2279 return element.hasAttribute( 'data-cke-temp' );
2280 }
2281
2282 function onEditableKey( widget, keyCode ) {
2283 var focusedEditable = widget.focusedEditable,
2284 range;
2285
2286 // CTRL+A.
2287 if ( keyCode == CKEDITOR.CTRL + 65 ) {
2288 var bogus = focusedEditable.getBogus();
2289
2290 range = widget.editor.createRange();
2291 range.selectNodeContents( focusedEditable );
2292 // Exclude bogus if exists.
2293 if ( bogus )
2294 range.setEndAt( bogus, CKEDITOR.POSITION_BEFORE_START );
2295
2296 range.select();
2297 // Cancel event - block default.
2298 return false;
2299 }
2300 // DEL or BACKSPACE.
2301 else if ( keyCode == 8 || keyCode == 46 ) {
2302 var ranges = widget.editor.getSelection().getRanges();
2303
2304 range = ranges[ 0 ];
2305
2306 // Block del or backspace if at editable's boundary.
2307 return !( ranges.length == 1 && range.collapsed &&
2308 range.checkBoundaryOfElement( focusedEditable, CKEDITOR[ keyCode == 8 ? 'START' : 'END' ] ) );
2309 }
2310 }
2311
2312 function setFocusedEditable( widgetsRepo, widget, editableElement, offline ) {
2313 var editor = widgetsRepo.editor;
2314
2315 editor.fire( 'lockSnapshot' );
2316
2317 if ( editableElement ) {
2318 var editableName = editableElement.data( 'cke-widget-editable' ),
2319 editableInstance = widget.editables[ editableName ];
2320
2321 widgetsRepo.widgetHoldingFocusedEditable = widget;
2322 widget.focusedEditable = editableInstance;
2323 editableElement.addClass( 'cke_widget_editable_focused' );
2324
2325 if ( editableInstance.filter )
2326 editor.setActiveFilter( editableInstance.filter );
2327 editor.setActiveEnterMode( editableInstance.enterMode, editableInstance.shiftEnterMode );
2328 } else {
2329 if ( !offline )
2330 widget.focusedEditable.removeClass( 'cke_widget_editable_focused' );
2331
2332 widget.focusedEditable = null;
2333 widgetsRepo.widgetHoldingFocusedEditable = null;
2334 editor.setActiveFilter( null );
2335 editor.setActiveEnterMode( null, null );
2336 }
2337
2338 editor.fire( 'unlockSnapshot' );
2339 }
2340
2341 function setupContextMenu( editor ) {
2342 if ( !editor.contextMenu )
2343 return;
2344
2345 editor.contextMenu.addListener( function( element ) {
2346 var widget = editor.widgets.getByElement( element, true );
2347
2348 if ( widget )
2349 return widget.fire( 'contextMenu', {} );
2350 } );
2351 }
2352
2353 // And now we've got two problems - original problem and RegExp.
2354 // Some softeners:
2355 // * FF tends to copy all blocks up to the copybin container.
2356 // * IE tends to copy only the copybin, without its container.
2357 // * We use spans on IE and blockless editors, but divs in other cases.
2358 var pasteReplaceRegex = new RegExp(
2359 '^' +
2360 '(?:<(?:div|span)(?: data-cke-temp="1")?(?: id="cke_copybin")?(?: data-cke-temp="1")?>)?' +
2361 '(?:<(?:div|span)(?: style="[^"]+")?>)?' +
2362 '<span [^>]*data-cke-copybin-start="1"[^>]*>.?</span>([\\s\\S]+)<span [^>]*data-cke-copybin-end="1"[^>]*>.?</span>' +
2363 '(?:</(?:div|span)>)?' +
2364 '(?:</(?:div|span)>)?' +
2365 '$',
2366 // IE8 prefers uppercase when browsers stick to lowercase HTML (http://dev.ckeditor.com/ticket/13460).
2367 'i'
2368 );
2369
2370 function pasteReplaceFn( match, wrapperHtml ) {
2371 // Avoid polluting pasted data with any whitspaces,
2372 // what's going to break check whether only one widget was pasted.
2373 return CKEDITOR.tools.trim( wrapperHtml );
2374 }
2375
2376 function setupDragAndDrop( widgetsRepo ) {
2377 var editor = widgetsRepo.editor,
2378 lineutils = CKEDITOR.plugins.lineutils;
2379
2380 // These listeners handle inline and block widgets drag and drop.
2381 // The only thing we need to do to make block widgets custom drag and drop functionality
2382 // is to fire those events with the right properties (like the target which must be the drag handle).
2383 editor.on( 'dragstart', function( evt ) {
2384 var target = evt.data.target;
2385
2386 if ( Widget.isDomDragHandler( target ) ) {
2387 var widget = widgetsRepo.getByElement( target );
2388
2389 evt.data.dataTransfer.setData( 'cke/widget-id', widget.id );
2390
2391 // IE needs focus.
2392 editor.focus();
2393
2394 // and widget need to be focused on drag start (http://dev.ckeditor.com/ticket/12172#comment:10).
2395 widget.focus();
2396 }
2397 } );
2398
2399 editor.on( 'drop', function( evt ) {
2400 var dataTransfer = evt.data.dataTransfer,
2401 id = dataTransfer.getData( 'cke/widget-id' ),
2402 transferType = dataTransfer.getTransferType( editor ),
2403 dragRange = editor.createRange(),
2404 sourceWidget;
2405
2406 // Disable cross-editor drag & drop for widgets - http://dev.ckeditor.com/ticket/13599.
2407 if ( id !== '' && transferType === CKEDITOR.DATA_TRANSFER_CROSS_EDITORS ) {
2408 evt.cancel();
2409 return;
2410 }
2411
2412 if ( id === '' || transferType != CKEDITOR.DATA_TRANSFER_INTERNAL ) {
2413 return;
2414 }
2415
2416 sourceWidget = widgetsRepo.instances[ id ];
2417 if ( !sourceWidget ) {
2418 return;
2419 }
2420
2421 dragRange.setStartBefore( sourceWidget.wrapper );
2422 dragRange.setEndAfter( sourceWidget.wrapper );
2423 evt.data.dragRange = dragRange;
2424
2425 // [IE8-9] Reset state of the clipboard#fixSplitNodesAfterDrop fix because by setting evt.data.dragRange
2426 // (see above) after drop happened we do not need it. That fix is needed only if dragRange was created
2427 // before drop (before text node was split).
2428 delete CKEDITOR.plugins.clipboard.dragStartContainerChildCount;
2429 delete CKEDITOR.plugins.clipboard.dragEndContainerChildCount;
2430
2431 evt.data.dataTransfer.setData( 'text/html', editor.editable().getHtmlFromRange( dragRange ).getHtml() );
2432 editor.widgets.destroy( sourceWidget, true );
2433 } );
2434
2435 editor.on( 'contentDom', function() {
2436 var editable = editor.editable();
2437
2438 // Register Lineutils's utilities as properties of repo.
2439 CKEDITOR.tools.extend( widgetsRepo, {
2440 finder: new lineutils.finder( editor, {
2441 lookups: {
2442 // Element is block but not list item and not in nested editable.
2443 'default': function( el ) {
2444 if ( el.is( CKEDITOR.dtd.$listItem ) )
2445 return;
2446
2447 if ( !el.is( CKEDITOR.dtd.$block ) )
2448 return;
2449
2450 // Allow drop line inside, but never before or after nested editable (http://dev.ckeditor.com/ticket/12006).
2451 if ( Widget.isDomNestedEditable( el ) )
2452 return;
2453
2454 // Do not allow droping inside the widget being dragged (http://dev.ckeditor.com/ticket/13397).
2455 if ( widgetsRepo._.draggedWidget.wrapper.contains( el ) ) {
2456 return;
2457 }
2458
2459 // If element is nested editable, make sure widget can be dropped there (http://dev.ckeditor.com/ticket/12006).
2460 var nestedEditable = Widget.getNestedEditable( editable, el );
2461 if ( nestedEditable ) {
2462 var draggedWidget = widgetsRepo._.draggedWidget;
2463
2464 // Don't let the widget to be dropped into its own nested editable.
2465 if ( widgetsRepo.getByElement( nestedEditable ) == draggedWidget )
2466 return;
2467
2468 var filter = CKEDITOR.filter.instances[ nestedEditable.data( 'cke-filter' ) ],
2469 draggedRequiredContent = draggedWidget.requiredContent;
2470
2471 // There will be no relation if the filter of nested editable does not allow
2472 // requiredContent of dragged widget.
2473 if ( filter && draggedRequiredContent && !filter.check( draggedRequiredContent ) )
2474 return;
2475 }
2476
2477 return CKEDITOR.LINEUTILS_BEFORE | CKEDITOR.LINEUTILS_AFTER;
2478 }
2479 }
2480 } ),
2481 locator: new lineutils.locator( editor ),
2482 liner: new lineutils.liner( editor, {
2483 lineStyle: {
2484 cursor: 'move !important',
2485 'border-top-color': '#666'
2486 },
2487 tipLeftStyle: {
2488 'border-left-color': '#666'
2489 },
2490 tipRightStyle: {
2491 'border-right-color': '#666'
2492 }
2493 } )
2494 }, true );
2495 } );
2496 }
2497
2498 // Setup mouse observer which will trigger:
2499 // * widget focus on widget click,
2500 // * widget#doubleclick forwarded from editor#doubleclick.
2501 function setupMouseObserver( widgetsRepo ) {
2502 var editor = widgetsRepo.editor;
2503
2504 editor.on( 'contentDom', function() {
2505 var editable = editor.editable(),
2506 evtRoot = editable.isInline() ? editable : editor.document,
2507 widget,
2508 mouseDownOnDragHandler;
2509
2510 editable.attachListener( evtRoot, 'mousedown', function( evt ) {
2511 var target = evt.data.getTarget();
2512
2513 // Clicking scrollbar in Chrome will invoke event with target object of document type (#663).
2514 // In IE8 the target object will be empty (http://dev.ckeditor.com/ticket/10887).
2515 // We need to check if target is a proper element.
2516 widget = ( target instanceof CKEDITOR.dom.element ) ? widgetsRepo.getByElement( target ) : null;
2517
2518 mouseDownOnDragHandler = 0; // Reset.
2519
2520 // Widget was clicked, but not editable nested in it.
2521 if ( widget ) {
2522 // Ignore mousedown on drag and drop handler if the widget is inline.
2523 // Block widgets are handled by Lineutils.
2524 if ( widget.inline && target.type == CKEDITOR.NODE_ELEMENT && target.hasAttribute( 'data-cke-widget-drag-handler' ) ) {
2525 mouseDownOnDragHandler = 1;
2526
2527 // When drag handler is pressed we have to clear current selection if it wasn't already on this widget.
2528 // Otherwise, the selection may be in a fillingChar, which prevents dragging a widget. (http://dev.ckeditor.com/ticket/13284, see comment 8 and 9.)
2529 if ( widgetsRepo.focused != widget )
2530 editor.getSelection().removeAllRanges();
2531
2532 return;
2533 }
2534
2535 if ( !Widget.getNestedEditable( widget.wrapper, target ) ) {
2536 evt.data.preventDefault();
2537 if ( !CKEDITOR.env.ie )
2538 widget.focus();
2539 } else {
2540 // Reset widget so mouseup listener is not confused.
2541 widget = null;
2542 }
2543 }
2544 } );
2545
2546 // Focus widget on mouseup if mousedown was fired on drag handler.
2547 // Note: mouseup won't be fired at all if widget was dragged and dropped, so
2548 // this code will be executed only when drag handler was clicked.
2549 editable.attachListener( evtRoot, 'mouseup', function() {
2550 // Check if widget is not destroyed (if widget is destroyed the wrapper will be null).
2551 if ( mouseDownOnDragHandler && widget && widget.wrapper ) {
2552 mouseDownOnDragHandler = 0;
2553 widget.focus();
2554 }
2555 } );
2556
2557 // On IE it is not enough to block mousedown. If widget wrapper (element with
2558 // contenteditable=false attribute) is clicked directly (it is a target),
2559 // then after mouseup/click IE will select that element.
2560 // It is not possible to prevent that default action,
2561 // so we force fake selection after everything happened.
2562 if ( CKEDITOR.env.ie ) {
2563 editable.attachListener( evtRoot, 'mouseup', function() {
2564 setTimeout( function() {
2565 // Check if widget is not destroyed (if widget is destroyed the wrapper will be null) and
2566 // in editable contains widget (it could be dragged and removed).
2567 if ( widget && widget.wrapper && editable.contains( widget.wrapper ) ) {
2568 widget.focus();
2569 widget = null;
2570 }
2571 } );
2572 } );
2573 }
2574 } );
2575
2576 editor.on( 'doubleclick', function( evt ) {
2577 var widget = widgetsRepo.getByElement( evt.data.element );
2578
2579 // Not in widget or in nested editable.
2580 if ( !widget || Widget.getNestedEditable( widget.wrapper, evt.data.element ) )
2581 return;
2582
2583 return widget.fire( 'doubleclick', { element: evt.data.element } );
2584 }, null, null, 1 );
2585 }
2586
2587 // Setup editor#key observer which will forward it
2588 // to focused widget.
2589 function setupKeyboardObserver( widgetsRepo ) {
2590 var editor = widgetsRepo.editor;
2591
2592 editor.on( 'key', function( evt ) {
2593 var focused = widgetsRepo.focused,
2594 widgetHoldingFocusedEditable = widgetsRepo.widgetHoldingFocusedEditable,
2595 ret;
2596
2597 if ( focused )
2598 ret = focused.fire( 'key', { keyCode: evt.data.keyCode } );
2599 else if ( widgetHoldingFocusedEditable )
2600 ret = onEditableKey( widgetHoldingFocusedEditable, evt.data.keyCode );
2601
2602 return ret;
2603 }, null, null, 1 );
2604 }
2605
2606 // Setup copybin on native copy and cut events in order to handle copy and cut commands
2607 // if user accepted security alert on IEs.
2608 // Note: when copying or cutting using keystroke, copySingleWidget will be first executed
2609 // by the keydown listener. Conflict between two calls will be resolved by copy_bin existence check.
2610 function setupNativeCutAndCopy( widgetsRepo ) {
2611 var editor = widgetsRepo.editor;
2612
2613 editor.on( 'contentDom', function() {
2614 var editable = editor.editable();
2615
2616 editable.attachListener( editable, 'copy', eventListener );
2617 editable.attachListener( editable, 'cut', eventListener );
2618 } );
2619
2620 function eventListener( evt ) {
2621 if ( widgetsRepo.focused )
2622 copySingleWidget( widgetsRepo.focused, evt.name == 'cut' );
2623 }
2624 }
2625
2626 // Setup selection observer which will trigger:
2627 // * widget select & focus on selection change,
2628 // * nested editable focus (related properites and classes) on selection change,
2629 // * deselecting and blurring all widgets on data,
2630 // * blurring widget on editor blur.
2631 function setupSelectionObserver( widgetsRepo ) {
2632 var editor = widgetsRepo.editor;
2633
2634 editor.on( 'selectionCheck', function() {
2635 widgetsRepo.fire( 'checkSelection' );
2636 } );
2637
2638 widgetsRepo.on( 'checkSelection', widgetsRepo.checkSelection, widgetsRepo );
2639
2640 editor.on( 'selectionChange', function( evt ) {
2641 var nestedEditable = Widget.getNestedEditable( editor.editable(), evt.data.selection.getStartElement() ),
2642 newWidget = nestedEditable && widgetsRepo.getByElement( nestedEditable ),
2643 oldWidget = widgetsRepo.widgetHoldingFocusedEditable;
2644
2645 if ( oldWidget ) {
2646 if ( oldWidget !== newWidget || !oldWidget.focusedEditable.equals( nestedEditable ) ) {
2647 setFocusedEditable( widgetsRepo, oldWidget, null );
2648
2649 if ( newWidget && nestedEditable )
2650 setFocusedEditable( widgetsRepo, newWidget, nestedEditable );
2651 }
2652 }
2653 // It may happen that there's no widget even if editable was found -
2654 // e.g. if selection was automatically set in editable although widget wasn't initialized yet.
2655 else if ( newWidget && nestedEditable ) {
2656 setFocusedEditable( widgetsRepo, newWidget, nestedEditable );
2657 }
2658 } );
2659
2660 // Invalidate old widgets early - immediately on dataReady.
2661 editor.on( 'dataReady', function() {
2662 // Deselect and blur all widgets.
2663 stateUpdater( widgetsRepo ).commit();
2664 } );
2665
2666 editor.on( 'blur', function() {
2667 var widget;
2668
2669 if ( ( widget = widgetsRepo.focused ) )
2670 blurWidget( widgetsRepo, widget );
2671
2672 if ( ( widget = widgetsRepo.widgetHoldingFocusedEditable ) )
2673 setFocusedEditable( widgetsRepo, widget, null );
2674 } );
2675 }
2676
2677 // Set up actions like:
2678 // * processing in toHtml/toDataFormat,
2679 // * pasting handling,
2680 // * insertion handling,
2681 // * editable reload handling (setData, mode switch, undo/redo),
2682 // * DOM invalidation handling,
2683 // * widgets checks.
2684 function setupWidgetsLifecycle( widgetsRepo ) {
2685 setupWidgetsLifecycleStart( widgetsRepo );
2686 setupWidgetsLifecycleEnd( widgetsRepo );
2687
2688 widgetsRepo.on( 'checkWidgets', checkWidgets );
2689 widgetsRepo.editor.on( 'contentDomInvalidated', widgetsRepo.checkWidgets, widgetsRepo );
2690 }
2691
2692 function setupWidgetsLifecycleEnd( widgetsRepo ) {
2693 var editor = widgetsRepo.editor,
2694 downcastingSessions = {};
2695
2696 // Listen before htmlDP#htmlFilter is applied to cache all widgets, because we'll
2697 // loose data-cke-* attributes.
2698 editor.on( 'toDataFormat', function( evt ) {
2699 // To avoid conflicts between htmlDP#toDF calls done at the same time
2700 // (e.g. nestedEditable#getData called during downcasting some widget)
2701 // mark every toDataFormat event chain with the downcasting session id.
2702 var id = CKEDITOR.tools.getNextNumber(),
2703 toBeDowncasted = [];
2704 evt.data.downcastingSessionId = id;
2705 downcastingSessions[ id ] = toBeDowncasted;
2706
2707 evt.data.dataValue.forEach( function( element ) {
2708 var attrs = element.attributes,
2709 widget, widgetElement;
2710
2711 // Wrapper.
2712 // Perform first part of downcasting (cleanup) and cache widgets,
2713 // because after applying DP's filter all data-cke-* attributes will be gone.
2714 if ( 'data-cke-widget-id' in attrs ) {
2715 widget = widgetsRepo.instances[ attrs[ 'data-cke-widget-id' ] ];
2716 if ( widget ) {
2717 widgetElement = element.getFirst( Widget.isParserWidgetElement );
2718 toBeDowncasted.push( {
2719 wrapper: element,
2720 element: widgetElement,
2721 widget: widget,
2722 editables: {}
2723 } );
2724
2725 // If widget did not have data-cke-widget attribute before upcasting remove it.
2726 if ( widgetElement.attributes[ 'data-cke-widget-keep-attr' ] != '1' )
2727 delete widgetElement.attributes[ 'data-widget' ];
2728 }
2729 }
2730 // Nested editable.
2731 else if ( 'data-cke-widget-editable' in attrs ) {
2732 // Save the reference to this nested editable in the closest widget to be downcasted.
2733 // Nested editables are downcasted in the successive toDataFormat to create an opportunity
2734 // for dataFilter's "excludeNestedEditable" option to do its job (that option relies on
2735 // contenteditable="true" attribute) (http://dev.ckeditor.com/ticket/11372).
2736 toBeDowncasted[ toBeDowncasted.length - 1 ].editables[ attrs[ 'data-cke-widget-editable' ] ] = element;
2737
2738 // Don't check children - there won't be next wrapper or nested editable which we
2739 // should process in this session.
2740 return false;
2741 }
2742 }, CKEDITOR.NODE_ELEMENT, true );
2743 }, null, null, 8 );
2744
2745 // Listen after dataProcessor.htmlFilter and ACF were applied
2746 // so wrappers securing widgets' contents are removed after all filtering was done.
2747 editor.on( 'toDataFormat', function( evt ) {
2748 // Ignore some unmarked sessions.
2749 if ( !evt.data.downcastingSessionId )
2750 return;
2751
2752 var toBeDowncasted = downcastingSessions[ evt.data.downcastingSessionId ],
2753 toBe, widget, widgetElement, retElement, editableElement, e;
2754
2755 while ( ( toBe = toBeDowncasted.shift() ) ) {
2756 widget = toBe.widget;
2757 widgetElement = toBe.element;
2758 retElement = widget._.downcastFn && widget._.downcastFn.call( widget, widgetElement );
2759
2760 // Replace nested editables' content with their output data.
2761 for ( e in toBe.editables ) {
2762 editableElement = toBe.editables[ e ];
2763
2764 delete editableElement.attributes.contenteditable;
2765 editableElement.setHtml( widget.editables[ e ].getData() );
2766 }
2767
2768 // Returned element always defaults to widgetElement.
2769 if ( !retElement )
2770 retElement = widgetElement;
2771
2772 toBe.wrapper.replaceWith( retElement );
2773 }
2774 }, null, null, 13 );
2775
2776
2777 editor.on( 'contentDomUnload', function() {
2778 widgetsRepo.destroyAll( true );
2779 } );
2780 }
2781
2782 function setupWidgetsLifecycleStart( widgetsRepo ) {
2783 var editor = widgetsRepo.editor,
2784 processedWidgetOnly,
2785 snapshotLoaded;
2786
2787 // Listen after ACF (so data are filtered),
2788 // but before dataProcessor.dataFilter was applied (so we can secure widgets' internals).
2789 editor.on( 'toHtml', function( evt ) {
2790 var upcastIterator = createUpcastIterator( widgetsRepo ),
2791 toBeWrapped;
2792
2793 evt.data.dataValue.forEach( upcastIterator.iterator, CKEDITOR.NODE_ELEMENT, true );
2794
2795 // Clean up and wrap all queued elements.
2796 while ( ( toBeWrapped = upcastIterator.toBeWrapped.pop() ) ) {
2797 cleanUpWidgetElement( toBeWrapped[ 0 ] );
2798 widgetsRepo.wrapElement( toBeWrapped[ 0 ], toBeWrapped[ 1 ] );
2799 }
2800
2801 // Used to determine whether only widget was pasted.
2802 if ( evt.data.protectedWhitespaces ) {
2803 // Whitespaces are protected by wrapping content with spans. Take the middle node only.
2804 processedWidgetOnly = evt.data.dataValue.children.length == 3 &&
2805 Widget.isParserWidgetWrapper( evt.data.dataValue.children[ 1 ] );
2806 } else {
2807 processedWidgetOnly = evt.data.dataValue.children.length == 1 &&
2808 Widget.isParserWidgetWrapper( evt.data.dataValue.children[ 0 ] );
2809 }
2810 }, null, null, 8 );
2811
2812 editor.on( 'dataReady', function() {
2813 // Clean up all widgets loaded from snapshot.
2814 if ( snapshotLoaded )
2815 cleanUpAllWidgetElements( widgetsRepo, editor.editable() );
2816 snapshotLoaded = 0;
2817
2818 // Some widgets were destroyed on contentDomUnload,
2819 // some on loadSnapshot, but that does not include
2820 // e.g. setHtml on inline editor or widgets removed just
2821 // before setting data.
2822 widgetsRepo.destroyAll( true );
2823 widgetsRepo.initOnAll();
2824 } );
2825
2826 // Set flag so dataReady will know that additional
2827 // cleanup is needed, because snapshot containing widgets was loaded.
2828 editor.on( 'loadSnapshot', function( evt ) {
2829 // Primitive but sufficient check which will prevent from executing
2830 // heavier cleanUpAllWidgetElements if not needed.
2831 if ( ( /data-cke-widget/ ).test( evt.data ) )
2832 snapshotLoaded = 1;
2833
2834 widgetsRepo.destroyAll( true );
2835 }, null, null, 9 );
2836
2837 // Handle pasted single widget.
2838 editor.on( 'paste', function( evt ) {
2839 var data = evt.data;
2840
2841 data.dataValue = data.dataValue.replace( pasteReplaceRegex, pasteReplaceFn );
2842
2843 // If drag'n'drop kind of paste into nested editable (data.range), selection is set AFTER
2844 // data is pasted, which means editor has no chance to change activeFilter's context.
2845 // As a result, pasted data is filtered with default editor's filter instead of NE's and
2846 // funny things get inserted. Changing the filter by analysis of the paste range below (http://dev.ckeditor.com/ticket/13186).
2847 if ( data.range ) {
2848 // Check if pasting into nested editable.
2849 var nestedEditable = Widget.getNestedEditable( editor.editable(), data.range.startContainer );
2850
2851 if ( nestedEditable ) {
2852 // Retrieve the filter from NE's data and set it active before editor.insertHtml is done
2853 // in clipboard plugin.
2854 var filter = CKEDITOR.filter.instances[ nestedEditable.data( 'cke-filter' ) ];
2855
2856 if ( filter ) {
2857 editor.setActiveFilter( filter );
2858 }
2859 }
2860 }
2861 } );
2862
2863 // Listen with high priority to check widgets after data was inserted.
2864 editor.on( 'afterInsertHtml', function( evt ) {
2865 if ( evt.data.intoRange ) {
2866 widgetsRepo.checkWidgets( { initOnlyNew: true } );
2867 } else {
2868 editor.fire( 'lockSnapshot' );
2869 // Init only new for performance reason.
2870 // Focus inited if only widget was processed.
2871 widgetsRepo.checkWidgets( { initOnlyNew: true, focusInited: processedWidgetOnly } );
2872
2873 editor.fire( 'unlockSnapshot' );
2874 }
2875 } );
2876 }
2877
2878 // Helper for coordinating which widgets should be
2879 // selected/deselected and which one should be focused/blurred.
2880 function stateUpdater( widgetsRepo ) {
2881 var currentlySelected = widgetsRepo.selected,
2882 toBeSelected = [],
2883 toBeDeselected = currentlySelected.slice( 0 ),
2884 focused = null;
2885
2886 return {
2887 select: function( widget ) {
2888 if ( CKEDITOR.tools.indexOf( currentlySelected, widget ) < 0 )
2889 toBeSelected.push( widget );
2890
2891 var index = CKEDITOR.tools.indexOf( toBeDeselected, widget );
2892 if ( index >= 0 )
2893 toBeDeselected.splice( index, 1 );
2894
2895 return this;
2896 },
2897
2898 focus: function( widget ) {
2899 focused = widget;
2900 return this;
2901 },
2902
2903 commit: function() {
2904 var focusedChanged = widgetsRepo.focused !== focused,
2905 widget, isDirty;
2906
2907 widgetsRepo.editor.fire( 'lockSnapshot' );
2908
2909 if ( focusedChanged && ( widget = widgetsRepo.focused ) )
2910 blurWidget( widgetsRepo, widget );
2911
2912 while ( ( widget = toBeDeselected.pop() ) ) {
2913 currentlySelected.splice( CKEDITOR.tools.indexOf( currentlySelected, widget ), 1 );
2914 // Widget could be destroyed in the meantime - e.g. data could be set.
2915 if ( widget.isInited() ) {
2916 isDirty = widget.editor.checkDirty();
2917
2918 widget.setSelected( false );
2919
2920 !isDirty && widget.editor.resetDirty();
2921 }
2922 }
2923
2924 if ( focusedChanged && focused ) {
2925 isDirty = widgetsRepo.editor.checkDirty();
2926
2927 widgetsRepo.focused = focused;
2928 widgetsRepo.fire( 'widgetFocused', { widget: focused } );
2929 focused.setFocused( true );
2930
2931 !isDirty && widgetsRepo.editor.resetDirty();
2932 }
2933
2934 while ( ( widget = toBeSelected.pop() ) ) {
2935 currentlySelected.push( widget );
2936 widget.setSelected( true );
2937 }
2938
2939 widgetsRepo.editor.fire( 'unlockSnapshot' );
2940 }
2941 };
2942 }
2943
2944
2945 //
2946 // WIDGET helpers ---------------------------------------------------------
2947 //
2948
2949 // LEFT, RIGHT, UP, DOWN, DEL, BACKSPACE - unblock default fake sel handlers.
2950 var keystrokesNotBlockedByWidget = { 37: 1, 38: 1, 39: 1, 40: 1, 8: 1, 46: 1 };
2951
2952 // Applies or removes style's classes from widget.
2953 // @param {CKEDITOR.style} style Custom widget style.
2954 // @param {Boolean} apply Whether to apply or remove style.
2955 function applyRemoveStyle( widget, style, apply ) {
2956 var changed = 0,
2957 classes = getStyleClasses( style ),
2958 updatedClasses = widget.data.classes || {},
2959 cl;
2960
2961 // Ee... Something is wrong with this style.
2962 if ( !classes )
2963 return;
2964
2965 // Clone, because we need to break reference.
2966 updatedClasses = CKEDITOR.tools.clone( updatedClasses );
2967
2968 while ( ( cl = classes.pop() ) ) {
2969 if ( apply ) {
2970 if ( !updatedClasses[ cl ] )
2971 changed = updatedClasses[ cl ] = 1;
2972 } else {
2973 if ( updatedClasses[ cl ] ) {
2974 delete updatedClasses[ cl ];
2975 changed = 1;
2976 }
2977 }
2978 }
2979 if ( changed )
2980 widget.setData( 'classes', updatedClasses );
2981 }
2982
2983 function cancel( evt ) {
2984 evt.cancel();
2985 }
2986
2987 function copySingleWidget( widget, isCut ) {
2988 var editor = widget.editor,
2989 doc = editor.document;
2990
2991 // We're still handling previous copy/cut.
2992 // When keystroke is used to copy/cut this will also prevent
2993 // conflict with copySingleWidget called again for native copy/cut event.
2994 if ( doc.getById( 'cke_copybin' ) )
2995 return;
2996
2997 // [IE] Use span for copybin and its container to avoid bug with expanding editable height by
2998 // absolutely positioned element.
2999 var copybinName = ( editor.blockless || CKEDITOR.env.ie ) ? 'span' : 'div',
3000 copybin = doc.createElement( copybinName ),
3001 copybinContainer = doc.createElement( copybinName ),
3002 // IE8 always jumps to the end of document.
3003 needsScrollHack = CKEDITOR.env.ie && CKEDITOR.env.version < 9;
3004
3005 copybinContainer.setAttributes( {
3006 id: 'cke_copybin',
3007 'data-cke-temp': '1'
3008 } );
3009
3010 // Position copybin element outside current viewport.
3011 copybin.setStyles( {
3012 position: 'absolute',
3013 width: '1px',
3014 height: '1px',
3015 overflow: 'hidden'
3016 } );
3017
3018 copybin.setStyle( editor.config.contentsLangDirection == 'ltr' ? 'left' : 'right', '-5000px' );
3019
3020 var range = editor.createRange();
3021 range.setStartBefore( widget.wrapper );
3022 range.setEndAfter( widget.wrapper );
3023
3024 copybin.setHtml(
3025 '<span data-cke-copybin-start="1">\u200b</span>' +
3026 editor.editable().getHtmlFromRange( range ).getHtml() +
3027 '<span data-cke-copybin-end="1">\u200b</span>' );
3028
3029 // Save snapshot with the current state.
3030 editor.fire( 'saveSnapshot' );
3031
3032 // Ignore copybin.
3033 editor.fire( 'lockSnapshot' );
3034
3035 copybinContainer.append( copybin );
3036 editor.editable().append( copybinContainer );
3037
3038 var listener1 = editor.on( 'selectionChange', cancel, null, null, 0 ),
3039 listener2 = widget.repository.on( 'checkSelection', cancel, null, null, 0 );
3040
3041 if ( needsScrollHack ) {
3042 var docElement = doc.getDocumentElement().$,
3043 scrollTop = docElement.scrollTop;
3044 }
3045
3046 // Once the clone of the widget is inside of copybin, select
3047 // the entire contents. This selection will be copied by the
3048 // native browser's clipboard system.
3049 range = editor.createRange();
3050 range.selectNodeContents( copybin );
3051 range.select();
3052
3053 if ( needsScrollHack )
3054 docElement.scrollTop = scrollTop;
3055
3056 setTimeout( function() {
3057 // [IE] Focus widget before removing copybin to avoid scroll jump.
3058 if ( !isCut )
3059 widget.focus();
3060
3061 copybinContainer.remove();
3062
3063 listener1.removeListener();
3064 listener2.removeListener();
3065
3066 editor.fire( 'unlockSnapshot' );
3067
3068 if ( isCut ) {
3069 widget.repository.del( widget );
3070 editor.fire( 'saveSnapshot' );
3071 }
3072 }, 100 ); // Use 100ms, so Chrome (@Mac) will be able to grab the content.
3073 }
3074
3075 // Extracts classes array from style instance.
3076 function getStyleClasses( style ) {
3077 var attrs = style.getDefinition().attributes,
3078 classes = attrs && attrs[ 'class' ];
3079
3080 return classes ? classes.split( /\s+/ ) : null;
3081 }
3082
3083 // [IE] Force keeping focus because IE sometimes forgets to fire focus on main editable
3084 // when blurring nested editable.
3085 // @context widget
3086 function onEditableBlur() {
3087 var active = CKEDITOR.document.getActive(),
3088 editor = this.editor,
3089 editable = editor.editable();
3090
3091 // If focus stays within editor override blur and set currentActive because it should be
3092 // automatically changed to editable on editable#focus but it is not fired.
3093 if ( ( editable.isInline() ? editable : editor.document.getWindow().getFrame() ).equals( active ) )
3094 editor.focusManager.focus( editable );
3095 }
3096
3097 // Force selectionChange when editable was focused.
3098 // Similar to hack in selection.js#~620.
3099 // @context widget
3100 function onEditableFocus() {
3101 // Gecko does not support 'DOMFocusIn' event on which we unlock selection
3102 // in selection.js to prevent selection locking when entering nested editables.
3103 if ( CKEDITOR.env.gecko )
3104 this.editor.unlockSelection();
3105
3106 // We don't need to force selectionCheck on Webkit, because on Webkit
3107 // we do that on DOMFocusIn in selection.js.
3108 if ( !CKEDITOR.env.webkit ) {
3109 this.editor.forceNextSelectionCheck();
3110 this.editor.selectionChange( 1 );
3111 }
3112 }
3113
3114 // Setup listener on widget#data which will update (remove/add) classes
3115 // by comparing newly set classes with the old ones.
3116 function setupDataClassesListener( widget ) {
3117 // Note: previousClasses and newClasses may be null!
3118 // Tip: for ( cl in null ) is correct.
3119 var previousClasses = null;
3120
3121 widget.on( 'data', function() {
3122 var newClasses = this.data.classes,
3123 cl;
3124
3125 // When setting new classes one need to remember
3126 // that he must break reference.
3127 if ( previousClasses == newClasses )
3128 return;
3129
3130 for ( cl in previousClasses ) {
3131 // Avoid removing and adding classes again.
3132 if ( !( newClasses && newClasses[ cl ] ) )
3133 this.removeClass( cl );
3134 }
3135 for ( cl in newClasses )
3136 this.addClass( cl );
3137
3138 previousClasses = newClasses;
3139 } );
3140 }
3141
3142 // Add a listener to data event that will set/change widget's label (http://dev.ckeditor.com/ticket/14539).
3143 function setupA11yListener( widget ) {
3144 // Note, the function gets executed in a context of widget instance.
3145 function getLabelDefault() {
3146 return this.editor.lang.widget.label.replace( /%1/, this.pathName || this.element.getName() );
3147 }
3148
3149 // Setting a listener on data is enough, there's no need to perform it on widget initialization, as
3150 // setupWidgetData fires this event anyway.
3151 widget.on( 'data', function() {
3152 // In some cases widget might get destroyed in an earlier data listener. For instance, image2 plugin, does
3153 // so when changing its internal state.
3154 if ( !widget.wrapper ) {
3155 return;
3156 }
3157
3158 var label = this.getLabel ? this.getLabel() : getLabelDefault.call( this );
3159
3160 widget.wrapper.setAttribute( 'role', 'region' );
3161 widget.wrapper.setAttribute( 'aria-label', label );
3162 }, null, null, 9999 );
3163 }
3164
3165 function setupDragHandler( widget ) {
3166 if ( !widget.draggable )
3167 return;
3168
3169 var editor = widget.editor,
3170 // Use getLast to find wrapper's direct descendant (http://dev.ckeditor.com/ticket/12022).
3171 container = widget.wrapper.getLast( Widget.isDomDragHandlerContainer ),
3172 img;
3173
3174 // Reuse drag handler if already exists (http://dev.ckeditor.com/ticket/11281).
3175 if ( container )
3176 img = container.findOne( 'img' );
3177 else {
3178 container = new CKEDITOR.dom.element( 'span', editor.document );
3179 container.setAttributes( {
3180 'class': 'cke_reset cke_widget_drag_handler_container',
3181 // Split background and background-image for IE8 which will break on rgba().
3182 style: 'background:rgba(220,220,220,0.5);background-image:url(' + editor.plugins.widget.path + 'images/handle.png)'
3183 } );
3184
3185 img = new CKEDITOR.dom.element( 'img', editor.document );
3186 img.setAttributes( {
3187 'class': 'cke_reset cke_widget_drag_handler',
3188 'data-cke-widget-drag-handler': '1',
3189 src: CKEDITOR.tools.transparentImageData,
3190 width: DRAG_HANDLER_SIZE,
3191 title: editor.lang.widget.move,
3192 height: DRAG_HANDLER_SIZE,
3193 role: 'presentation'
3194 } );
3195 widget.inline && img.setAttribute( 'draggable', 'true' );
3196
3197 container.append( img );
3198 widget.wrapper.append( container );
3199 }
3200
3201 // Preventing page reload when dropped content on widget wrapper (http://dev.ckeditor.com/ticket/13015).
3202 // Widget is not editable so by default drop on it isn't allowed what means that
3203 // browser handles it (there's no editable#drop event). If there's no drop event we cannot block
3204 // the drop, so page is reloaded. This listener enables drop on widget wrappers.
3205 widget.wrapper.on( 'dragover', function( evt ) {
3206 evt.data.preventDefault();
3207 } );
3208
3209 widget.wrapper.on( 'mouseenter', widget.updateDragHandlerPosition, widget );
3210 setTimeout( function() {
3211 widget.on( 'data', widget.updateDragHandlerPosition, widget );
3212 }, 50 );
3213
3214 if ( !widget.inline ) {
3215 img.on( 'mousedown', onBlockWidgetDrag, widget );
3216
3217 // On IE8 'dragstart' is propagated to editable, so editor#dragstart is fired twice on block widgets.
3218 if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) {
3219 img.on( 'dragstart', function( evt ) {
3220 evt.data.preventDefault( true );
3221 } );
3222 }
3223 }
3224
3225 widget.dragHandlerContainer = container;
3226 }
3227
3228 function onBlockWidgetDrag( evt ) {
3229 var finder = this.repository.finder,
3230 locator = this.repository.locator,
3231 liner = this.repository.liner,
3232 editor = this.editor,
3233 editable = editor.editable(),
3234 listeners = [],
3235 sorted = [],
3236 locations,
3237 y;
3238
3239 // Mark dragged widget for repository#finder.
3240 this.repository._.draggedWidget = this;
3241
3242 // Harvest all possible relations and display some closest.
3243 var relations = finder.greedySearch(),
3244
3245 buffer = CKEDITOR.tools.eventsBuffer( 50, function() {
3246 locations = locator.locate( relations );
3247
3248 // There's only a single line displayed for D&D.
3249 sorted = locator.sort( y, 1 );
3250
3251 if ( sorted.length ) {
3252 liner.prepare( relations, locations );
3253 liner.placeLine( sorted[ 0 ] );
3254 liner.cleanup();
3255 }
3256 } );
3257
3258 // Let's have the "dragging cursor" over entire editable.
3259 editable.addClass( 'cke_widget_dragging' );
3260
3261 // Cache mouse position so it is re-used in events buffer.
3262 listeners.push( editable.on( 'mousemove', function( evt ) {
3263 y = evt.data.$.clientY;
3264 buffer.input();
3265 } ) );
3266
3267 // Fire drag start as it happens during the native D&D.
3268 editor.fire( 'dragstart', { target: evt.sender } );
3269
3270 function onMouseUp() {
3271 var l;
3272
3273 buffer.reset();
3274
3275 // Stop observing events.
3276 while ( ( l = listeners.pop() ) )
3277 l.removeListener();
3278
3279 onBlockWidgetDrop.call( this, sorted, evt.sender );
3280 }
3281
3282 // Mouseup means "drop". This is when the widget is being detached
3283 // from DOM and placed at range determined by the line (location).
3284 listeners.push( editor.document.once( 'mouseup', onMouseUp, this ) );
3285
3286 // Prevent calling 'onBlockWidgetDrop' twice in the inline editor.
3287 // `removeListener` does not work if it is called at the same time event is fired.
3288 if ( !editable.isInline() ) {
3289 // Mouseup may occur when user hovers the line, which belongs to
3290 // the outer document. This is, of course, a valid listener too.
3291 listeners.push( CKEDITOR.document.once( 'mouseup', onMouseUp, this ) );
3292 }
3293 }
3294
3295 function onBlockWidgetDrop( sorted, dragTarget ) {
3296 var finder = this.repository.finder,
3297 liner = this.repository.liner,
3298 editor = this.editor,
3299 editable = this.editor.editable();
3300
3301 if ( !CKEDITOR.tools.isEmpty( liner.visible ) ) {
3302 // Retrieve range for the closest location.
3303 var dropRange = finder.getRange( sorted[ 0 ] );
3304
3305 // Focus widget (it could lost focus after mousedown+mouseup)
3306 // and save this state as the one where we want to be taken back when undoing.
3307 this.focus();
3308
3309 // Drag range will be set in the drop listener.
3310 editor.fire( 'drop', {
3311 dropRange: dropRange,
3312 target: dropRange.startContainer
3313 } );
3314 }
3315
3316 // Clean-up custom cursor for editable.
3317 editable.removeClass( 'cke_widget_dragging' );
3318
3319 // Clean-up all remaining lines.
3320 liner.hideVisible();
3321
3322 // Clean-up drag & drop.
3323 editor.fire( 'dragend', { target: dragTarget } );
3324 }
3325
3326 function setupEditables( widget ) {
3327 var editableName,
3328 editableDef,
3329 definedEditables = widget.editables;
3330
3331 widget.editables = {};
3332
3333 if ( !widget.editables )
3334 return;
3335
3336 for ( editableName in definedEditables ) {
3337 editableDef = definedEditables[ editableName ];
3338 widget.initEditable( editableName, typeof editableDef == 'string' ? { selector: editableDef } : editableDef );
3339 }
3340 }
3341
3342 function setupMask( widget ) {
3343 if ( !widget.mask )
3344 return;
3345
3346 // Reuse mask if already exists (http://dev.ckeditor.com/ticket/11281).
3347 var img = widget.wrapper.findOne( '.cke_widget_mask' );
3348
3349 if ( !img ) {
3350 img = new CKEDITOR.dom.element( 'img', widget.editor.document );
3351 img.setAttributes( {
3352 src: CKEDITOR.tools.transparentImageData,
3353 'class': 'cke_reset cke_widget_mask'
3354 } );
3355 widget.wrapper.append( img );
3356 }
3357
3358 widget.mask = img;
3359 }
3360
3361 // Replace parts object containing:
3362 // partName => selector pairs
3363 // with:
3364 // partName => element pairs
3365 function setupParts( widget ) {
3366 if ( widget.parts ) {
3367 var parts = {},
3368 el, partName;
3369
3370 for ( partName in widget.parts ) {
3371 el = widget.wrapper.findOne( widget.parts[ partName ] );
3372 parts[ partName ] = el;
3373 }
3374 widget.parts = parts;
3375 }
3376 }
3377
3378 function setupWidget( widget, widgetDef ) {
3379 setupWrapper( widget );
3380 setupParts( widget );
3381 setupEditables( widget );
3382 setupMask( widget );
3383 setupDragHandler( widget );
3384 setupDataClassesListener( widget );
3385 setupA11yListener( widget );
3386
3387 // http://dev.ckeditor.com/ticket/11145: [IE8] Non-editable content of widget is draggable.
3388 if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) {
3389 widget.wrapper.on( 'dragstart', function( evt ) {
3390 var target = evt.data.getTarget();
3391
3392 // Allow text dragging inside nested editables or dragging inline widget's drag handler.
3393 if ( !Widget.getNestedEditable( widget, target ) && !( widget.inline && Widget.isDomDragHandler( target ) ) )
3394 evt.data.preventDefault();
3395 } );
3396 }
3397
3398 widget.wrapper.removeClass( 'cke_widget_new' );
3399 widget.element.addClass( 'cke_widget_element' );
3400
3401 widget.on( 'key', function( evt ) {
3402 var keyCode = evt.data.keyCode;
3403
3404 // ENTER.
3405 if ( keyCode == 13 ) {
3406 widget.edit();
3407 // CTRL+C or CTRL+X.
3408 } else if ( keyCode == CKEDITOR.CTRL + 67 || keyCode == CKEDITOR.CTRL + 88 ) {
3409 copySingleWidget( widget, keyCode == CKEDITOR.CTRL + 88 );
3410 return; // Do not preventDefault.
3411 } else if ( keyCode in keystrokesNotBlockedByWidget || ( CKEDITOR.CTRL & keyCode ) || ( CKEDITOR.ALT & keyCode ) ) {
3412 // Pass chosen keystrokes to other plugins or default fake sel handlers.
3413 // Pass all CTRL/ALT keystrokes.
3414 return;
3415 }
3416
3417 return false;
3418 }, null, null, 999 );
3419 // Listen with high priority so it's possible
3420 // to overwrite this callback.
3421
3422 widget.on( 'doubleclick', function( evt ) {
3423 if ( widget.edit() ) {
3424 // We have to cancel event if edit method opens a dialog, otherwise
3425 // link plugin may open extra dialog (http://dev.ckeditor.com/ticket/12140).
3426 evt.cancel();
3427 }
3428 } );
3429
3430 if ( widgetDef.data )
3431 widget.on( 'data', widgetDef.data );
3432
3433 if ( widgetDef.edit )
3434 widget.on( 'edit', widgetDef.edit );
3435 }
3436
3437 function setupWidgetData( widget, startupData ) {
3438 var widgetDataAttr = widget.element.data( 'cke-widget-data' );
3439
3440 if ( widgetDataAttr )
3441 widget.setData( JSON.parse( decodeURIComponent( widgetDataAttr ) ) );
3442 if ( startupData )
3443 widget.setData( startupData );
3444
3445 // Populate classes if they are not preset.
3446 if ( !widget.data.classes )
3447 widget.setData( 'classes', widget.getClasses() );
3448
3449 // Unblock data and...
3450 widget.dataReady = true;
3451
3452 // Write data to element because this was blocked when data wasn't ready.
3453 writeDataToElement( widget );
3454
3455 // Fire data event first time, because this was blocked when data wasn't ready.
3456 widget.fire( 'data', widget.data );
3457 }
3458
3459 function setupWrapper( widget ) {
3460 // Retrieve widget wrapper. Assign an id to it.
3461 var wrapper = widget.wrapper = widget.element.getParent();
3462 wrapper.setAttribute( 'data-cke-widget-id', widget.id );
3463 }
3464
3465 function writeDataToElement( widget ) {
3466 widget.element.data( 'cke-widget-data', encodeURIComponent( JSON.stringify( widget.data ) ) );
3467 }
3468
3469 //
3470 // WIDGET STYLE HANDLER ---------------------------------------------------
3471 //
3472
3473 ( function() {
3474 // Styles categorized by group. It is used to prevent applying styles for the same group being used together.
3475 var styleGroups = {};
3476
3477 /**
3478 * The class representing a widget style. It is an {@link CKEDITOR#STYLE_OBJECT object} like
3479 * the styles handler for widgets.
3480 *
3481 * **Note:** This custom style handler does not support all methods of the {@link CKEDITOR.style} class.
3482 * Not supported methods: {@link #applyToRange}, {@link #removeFromRange}, {@link #applyToObject}.
3483 *
3484 * @since 4.4
3485 * @class CKEDITOR.style.customHandlers.widget
3486 * @extends CKEDITOR.style
3487 */
3488 CKEDITOR.style.addCustomHandler( {
3489 type: 'widget',
3490
3491 setup: function( styleDefinition ) {
3492 /**
3493 * The name of widget to which this style can be applied.
3494 * It is extracted from style definition's `widget` property.
3495 *
3496 * @property {String} widget
3497 */
3498 this.widget = styleDefinition.widget;
3499
3500 /**
3501 * An array of groups that this style belongs to.
3502 * Styles assigned to the same group cannot be combined.
3503 *
3504 * @since 4.6.2
3505 * @property {Array} group
3506 */
3507 this.group = typeof styleDefinition.group == 'string' ? [ styleDefinition.group ] : styleDefinition.group;
3508
3509 // Store style categorized by its group.
3510 // It is used to prevent enabling two styles from same group.
3511 if ( this.group ) {
3512 saveStyleGroup( this );
3513 }
3514 },
3515
3516 apply: function( editor ) {
3517 var widget;
3518
3519 // Before CKEditor 4.4 wasn't a required argument, so we need to
3520 // handle a case when it wasn't provided.
3521 if ( !( editor instanceof CKEDITOR.editor ) )
3522 return;
3523
3524 // Theoretically we could bypass checkApplicable, get widget from
3525 // widgets.focused and check its name, what would be faster, but then
3526 // this custom style would work differently than the default style
3527 // which checks if it's applicable before applying or removing itself.
3528 if ( this.checkApplicable( editor.elementPath(), editor ) ) {
3529 widget = editor.widgets.focused;
3530
3531 // Remove other styles from the same group.
3532 if ( this.group ) {
3533 this.removeStylesFromSameGroup( editor );
3534 }
3535
3536 widget.applyStyle( this );
3537 }
3538 },
3539
3540 remove: function( editor ) {
3541 // Before CKEditor 4.4 wasn't a required argument, so we need to
3542 // handle a case when it wasn't provided.
3543 if ( !( editor instanceof CKEDITOR.editor ) )
3544 return;
3545
3546 if ( this.checkApplicable( editor.elementPath(), editor ) )
3547 editor.widgets.focused.removeStyle( this );
3548 },
3549
3550 /**
3551 * Removes all styles that belong to the same group as this style. This method will neither add nor remove
3552 * the current style.
3553 * Returns `true` if any style was removed, otherwise returns `false`.
3554 *
3555 * @since 4.6.2
3556 * @param {CKEDITOR.editor} editor
3557 * @returns {Boolean}
3558 */
3559 removeStylesFromSameGroup: function( editor ) {
3560 var stylesFromSameGroup,
3561 path,
3562 removed = false;
3563
3564 // Before CKEditor 4.4 wasn't a required argument, so we need to
3565 // handle a case when it wasn't provided.
3566 if ( !( editor instanceof CKEDITOR.editor ) )
3567 return false;
3568
3569 path = editor.elementPath();
3570 if ( this.checkApplicable( path, editor ) ) {
3571 // Iterate over each group.
3572 for ( var i = 0, l = this.group.length; i < l; i++ ) {
3573 stylesFromSameGroup = styleGroups[ this.widget ][ this.group[ i ] ];
3574 // Iterate over each style from group.
3575 for ( var j = 0; j < stylesFromSameGroup.length; j++ ) {
3576 if ( stylesFromSameGroup[ j ] !== this && stylesFromSameGroup[ j ].checkActive( path, editor ) ) {
3577 editor.widgets.focused.removeStyle( stylesFromSameGroup[ j ] );
3578 removed = true;
3579 }
3580 }
3581 }
3582 }
3583
3584 return removed;
3585 },
3586
3587 checkActive: function( elementPath, editor ) {
3588 return this.checkElementMatch( elementPath.lastElement, 0, editor );
3589 },
3590
3591 checkApplicable: function( elementPath, editor ) {
3592 // Before CKEditor 4.4 wasn't a required argument, so we need to
3593 // handle a case when it wasn't provided.
3594 if ( !( editor instanceof CKEDITOR.editor ) )
3595 return false;
3596
3597 return this.checkElement( elementPath.lastElement );
3598 },
3599
3600 checkElementMatch: checkElementMatch,
3601
3602 checkElementRemovable: checkElementMatch,
3603
3604 /**
3605 * Checks if an element is a {@link CKEDITOR.plugins.widget#wrapper wrapper} of a
3606 * widget whose name matches the {@link #widget widget name} specified in the style definition.
3607 *
3608 * @param {CKEDITOR.dom.element} element
3609 * @returns {Boolean}
3610 */
3611 checkElement: function( element ) {
3612 if ( !Widget.isDomWidgetWrapper( element ) )
3613 return false;
3614
3615 var widgetElement = element.getFirst( Widget.isDomWidgetElement );
3616 return widgetElement && widgetElement.data( 'widget' ) == this.widget;
3617 },
3618
3619 buildPreview: function( label ) {
3620 return label || this._.definition.name;
3621 },
3622
3623 /**
3624 * Returns allowed content rules which should be registered for this style.
3625 * Uses widget's {@link CKEDITOR.plugins.widget.definition#styleableElements} to make a rule
3626 * allowing classes on specified elements or use widget's
3627 * {@link CKEDITOR.plugins.widget.definition#styleToAllowedContentRules} method to transform a style
3628 * into allowed content rules.
3629 *
3630 * @param {CKEDITOR.editor} The editor instance.
3631 * @returns {CKEDITOR.filter.allowedContentRules}
3632 */
3633 toAllowedContentRules: function( editor ) {
3634 if ( !editor )
3635 return null;
3636
3637 var widgetDef = editor.widgets.registered[ this.widget ],
3638 classes,
3639 rule = {};
3640
3641 if ( !widgetDef )
3642 return null;
3643
3644 if ( widgetDef.styleableElements ) {
3645 classes = this.getClassesArray();
3646 if ( !classes )
3647 return null;
3648
3649 rule[ widgetDef.styleableElements ] = {
3650 classes: classes,
3651 propertiesOnly: true
3652 };
3653 return rule;
3654 }
3655 if ( widgetDef.styleToAllowedContentRules )
3656 return widgetDef.styleToAllowedContentRules( this );
3657 return null;
3658 },
3659
3660 /**
3661 * Returns classes defined in the style in form of an array.
3662 *
3663 * @returns {String[]}
3664 */
3665 getClassesArray: function() {
3666 var classes = this._.definition.attributes && this._.definition.attributes[ 'class' ];
3667
3668 return classes ? CKEDITOR.tools.trim( classes ).split( /\s+/ ) : null;
3669 },
3670
3671 /**
3672 * Not implemented.
3673 *
3674 * @method applyToRange
3675 */
3676 applyToRange: notImplemented,
3677
3678 /**
3679 * Not implemented.
3680 *
3681 * @method removeFromRange
3682 */
3683 removeFromRange: notImplemented,
3684
3685 /**
3686 * Not implemented.
3687 *
3688 * @method applyToObject
3689 */
3690 applyToObject: notImplemented
3691 } );
3692
3693 function notImplemented() {}
3694
3695 // @context style
3696 function checkElementMatch( element, fullMatch, editor ) {
3697 // Before CKEditor 4.4 wasn't a required argument, so we need to
3698 // handle a case when it wasn't provided.
3699 if ( !editor )
3700 return false;
3701
3702 if ( !this.checkElement( element ) )
3703 return false;
3704
3705 var widget = editor.widgets.getByElement( element, true );
3706 return widget && widget.checkStyleActive( this );
3707 }
3708
3709 // Save and categorize style by its group.
3710 function saveStyleGroup( style ) {
3711 var widgetName = style.widget,
3712 group;
3713
3714 if ( !styleGroups[ widgetName ] ) {
3715 styleGroups[ widgetName ] = {};
3716 }
3717
3718 for ( var i = 0, l = style.group.length; i < l; i++ ) {
3719 group = style.group[ i ];
3720 if ( !styleGroups[ widgetName ][ group ] ) {
3721 styleGroups[ widgetName ][ group ] = [];
3722 }
3723
3724 styleGroups[ widgetName ][ group ].push( style );
3725 }
3726 }
3727
3728 } )();
3729
3730 //
3731 // EXPOSE PUBLIC API ------------------------------------------------------
3732 //
3733
3734 CKEDITOR.plugins.widget = Widget;
3735 Widget.repository = Repository;
3736 Widget.nestedEditable = NestedEditable;
3737 } )();
3738
3739 /**
3740 * An event fired when a widget definition is registered by the {@link CKEDITOR.plugins.widget.repository#add} method.
3741 * It is possible to modify the definition being registered.
3742 *
3743 * @event widgetDefinition
3744 * @member CKEDITOR.editor
3745 * @param {CKEDITOR.plugins.widget.definition} data Widget definition.
3746 */
3747
3748 /**
3749 * This is an abstract class that describes the definition of a widget.
3750 * It is a type of {@link CKEDITOR.plugins.widget.repository#add} method's second argument.
3751 *
3752 * Widget instances inherit from registered widget definitions, although not in a prototypal way.
3753 * They are simply extended with corresponding widget definitions. Note that not all properties of
3754 * the widget definition become properties of a widget. Some, like {@link #data} or {@link #edit}, become
3755 * widget's events listeners.
3756 *
3757 * @class CKEDITOR.plugins.widget.definition
3758 * @abstract
3759 * @mixins CKEDITOR.feature
3760 */
3761
3762 /**
3763 * Widget definition name. It is automatically set when the definition is
3764 * {@link CKEDITOR.plugins.widget.repository#add registered}.
3765 *
3766 * @property {String} name
3767 */
3768
3769 /**
3770 * The method executed while initializing a widget, after a widget instance
3771 * is created, but before it is ready. It is executed before the first
3772 * {@link CKEDITOR.plugins.widget#event-data} is fired so it is common to
3773 * use the `init` method to populate widget data with information loaded from
3774 * the DOM, like for exmaple:
3775 *
3776 * init: function() {
3777 * this.setData( 'width', this.element.getStyle( 'width' ) );
3778 *
3779 * if ( this.parts.caption.getStyle( 'display' ) != 'none' )
3780 * this.setData( 'showCaption', true );
3781 * }
3782 *
3783 * @property {Function} init
3784 */
3785
3786 /**
3787 * The function to be used to upcast an element to this widget or a
3788 * comma-separated list of upcast methods from the {@link #upcasts} object.
3789 *
3790 * The upcast function **is not** executed in the widget context (because the widget
3791 * does not exist yet) and two arguments are passed:
3792 *
3793 * * `element` ({@link CKEDITOR.htmlParser.element}) &ndash; The element to be checked.
3794 * * `data` (`Object`) &ndash; The object which can be extended with data which will then be passed to the widget.
3795 *
3796 * An element will be upcasted if a function returned `true` or an instance of
3797 * a {@link CKEDITOR.htmlParser.element} if upcasting meant DOM structure changes
3798 * (in this case the widget will be initialized on the returned element).
3799 *
3800 * @property {String/Function} upcast
3801 */
3802
3803 /**
3804 * The object containing functions which can be used to upcast this widget.
3805 * Only those pointed by the {@link #upcast} property will be used.
3806 *
3807 * In most cases it is appropriate to use {@link #upcast} directly,
3808 * because majority of widgets need just one method.
3809 * However, in some cases the widget author may want to expose more than one variant
3810 * and then this property may be used.
3811 *
3812 * upcasts: {
3813 * // This function may upcast only figure elements.
3814 * figure: function() {
3815 * // ...
3816 * },
3817 * // This function may upcast only image elements.
3818 * image: function() {
3819 * // ...
3820 * },
3821 * // More variants...
3822 * }
3823 *
3824 * // Then, widget user may choose which upcast methods will be enabled.
3825 * editor.on( 'widgetDefinition', function( evt ) {
3826 * if ( evt.data.name == 'image' )
3827 * evt.data.upcast = 'figure,image'; // Use both methods.
3828 * } );
3829 *
3830 * @property {Object} upcasts
3831 */
3832
3833 /**
3834 * The {@link #upcast} method(s) priority. The upcast with a lower priority number will be called before
3835 * the one with a higher number. The default priority is `10`.
3836 *
3837 * @since 4.5
3838 * @property {Number} [upcastPriority=10]
3839 */
3840
3841 /**
3842 * The function to be used to downcast this widget or
3843 * a name of the downcast option from the {@link #downcasts} object.
3844 *
3845 * The downcast funciton will be executed in the {@link CKEDITOR.plugins.widget} context
3846 * and with `widgetElement` ({@link CKEDITOR.htmlParser.element}) argument which is
3847 * the widget's main element.
3848 *
3849 * The function may return an instance of the {@link CKEDITOR.htmlParser.node} class if the widget
3850 * needs to be downcasted to a different node than the widget's main element.
3851 *
3852 * @property {String/Function} downcast
3853 */
3854
3855 /**
3856 * The object containing functions which can be used to downcast this widget.
3857 * Only the one pointed by the {@link #downcast} property will be used.
3858 *
3859 * In most cases it is appropriate to use {@link #downcast} directly,
3860 * because majority of widgets have just one variant of downcasting (or none at all).
3861 * However, in some cases the widget author may want to expose more than one variant
3862 * and then this property may be used.
3863 *
3864 * downcasts: {
3865 * // This downcast may transform the widget into the figure element.
3866 * figure: function() {
3867 * // ...
3868 * },
3869 * // This downcast may transform the widget into the image element with data-* attributes.
3870 * image: function() {
3871 * // ...
3872 * }
3873 * }
3874 *
3875 * // Then, the widget user may choose one of the downcast options when setting up his editor.
3876 * editor.on( 'widgetDefinition', function( evt ) {
3877 * if ( evt.data.name == 'image' )
3878 * evt.data.downcast = 'figure';
3879 * } );
3880 *
3881 * @property downcasts
3882 */
3883
3884 /**
3885 * If set, it will be added as the {@link CKEDITOR.plugins.widget#event-edit} event listener.
3886 * This means that it will be executed when a widget is being edited.
3887 * See the {@link CKEDITOR.plugins.widget#method-edit} method.
3888 *
3889 * @property {Function} edit
3890 */
3891
3892 /**
3893 * If set, it will be added as the {@link CKEDITOR.plugins.widget#event-data} event listener.
3894 * This means that it will be executed every time the {@link CKEDITOR.plugins.widget#property-data widget data} changes.
3895 *
3896 * @property {Function} data
3897 */
3898
3899 /**
3900 * The method to be executed when the widget's command is executed in order to insert a new widget
3901 * (widget of this type is not focused). If not defined, then the default action will be
3902 * performed which means that:
3903 *
3904 * * An instance of the widget will be created in a detached {@link CKEDITOR.dom.documentFragment document fragment},
3905 * * The {@link CKEDITOR.plugins.widget#method-edit} method will be called to trigger widget editing,
3906 * * The widget element will be inserted into DOM.
3907 *
3908 * @property {Function} insert
3909 */
3910
3911 /**
3912 * The name of a dialog window which will be opened on {@link CKEDITOR.plugins.widget#method-edit}.
3913 * If not defined, then the {@link CKEDITOR.plugins.widget#method-edit} method will not perform any action and
3914 * widget's command will insert a new widget without opening a dialog window first.
3915 *
3916 * @property {String} dialog
3917 */
3918
3919 /**
3920 * The template which will be used to create a new widget element (when the widget's command is executed).
3921 * This string is populated with {@link #defaults default values} by using the {@link CKEDITOR.template} format.
3922 * Therefore it has to be a valid {@link CKEDITOR.template} argument.
3923 *
3924 * @property {String} template
3925 */
3926
3927 /**
3928 * The data object which will be used to populate the data of a newly created widget.
3929 * See {@link CKEDITOR.plugins.widget#property-data}.
3930 *
3931 * defaults: {
3932 * showCaption: true,
3933 * align: 'none'
3934 * }
3935 *
3936 * @property defaults
3937 */
3938
3939 /**
3940 * An object containing definitions of widget components (part name => CSS selector).
3941 *
3942 * parts: {
3943 * image: 'img',
3944 * caption: 'div.caption'
3945 * }
3946 *
3947 * @property parts
3948 */
3949
3950 /**
3951 * An object containing definitions of nested editables (editable name => {@link CKEDITOR.plugins.widget.nestedEditable.definition}).
3952 * Note that editables *have to* be defined in the same order as they are in DOM / {@link CKEDITOR.plugins.widget.definition#template template}.
3953 * Otherwise errors will occur when nesting widgets inside each other.
3954 *
3955 * editables: {
3956 * header: 'h1',
3957 * content: {
3958 * selector: 'div.content',
3959 * allowedContent: 'p strong em; a[!href]'
3960 * }
3961 * }
3962 *
3963 * @property editables
3964 */
3965
3966 /**
3967 * The function used to obtain an accessibility label for the widget. It might be used to make
3968 * the widget labels as precise as possible, since it has access to the widget instance.
3969 *
3970 * If not specified, the default implementation will use the {@link #pathName} or the main
3971 * {@link CKEDITOR.plugins.widget#element element} tag name.
3972 *
3973 * @property {Function} getLabel
3974 */
3975
3976 /**
3977 * The widget name displayed in the elements path.
3978 *
3979 * @property {String} pathName
3980 */
3981
3982 /**
3983 * If set to `true`, the widget's element will be covered with a transparent mask.
3984 * This will prevent its content from being clickable, which matters in case
3985 * of special elements like embedded Flash or iframes that generate a separate "context".
3986 *
3987 * @property {Boolean} mask
3988 */
3989
3990 /**
3991 * If set to `true/false`, it will force the widget to be either an inline or a block widget.
3992 * If not set, the widget type will be determined from the widget element.
3993 *
3994 * Widget type influences whether a block (`div`) or an inline (`span`) element is used
3995 * for the wrapper.
3996 *
3997 * @property {Boolean} inline
3998 */
3999
4000 /**
4001 * The label for the widget toolbar button.
4002 *
4003 * editor.widgets.add( 'simplebox', {
4004 * button: 'Create a simple box'
4005 * } );
4006 *
4007 * editor.widgets.add( 'simplebox', {
4008 * button: editor.lang.simplebox.title
4009 * } );
4010 *
4011 * @property {String} button
4012 */
4013
4014 /**
4015 * Whether widget should be draggable. Defaults to `true`.
4016 * If set to `false` drag handler will not be displayed when hovering widget.
4017 *
4018 * @property {Boolean} draggable
4019 */
4020
4021 /**
4022 * Names of element(s) (separated by spaces) for which the {@link CKEDITOR.filter} should allow classes
4023 * defined in the widget styles. For example if your widget is upcasted from a simple `<div>`
4024 * element, then in order to make it styleable you can set:
4025 *
4026 * editor.widgets.add( 'customWidget', {
4027 * upcast: function( element ) {
4028 * return element.name == 'div';
4029 * },
4030 *
4031 * // ...
4032 *
4033 * styleableElements: 'div'
4034 * } );
4035 *
4036 * Then, when the following style is defined:
4037 *
4038 * {
4039 * name: 'Thick border', type: 'widget', widget: 'customWidget',
4040 * attributes: { 'class': 'thickBorder' }
4041 * }
4042 *
4043 * a rule allowing the `thickBorder` class for `div` elements will be registered in the {@link CKEDITOR.filter}.
4044 *
4045 * If you need to have more freedom when transforming widget style to allowed content rules,
4046 * you can use the {@link #styleToAllowedContentRules} callback.
4047 *
4048 * @since 4.4
4049 * @property {String} styleableElements
4050 */
4051
4052 /**
4053 * Function transforming custom widget's {@link CKEDITOR.style} instance into
4054 * {@link CKEDITOR.filter.allowedContentRules}. It may be used when a static
4055 * {@link #styleableElements} property is not enough to inform the {@link CKEDITOR.filter}
4056 * what HTML features should be enabled when allowing the given style.
4057 *
4058 * In most cases, when style's classes just have to be added to element name(s) used by
4059 * the widget element, it is recommended to use simpler {@link #styleableElements} property.
4060 *
4061 * In order to get parsed classes from the style definition you can use
4062 * {@link CKEDITOR.style.customHandlers.widget#getClassesArray}.
4063 *
4064 * For example, if you want to use the [object format of allowed content rules](#!/guide/dev_allowed_content_rules-section-object-format),
4065 * to specify `match` validator, your implementation could look like this:
4066 *
4067 * editor.widgets.add( 'customWidget', {
4068 * // ...
4069 *
4070 * styleToAllowedContentRules: funciton( style ) {
4071 * // Retrieve classes defined in the style.
4072 * var classes = style.getClassesArray();
4073 *
4074 * // Do something crazy - for example return allowed content rules in object format,
4075 * // with custom match property and propertiesOnly flag.
4076 * return {
4077 * h1: {
4078 * match: isWidgetElement,
4079 * propertiesOnly: true,
4080 * classes: classes
4081 * }
4082 * };
4083 * }
4084 * } );
4085 *
4086 * @since 4.4
4087 * @property {Function} styleToAllowedContentRules
4088 * @param {CKEDITOR.style.customHandlers.widget} style The style to be transformed.
4089 * @returns {CKEDITOR.filter.allowedContentRules}
4090 */
4091
4092 /**
4093 * This is an abstract class that describes the definition of a widget's nested editable.
4094 * It is a type of values in the {@link CKEDITOR.plugins.widget.definition#editables} object.
4095 *
4096 * In the simplest case the definition is a string which is a CSS selector used to
4097 * find an element that will become a nested editable inside the widget. Note that
4098 * the widget element can be a nested editable, too.
4099 *
4100 * In the more advanced case a definition is an object with a required `selector` property.
4101 *
4102 * editables: {
4103 * header: 'h1',
4104 * content: {
4105 * selector: 'div.content',
4106 * allowedContent: 'p strong em; a[!href]'
4107 * }
4108 * }
4109 *
4110 * @class CKEDITOR.plugins.widget.nestedEditable.definition
4111 * @abstract
4112 */
4113
4114 /**
4115 * The CSS selector used to find an element which will become a nested editable.
4116 *
4117 * @property {String} selector
4118 */
4119
4120 /**
4121 * The [Advanced Content Filter](#!/guide/dev_advanced_content_filter) rules
4122 * which will be used to limit the content allowed in this nested editable.
4123 * This option is similar to {@link CKEDITOR.config#allowedContent} and one can
4124 * use it to limit the editor features available in the nested editable.
4125 *
4126 * If no `allowedContent` is specified, the editable will use the editor default
4127 * {@link CKEDITOR.editor#filter}.
4128 *
4129 * @property {CKEDITOR.filter.allowedContentRules} allowedContent
4130 */
4131
4132 /**
4133 * The [Advanced Content Filter](#!/guide/dev_advanced_content_filter) rules
4134 * which will be used to blacklist elements within this nested editable.
4135 * This option is similar to {@link CKEDITOR.config#disallowedContent}.
4136 *
4137 * Note that `disallowedContent` work on top of the definition's {@link #allowedContent}.
4138 *
4139 * @since 4.7.3
4140 * @property {CKEDITOR.filter.disallowedContentRules} disallowedContent
4141 */
4142
4143 /**
4144 * Nested editable name displayed in the elements path.
4145 *
4146 * @property {String} pathName
4147 */