Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

i/4665: Added customization of accept mention keys #9751

Merged
merged 6 commits into from
Aug 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions packages/ckeditor5-mention/src/mention.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,32 @@ export default class Mention extends Plugin {
* @member {Array.<module:mention/mention~MentionFeed>} module:mention/mention~MentionConfig#feeds
*/

/**
* The configuration of the custom commit keys supported by the editor.
*
* ClassicEditor
* .create( editorElement, {
* plugins: [ Mention, ... ],
* mention: {
* // [ Enter, Space ]
* commitKeys: [ 13, 32 ]
* feeds: [
* { ... }
* ...
* ]
* }
* } )
* .then( ... )
* .catch( ... );
*
* Custom commit keys configuration allows you to customize how users will confirm the selection of mentions from the dropdown list.
* You can add as many mention commit keys as you need. For instance, in the snippet above new mentions will be committed by pressing
* either <kbd>Enter</kbd> or <kbd>Space</kbd> (13 and 32 key codes respectively).
*
* @member {Array.<Number>} module:mention/mention~MentionConfig#commitKeys
* @default [ 13, 9 ] // [ Enter, Tab ]
*/

/**
* The mention feed descriptor. Used in {@link module:mention/mention~MentionConfig `config.mention`}.
*
Expand Down
33 changes: 20 additions & 13 deletions packages/ckeditor5-mention/src/mentionui.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,19 @@ import MentionListItemView from './ui/mentionlistitemview';

const VERTICAL_SPACING = 3;

// The key codes that mention UI handles when it is open.
const handledKeyCodes = [
// The key codes that mention UI handles when it is open (without commit keys).
const defaultHandledKeyCodes = [
keyCodes.arrowup,
keyCodes.arrowdown,
keyCodes.enter,
keyCodes.tab,
keyCodes.esc
];

// Dropdown commit key codes.
const defaultCommitKeyCodes = [
keyCodes.enter,
keyCodes.tab
];

/**
* The mention UI feature.
*
Expand Down Expand Up @@ -90,6 +94,9 @@ export default class MentionUI extends Plugin {
init() {
const editor = this.editor;

const commitKeys = editor.config.get( 'mention.commitKeys' ) || defaultCommitKeyCodes;
const handledKeyCodes = defaultHandledKeyCodes.concat( commitKeys );

/**
* The contextual balloon plugin instance.
*
Expand All @@ -112,7 +119,7 @@ export default class MentionUI extends Plugin {
this._mentionsView.selectPrevious();
}

if ( data.keyCode == keyCodes.enter || data.keyCode == keyCodes.tab ) {
if ( commitKeys.includes( data.keyCode ) ) {
this._mentionsView.executeSelected();
}

Expand Down Expand Up @@ -165,6 +172,14 @@ export default class MentionUI extends Plugin {

this.on( 'requestFeed:response', ( evt, data ) => this._handleFeedResponse( data ) );
this.on( 'requestFeed:error', () => this._hideUIAndRemoveMarker() );

// Checks if a given key code is handled by the mention UI.
//
// @param {Number}
// @returns {Boolean}
function isHandledKey( keyCode ) {
return handledKeyCodes.includes( keyCode );
}
}

/**
Expand Down Expand Up @@ -686,14 +701,6 @@ function createFeedCallback( feedItems ) {
};
}

// Checks if a given key code is handled by the mention UI.
//
// @param {Number}
// @returns {Boolean}
function isHandledKey( keyCode ) {
return handledKeyCodes.includes( keyCode );
}

// Checks if position in inside or right after a text with a mention.
//
// @param {module:engine/model/position~Position} position.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<div id="editor">
<p>Hello <span class="mention" data-mention="@Ted">@Ted</span>.</p>

<figure class="image">
<img src="sample.jpg" />
<figcaption>CKEditor logo - caption</figcaption>
</figure>
</div>
142 changes: 142 additions & 0 deletions packages/ckeditor5-mention/tests/manual/mention-custom-commitkeys.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/* global console, window */

import global from '@ckeditor/ckeditor5-utils/src/dom/global';

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Mention from '../../src/mention';
import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline';
import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';

import { toWidget, viewToModelPositionOutsideModelElement } from '@ckeditor/ckeditor5-widget/src/utils';
import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';

import { keyCodes } from 'ckeditor5/src/utils';

class InlineWidget extends Plugin {
constructor( editor ) {
super( editor );

editor.model.schema.register( 'placeholder', {
allowWhere: '$text',
isObject: true,
isInline: true,
allowAttributes: [ 'type' ]
} );

editor.conversion.for( 'editingDowncast' ).elementToElement( {
model: 'placeholder',
view: ( modelItem, conversionApi ) => {
const widgetElement = createPlaceholderView( modelItem, conversionApi );

return toWidget( widgetElement, conversionApi.writer );
}
} );

editor.conversion.for( 'dataDowncast' ).elementToElement( {
model: 'placeholder',
view: createPlaceholderView
} );

editor.conversion.for( 'upcast' ).elementToElement( {
view: 'placeholder',
model: ( viewElement, { writer } ) => {
let type = 'general';

if ( viewElement.childCount ) {
const text = viewElement.getChild( 0 );

if ( text.is( '$text' ) ) {
type = text.data.slice( 1, -1 );
}
}

return writer.createElement( 'placeholder', { type } );
}
} );

editor.editing.mapper.on(
'viewToModelPosition',
viewToModelPositionOutsideModelElement( editor.model, viewElement => viewElement.name == 'placeholder' )
);

this._createToolbarButton();

function createPlaceholderView( modelItem, { writer } ) {
const widgetElement = writer.createContainerElement( 'placeholder' );
const viewText = writer.createText( '{' + modelItem.getAttribute( 'type' ) + '}' );

writer.insert( writer.createPositionAt( widgetElement, 0 ), viewText );

return widgetElement;
}
}

_createToolbarButton() {
const editor = this.editor;
const t = editor.t;

editor.ui.componentFactory.add( 'placeholder', locale => {
const buttonView = new ButtonView( locale );

buttonView.set( {
label: t( 'Insert placeholder' ),
tooltip: true,
withText: true
} );

this.listenTo( buttonView, 'execute', () => {
const model = editor.model;

model.change( writer => {
const placeholder = writer.createElement( 'placeholder', { type: 'placeholder' } );

model.insertContent( placeholder );

writer.setSelection( placeholder, 'on' );
} );
} );

return buttonView;
} );
}
}

ClassicEditor
.create( global.document.querySelector( '#editor' ), {
plugins: [ ArticlePluginSet, Underline, Mention, InlineWidget ],
toolbar: [
'heading',
'|', 'bulletedList', 'numberedList', 'blockQuote',
'|', 'bold', 'italic', 'underline', 'link',
'|', 'insertTable', 'placeholder',
'|', 'undo', 'redo'
],
image: {
toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ]
},
table: {
contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ],
tableToolbar: [ 'bold', 'italic' ]
},
mention: {
commitKeys: [ keyCodes.a, keyCodes.space ],
feeds: [
{
marker: '@',
feed: [ '@Barney', '@Lily', '@Marshall', '@Robin', '@Ted' ]
}
]
}
} )
.then( editor => {
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## Mention

The mention configuration with a custom `config.mention.commitKeys` configuration and a static list of autocomplete feed:

### Configuration

Type "@" to display the list of available mentions.

### Interaction

- Use **<kbd>arrowup</kbd>** to select previous item
- Use **<kbd>arrowdown</kbd>** to select next item
- Use **<kbd>space</kbd>** or **<kbd>a</kbd>** keys to insert a mention into the documentation.
71 changes: 70 additions & 1 deletion packages/ckeditor5-mention/tests/mentionui.js
Original file line number Diff line number Diff line change
Expand Up @@ -1549,7 +1549,7 @@ describe( 'MentionUI', () => {
};

const keyUpEvtData = {
keyCode: keyCodes.arrowdown,
keyCode: keyCodes.arrowup,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a keyDownEvtData above that's using keyCodes.arrowdown, and this one is called keyUpEvtData - and it looked like it should've been using the keyCodes.arrowup as it makes more sense.

preventDefault: sinon.spy(),
stopPropagation: sinon.spy()
};
Expand Down Expand Up @@ -2099,6 +2099,75 @@ describe( 'MentionUI', () => {
} );
} );
}

describe( 'overriding commit keys using config.mention.commitKeys', () => {
const issues = [
{ id: '@Ted' },
{ id: '@Barney' },
{ id: '@Robin' },
{ id: '@Lily' },
{ id: '@Marshal' }
];

beforeEach( () => {
return createClassicTestEditor( {
commitKeys: [ keyCodes.a ],
feeds: [
{
marker: '@',
feed: feedText => issues.filter( issue => issue.id.includes( feedText ) )
}
]
} );
} );

// Testing if custom key configuration will execute the mention command.
testExecuteKey( 'a', keyCodes.a, issues );

it( 'should no longer commit on enter (default)', () => {
setData( model, '<paragraph>foo []</paragraph>' );

model.change( writer => {
writer.insertText( '@', doc.selection.getFirstPosition() );
} );

return waitForDebounce()
.then( () => {
const command = editor.commands.get( 'mention' );
const executeSpy = testUtils.sinon.spy( command, 'execute' );

fireKeyDownEvent( {
keyCode: keyCodes.enter,
preventDefault: sinon.spy(),
stopPropagation: sinon.spy()
} );

sinon.assert.notCalled( executeSpy );
} );
} );

it( 'should no longer commit on tab (default)', () => {
setData( model, '<paragraph>foo []</paragraph>' );

model.change( writer => {
writer.insertText( '@', doc.selection.getFirstPosition() );
} );

return waitForDebounce()
.then( () => {
const command = editor.commands.get( 'mention' );
const executeSpy = testUtils.sinon.spy( command, 'execute' );

fireKeyDownEvent( {
keyCode: keyCodes.tab,
preventDefault: sinon.spy(),
stopPropagation: sinon.spy()
} );

sinon.assert.notCalled( executeSpy );
} );
} );
} );
} );

describe( 'execute', () => {
Expand Down