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