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 1 commit
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
23 changes: 23 additions & 0 deletions packages/ckeditor5-mention/src/mention.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,29 @@ export default class Mention extends Plugin {
* @member {Array.<module:mention/mention~MentionFeed>} module:mention/mention~MentionConfig#feeds
*/

/**
* The configuration of the custom commitKeys support by the editor
mateuszzagorski marked this conversation as resolved.
Show resolved Hide resolved
*
* ClassicEditor
* .create( editorElement, {
* plugins: [ Mention, ... ],
* mention: {
* commitKeys: [ keyCode.enter, keyCode.space ]
mateuszzagorski marked this conversation as resolved.
Show resolved Hide resolved
* feeds: [
* { ... }
* ...
* ]
* }
* } )
* .then( ... )
* .catch( ... );
*
* You can use as many mention commitKeys, but they have to use different keyCodes.
mateuszzagorski marked this conversation as resolved.
Show resolved Hide resolved
* For example, you can use the custom configuration to commit by using either `keyCode.enter` or `keyCode.space`.
mateuszzagorski marked this conversation as resolved.
Show resolved Hide resolved
*
* @member {Array.<module:mention/mention~MentionFeed>} module:mention/mention~MentionConfig#commitKeys
*/
mateuszzagorski marked this conversation as resolved.
Show resolved Hide resolved

/**
* The mention feed descriptor. Used in {@link module:mention/mention~MentionConfig `config.mention`}.
*
Expand Down
34 changes: 21 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,10 @@ export default class MentionUI extends Plugin {
init() {
const editor = this.editor;

const configCommitKeys = editor.config.get( 'mention.commitKeys' );
const commitKeys = Array.isArray( configCommitKeys ) ? configCommitKeys : defaultCommitKeyCodes;
mateuszzagorski marked this conversation as resolved.
Show resolved Hide resolved
const handledKeyCodes = defaultHandledKeyCodes.concat( commitKeys );

/**
* The contextual balloon plugin instance.
*
Expand All @@ -112,7 +120,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 +173,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 +702,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">
mateuszzagorski marked this conversation as resolved.
Show resolved Hide resolved
<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 @@
/**
mateuszzagorski marked this conversation as resolved.
Show resolved Hide resolved
* @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, keyCodes.space ],
mateuszzagorski marked this conversation as resolved.
Show resolved Hide resolved
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,37 @@
## Mention
mateuszzagorski marked this conversation as resolved.
Show resolved Hide resolved

The mention configuration with a custom commitKeys configuration and static list of autocomplete feed:
mateuszzagorski marked this conversation as resolved.
Show resolved Hide resolved

### Configuration

The feeds:

1. Static list with `@` marker:

- Barney
- Lily
- Marshall
- Robin
- Ted

### Interaction
mateuszzagorski marked this conversation as resolved.
Show resolved Hide resolved

You can interact with mention panel with keyboard:

- 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.
mateuszzagorski marked this conversation as resolved.
Show resolved Hide resolved
- The <kbd>esc</kbd> should close the panel.

Mention panel should be closed on:
- Click outside the panel view.
- Changing selection - like placing it in other part of text.

### Editing behavior:

The mention should be removed from the text when:

- typing inside a mention
- removing characters from a mention
- breaking the mention (<kbd>enter</kbd>)
- pasting part of a mention
mateuszzagorski marked this conversation as resolved.
Show resolved Hide resolved
49 changes: 48 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,53 @@ describe( 'MentionUI', () => {
} );
} );
}

describe( 'allows for overriding keys using config.mention.commitKeys', () => {
mateuszzagorski marked this conversation as resolved.
Show resolved Hide resolved
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 ) )
}
]
} );
} );

// Supports a custom key set in the config.
mateuszzagorski marked this conversation as resolved.
Show resolved Hide resolved
testExecuteKey( 'a', keyCodes.a, issues );

it( 'overrides default commit keys', () => {
mateuszzagorski marked this conversation as resolved.
Show resolved Hide resolved
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,
mateuszzagorski marked this conversation as resolved.
Show resolved Hide resolved
preventDefault: sinon.spy(),
stopPropagation: sinon.spy()
} );

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

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