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