363 lines
12 KiB
JavaScript
363 lines
12 KiB
JavaScript
/**
|
|
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
|
|
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
|
*/
|
|
|
|
/**
|
|
* @fileOverview A plugin created to handle ticket https://dev.ckeditor.com/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 ) {
|
|
// Ctrl/Cmd + A
|
|
if ( evt.data.getKeystroke() == CKEDITOR.CTRL + 65 ) {
|
|
// 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](https://ckeditor.com/cke4/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' );
|
|
}
|
|
|
|
} )();
|