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