--- /dev/null
+/**\r
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+ * For licensing, see LICENSE.md or http://ckeditor.com/license\r
+ */\r
+\r
+/**\r
+ * @fileOverview A plugin created to handle ticket http://dev.ckeditor.com/ticket/11064. While the issue is caused by native WebKit/Blink behaviour,\r
+ * this plugin can be easily detached or modified when the issue is fixed in the browsers without changing the core.\r
+ * When Ctrl/Cmd + A is pressed to select all content it does not work due to a bug in\r
+ * Webkit/Blink if a non-editable element is at the beginning or the end of the content.\r
+ */\r
+\r
+( function() {\r
+ 'use strict';\r
+\r
+ CKEDITOR.plugins.add( 'widgetselection', {\r
+\r
+ init: function( editor ) {\r
+ if ( CKEDITOR.env.webkit ) {\r
+ var widgetselection = CKEDITOR.plugins.widgetselection;\r
+\r
+ editor.on( 'contentDom', function( evt ) {\r
+\r
+ var editor = evt.editor,\r
+ doc = editor.document,\r
+ editable = editor.editable();\r
+\r
+ editable.attachListener( doc, 'keydown', function( evt ) {\r
+ var data = evt.data.$;\r
+\r
+ // Ctrl/Cmd + A\r
+ if ( evt.data.getKey() == 65 && ( CKEDITOR.env.mac && data.metaKey || !CKEDITOR.env.mac && data.ctrlKey ) ) {\r
+\r
+ // Defer the call so the selection is already changed by the pressed keys.\r
+ CKEDITOR.tools.setTimeout( function() {\r
+\r
+ // Manage filler elements on keydown. If there is no need\r
+ // to add fillers, we need to check and clean previously used once.\r
+ if ( !widgetselection.addFillers( editable ) ) {\r
+ widgetselection.removeFillers( editable );\r
+ }\r
+ }, 0 );\r
+ }\r
+ }, null, null, -1 );\r
+\r
+ // Check and clean previously used fillers.\r
+ editor.on( 'selectionCheck', function( evt ) {\r
+ widgetselection.removeFillers( evt.editor.editable() );\r
+ } );\r
+\r
+ // Remove fillers on paste before data gets inserted into editor.\r
+ editor.on( 'paste', function( evt ) {\r
+ evt.data.dataValue = widgetselection.cleanPasteData( evt.data.dataValue );\r
+ } );\r
+\r
+ if ( 'selectall' in editor.plugins ) {\r
+ widgetselection.addSelectAllIntegration( editor );\r
+ }\r
+ } );\r
+ }\r
+ }\r
+ } );\r
+\r
+ /**\r
+ * A set of helper methods for the Widget Selection plugin.\r
+ *\r
+ * @property widgetselection\r
+ * @member CKEDITOR.plugins\r
+ * @since 4.6.1\r
+ */\r
+ CKEDITOR.plugins.widgetselection = {\r
+\r
+ /**\r
+ * The start filler element reference.\r
+ *\r
+ * @property {CKEDITOR.dom.element}\r
+ * @member CKEDITOR.plugins.widgetselection\r
+ * @private\r
+ */\r
+ startFiller: null,\r
+\r
+ /**\r
+ * The end filler element reference.\r
+ *\r
+ * @property {CKEDITOR.dom.element}\r
+ * @member CKEDITOR.plugins.widgetselection\r
+ * @private\r
+ */\r
+ endFiller: null,\r
+\r
+ /**\r
+ * An attribute which identifies the filler element.\r
+ *\r
+ * @property {String}\r
+ * @member CKEDITOR.plugins.widgetselection\r
+ * @private\r
+ */\r
+ fillerAttribute: 'data-cke-filler-webkit',\r
+\r
+ /**\r
+ * The default content of the filler element. Note: The filler needs to have `visible` content.\r
+ * Unprintable elements or empty content do not help as a workaround.\r
+ *\r
+ * @property {String}\r
+ * @member CKEDITOR.plugins.widgetselection\r
+ * @private\r
+ */\r
+ fillerContent: ' ',\r
+\r
+ /**\r
+ * Tag name which is used to create fillers.\r
+ *\r
+ * @property {String}\r
+ * @member CKEDITOR.plugins.widgetselection\r
+ * @private\r
+ */\r
+ fillerTagName: 'div',\r
+\r
+ /**\r
+ * Adds a filler before or after a non-editable element at the beginning or the end of the `editable`.\r
+ *\r
+ * @param {CKEDITOR.editable} editable\r
+ * @returns {Boolean}\r
+ * @member CKEDITOR.plugins.widgetselection\r
+ */\r
+ addFillers: function( editable ) {\r
+ var editor = editable.editor;\r
+\r
+ // Whole content should be selected, if not fix the selection manually.\r
+ if ( !this.isWholeContentSelected( editable ) && editable.getChildCount() > 0 ) {\r
+\r
+ var firstChild = editable.getFirst( filterTempElements ),\r
+ lastChild = editable.getLast( filterTempElements );\r
+\r
+ // Check if first element is editable. If not prepend with filler.\r
+ if ( firstChild && firstChild.type == CKEDITOR.NODE_ELEMENT && !firstChild.isEditable() ) {\r
+ this.startFiller = this.createFiller();\r
+ editable.append( this.startFiller, 1 );\r
+ }\r
+\r
+ // Check if last element is editable. If not append filler.\r
+ if ( lastChild && lastChild.type == CKEDITOR.NODE_ELEMENT && !lastChild.isEditable() ) {\r
+ this.endFiller = this.createFiller( true );\r
+ editable.append( this.endFiller, 0 );\r
+ }\r
+\r
+ // Reselect whole content after any filler was added.\r
+ if ( this.hasFiller( editable ) ) {\r
+ var rangeAll = editor.createRange();\r
+ rangeAll.selectNodeContents( editable );\r
+ rangeAll.select();\r
+ return true;\r
+ }\r
+ }\r
+ return false;\r
+ },\r
+\r
+ /**\r
+ * Removes filler elements or updates their references.\r
+ *\r
+ * It will **not remove** filler elements if the whole content is selected, as it would break the\r
+ * selection.\r
+ *\r
+ * @param {CKEDITOR.editable} editable\r
+ * @member CKEDITOR.plugins.widgetselection\r
+ */\r
+ removeFillers: function( editable ) {\r
+ // If startFiller or endFiller exists and not entire content is selected it means the selection\r
+ // just changed from selected all. We need to remove fillers and set proper selection/content.\r
+ if ( this.hasFiller( editable ) && !this.isWholeContentSelected( editable ) ) {\r
+\r
+ var startFillerContent = editable.findOne( this.fillerTagName + '[' + this.fillerAttribute + '=start]' ),\r
+ endFillerContent = editable.findOne( this.fillerTagName + '[' + this.fillerAttribute + '=end]' );\r
+\r
+ if ( this.startFiller && startFillerContent && this.startFiller.equals( startFillerContent ) ) {\r
+ this.removeFiller( this.startFiller, editable );\r
+ } else {\r
+ // The start filler is still present but it is a different element than previous one. It means the\r
+ // undo recreating entirely selected content was performed. We need to update filler reference.\r
+ this.startFiller = startFillerContent;\r
+ }\r
+\r
+ if ( this.endFiller && endFillerContent && this.endFiller.equals( endFillerContent ) ) {\r
+ this.removeFiller( this.endFiller, editable );\r
+ } else {\r
+ // Same as with start filler.\r
+ this.endFiller = endFillerContent;\r
+ }\r
+ }\r
+ },\r
+\r
+ /**\r
+ * Removes fillers from the paste data.\r
+ *\r
+ * @param {String} data\r
+ * @returns {String}\r
+ * @member CKEDITOR.plugins.widgetselection\r
+ * @private\r
+ */\r
+ cleanPasteData: function( data ) {\r
+ if ( data && data.length ) {\r
+ data = data\r
+ .replace( this.createFillerRegex(), '' )\r
+ .replace( this.createFillerRegex( true ), '' );\r
+ }\r
+ return data;\r
+ },\r
+\r
+ /**\r
+ * Checks if the entire content of the given editable is selected.\r
+ *\r
+ * @param {CKEDITOR.editable} editable\r
+ * @returns {Boolean}\r
+ * @member CKEDITOR.plugins.widgetselection\r
+ * @private\r
+ */\r
+ isWholeContentSelected: function( editable ) {\r
+\r
+ var range = editable.editor.getSelection().getRanges()[ 0 ];\r
+ if ( range ) {\r
+\r
+ if ( range && range.collapsed ) {\r
+ return false;\r
+\r
+ } else {\r
+ var rangeClone = range.clone();\r
+ rangeClone.enlarge( CKEDITOR.ENLARGE_ELEMENT );\r
+\r
+ return !!( rangeClone && editable && rangeClone.startContainer && rangeClone.endContainer &&\r
+ rangeClone.startOffset === 0 && rangeClone.endOffset === editable.getChildCount() &&\r
+ rangeClone.startContainer.equals( editable ) && rangeClone.endContainer.equals( editable ) );\r
+ }\r
+ }\r
+ return false;\r
+ },\r
+\r
+ /**\r
+ * Checks if there is any filler element in the given editable.\r
+ *\r
+ * @param {CKEDITOR.editable} editable\r
+ * @returns {Boolean}\r
+ * @member CKEDITOR.plugins.widgetselection\r
+ * @private\r
+ */\r
+ hasFiller: function( editable ) {\r
+ return editable.find( this.fillerTagName + '[' + this.fillerAttribute + ']' ).count() > 0;\r
+ },\r
+\r
+ /**\r
+ * Creates a filler element.\r
+ *\r
+ * @param {Boolean} [onEnd] If filler will be placed on end or beginning of the content.\r
+ * @returns {CKEDITOR.dom.element}\r
+ * @member CKEDITOR.plugins.widgetselection\r
+ * @private\r
+ */\r
+ createFiller: function( onEnd ) {\r
+ var filler = new CKEDITOR.dom.element( this.fillerTagName );\r
+ filler.setHtml( this.fillerContent );\r
+ filler.setAttribute( this.fillerAttribute, onEnd ? 'end' : 'start' );\r
+ filler.setAttribute( 'data-cke-temp', 1 );\r
+ filler.setStyles( {\r
+ display: 'block',\r
+ width: 0,\r
+ height: 0,\r
+ padding: 0,\r
+ border: 0,\r
+ margin: 0,\r
+ position: 'absolute',\r
+ top: 0,\r
+ left: '-9999px',\r
+ opacity: 0,\r
+ overflow: 'hidden'\r
+ } );\r
+\r
+ return filler;\r
+ },\r
+\r
+ /**\r
+ * Removes the specific filler element from the given editable. If the filler contains any content (typed or pasted),\r
+ * it replaces the current editable content. If not, the caret is placed before the first or after the last editable\r
+ * element (depends if the filler was at the beginning or the end).\r
+ *\r
+ * @param {CKEDITOR.dom.element} filler\r
+ * @param {CKEDITOR.editable} editable\r
+ * @member CKEDITOR.plugins.widgetselection\r
+ * @private\r
+ */\r
+ removeFiller: function( filler, editable ) {\r
+ if ( filler ) {\r
+ var editor = editable.editor,\r
+ currentRange = editable.editor.getSelection().getRanges()[ 0 ],\r
+ currentPath = currentRange.startPath(),\r
+ range = editor.createRange(),\r
+ insertedHtml,\r
+ fillerOnStart,\r
+ manuallyHandleCaret;\r
+\r
+ if ( currentPath.contains( filler ) ) {\r
+ insertedHtml = filler.getHtml();\r
+ manuallyHandleCaret = true;\r
+ }\r
+\r
+ fillerOnStart = filler.getAttribute( this.fillerAttribute ) == 'start';\r
+ filler.remove();\r
+ filler = null;\r
+\r
+ if ( insertedHtml && insertedHtml.length > 0 && insertedHtml != this.fillerContent ) {\r
+ editable.insertHtmlIntoRange( insertedHtml, editor.getSelection().getRanges()[ 0 ] );\r
+ range.setStartAt( editable.getChild( editable.getChildCount() - 1 ), CKEDITOR.POSITION_BEFORE_END );\r
+ editor.getSelection().selectRanges( [ range ] );\r
+\r
+ } else if ( manuallyHandleCaret ) {\r
+ if ( fillerOnStart ) {\r
+ range.setStartAt( editable.getFirst().getNext(), CKEDITOR.POSITION_AFTER_START );\r
+ } else {\r
+ range.setEndAt( editable.getLast().getPrevious(), CKEDITOR.POSITION_BEFORE_END );\r
+ }\r
+ editable.editor.getSelection().selectRanges( [ range ] );\r
+ }\r
+ }\r
+ },\r
+\r
+ /**\r
+ * Creates a regular expression which will match the filler HTML in the text.\r
+ *\r
+ * @param {Boolean} [onEnd] Whether a regular expression should be created for the filler at the beginning or\r
+ * the end of the content.\r
+ * @returns {RegExp}\r
+ * @member CKEDITOR.plugins.widgetselection\r
+ * @private\r
+ */\r
+ createFillerRegex: function( onEnd ) {\r
+ var matcher = this.createFiller( onEnd ).getOuterHtml()\r
+ .replace( /style="[^"]*"/gi, 'style="[^"]*"' )\r
+ .replace( />[^<]*</gi, '>[^<]*<' );\r
+\r
+ return new RegExp( ( !onEnd ? '^' : '' ) + matcher + ( onEnd ? '$' : '' ) );\r
+ },\r
+\r
+ /**\r
+ * Adds an integration for the [Select All](http://ckeditor.com/addon/selectall) plugin to the given `editor`.\r
+ *\r
+ * @private\r
+ * @param {CKEDITOR.editor} editor\r
+ * @member CKEDITOR.plugins.widgetselection\r
+ */\r
+ addSelectAllIntegration: function( editor ) {\r
+ var widgetselection = this;\r
+\r
+ editor.editable().attachListener( editor, 'beforeCommandExec', function( evt ) {\r
+ var editable = editor.editable();\r
+\r
+ if ( evt.data.name == 'selectAll' && editable ) {\r
+ widgetselection.addFillers( editable );\r
+ }\r
+ }, null, null, 9999 );\r
+ }\r
+ };\r
+\r
+\r
+ function filterTempElements( el ) {\r
+ return el.getName && !el.hasAttribute( 'data-cke-temp' );\r
+ }\r
+\r
+} )();\r