Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

T/44: Show mention after softBreak and some punctuation characters #61

Merged
merged 24 commits into from
May 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e0bf6e3
MentionUI will be shown when marker is typed after a soft break.
jodator Apr 25, 2019
e64b998
Whitelist some characters to enable mention UI after them.
jodator Apr 25, 2019
946721e
Update the regex for mention text watcher to ES2018 syntax.
jodator Apr 26, 2019
14de30b
Merge branch 'master' into t/44
jodator Apr 26, 2019
53d3d23
Fix pattern for $ marker.
jodator Apr 26, 2019
117c7f8
Add tests for ES2018 RegExp unicode property escapes.
jodator May 6, 2019
52fa52e
Add missing dev dependency.
jodator May 6, 2019
1ff27df
Add RegExp escapes for [ and ( in pattern string.
jodator May 7, 2019
7fcbccf
Fix RegExp escape for brackets.
jodator May 7, 2019
f216cd5
Fix RegExp unicode property escapes feature detection for Edge.
jodator May 7, 2019
42bb621
Update tests for env.isEdge check.
jodator May 7, 2019
ced456e
Update env.isEdge stub comment.
jodator May 7, 2019
51b59c8
Merge branch 'master' into t/44
jodator May 8, 2019
b9ebfa9
Internal: Remove browser sniffing for edge and instead use feature de…
mlewand May 10, 2019
a17d84f
Internal: Restructured the code to achieve 100% code coverage.
mlewand May 10, 2019
fcaeb31
Internal: Final adjustments to achieve 100% code coverage.
mlewand May 10, 2019
7d2f6c4
Tests: Reused common code for RegExp punctuation groups feature detec…
mlewand May 10, 2019
bb06299
Update src/mentionui.js docs.
jodator May 10, 2019
ef27aa7
Update tests/mentionui.js docs.
jodator May 10, 2019
1103d50
Update src/mentionui.js docs.
jodator May 10, 2019
cfcc99f
Code style fixes.
jodator May 10, 2019
20a4ec0
Update internal docs of createRegExp function.
jodator May 10, 2019
10a4276
Add punctuation initial quote unicode group support.
jodator May 10, 2019
eba5b82
Merge branch 't/44b' into t/44
jodator May 10, 2019
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@ckeditor/ckeditor5-font": "^11.1.0",
"@ckeditor/ckeditor5-paragraph": "^11.0.1",
"@ckeditor/ckeditor5-undo": "^11.0.1",
"@ckeditor/ckeditor5-widget": "^11.0.1",
"eslint": "^5.5.0",
"eslint-config-ckeditor5": "^1.0.11",
"husky": "^1.3.1",
Expand Down
35 changes: 35 additions & 0 deletions src/featuredetection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/**
* @module mention/featuredetection
*/

/**
* Holds feature detection resolutions used by the mention plugin.
*
* @protected
* @namespace
*/
export default {
/**
* Indicates whether the current browser supports ES2018 Unicode punctuation groups `\p{P}`.
*
* @type {Boolean}
*/
isPunctuationGroupSupported: ( function() {
let punctuationSupported = false;
// Feature detection for Unicode punctuation groups. It's added in ES2018. Currently Firefox and Edge does not support it.
// See https://github.com/ckeditor/ckeditor5-mention/issues/44#issuecomment-487002174.

try {
punctuationSupported = '.'.search( new RegExp( '[\\p{P}]', 'u' ) ) === 0;
} catch ( error ) {
// Firefox throws a SyntaxError when the group is unsupported.
}

return punctuationSupported;
}() )
};
26 changes: 20 additions & 6 deletions src/mentionui.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
import Collection from '@ckeditor/ckeditor5-utils/src/collection';
import clickOutsideHandler from '@ckeditor/ckeditor5-ui/src/bindings/clickoutsidehandler';
import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
import featureDetection from './featuredetection';
import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
Expand Down Expand Up @@ -533,15 +534,28 @@ function getBalloonPanelPositions( preferredPosition ) {
];
}

// Creates a regex pattern for the marker.
// Creates a RegExp pattern for the marker.
//
// Function has to be exported to achieve 100% code coverage.
//
// @param {String} marker
// @param {Number} minimumCharacters
// @returns {String}
function createPattern( marker, minimumCharacters ) {
// @returns {RegExp}
export function createRegExp( marker, minimumCharacters ) {
const numberOfCharacters = minimumCharacters == 0 ? '*' : `{${ minimumCharacters },}`;
const patternBase = featureDetection.isPunctuationGroupSupported ? '\\p{Ps}\\p{Pi}"\'' : '\\(\\[{"\'';

return new RegExp( buildPattern( patternBase, marker, numberOfCharacters ), 'u' );
}

return `(^| )(\\${ marker })([_a-zA-Z0-9À-ž]${ numberOfCharacters }?)$`;
// Helper to build a RegExp pattern string for the marker.
//
// @param {String} whitelistedCharacters
// @param {String} marker
// @param {Number} minimumCharacters
// @returns {String}
function buildPattern( whitelistedCharacters, marker, numberOfCharacters ) {
return `(^|[ ${ whitelistedCharacters }])([${ marker }])([_a-zA-Z0-9À-ž]${ numberOfCharacters }?)$`;
}

// Creates a test callback for the marker to be used in the text watcher instance.
Expand All @@ -550,7 +564,7 @@ function createPattern( marker, minimumCharacters ) {
// @param {Number} minimumCharacters
// @returns {Function}
function createTestCallback( marker, minimumCharacters ) {
const regExp = new RegExp( createPattern( marker, minimumCharacters ) );
const regExp = createRegExp( marker, minimumCharacters );

return text => regExp.test( text );
}
Expand All @@ -560,7 +574,7 @@ function createTestCallback( marker, minimumCharacters ) {
// @param {String} marker
// @returns {Function}
function createTextMatcher( marker ) {
const regExp = new RegExp( createPattern( marker, 0 ) );
const regExp = createRegExp( marker, 0 );

return text => {
const match = text.match( regExp );
Expand Down
16 changes: 12 additions & 4 deletions src/textwatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,17 @@ export default class TextWatcher {
*/
_getText() {
const editor = this.editor;
const selection = editor.model.document.selection;
const model = editor.model;
const selection = model.document.selection;

// Do nothing if the selection is not collapsed.
if ( !selection.isCollapsed ) {
return;
}

const block = selection.focus.parent;
const rangeBeforeSelection = model.createRange( model.createPositionAt( selection.focus.parent, 0 ), selection.focus );

return _getText( editor.model.createRangeIn( block ) ).slice( 0, selection.focus.offset );
return _getText( rangeBeforeSelection );
}
}

Expand All @@ -135,7 +136,14 @@ export default class TextWatcher {
* @returns {String}
*/
export function _getText( range ) {
return Array.from( range.getItems() ).reduce( ( a, b ) => a + b.data, '' );
return Array.from( range.getItems() ).reduce( ( rangeText, node ) => {
if ( node.is( 'softBreak' ) ) {
// Trim text to softBreak
return '';
}

return rangeText + node.data;
}, '' );
}

mix( TextWatcher, EmitterMixin );
Expand Down
97 changes: 95 additions & 2 deletions tests/manual/mention.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,109 @@ import Mention from '../../src/mention';
import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline';
import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
import Font from '@ckeditor/ckeditor5-font/src/font';
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';

class InlineWidget extends Plugin {
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the reason for adding a placeholder-like widget in this manual test?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because I wanted to test how this behaves with not only a <softBreak> but also with inline widget. (potential errors, etc).

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, viewWriter ) => {
const widgetElement = createPlaceholderView( modelItem, viewWriter );

return toWidget( widgetElement, viewWriter );
}
} );

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

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

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

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

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

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

this._createToolbarButton();

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

viewWriter.insert( viewWriter.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, Font, Mention ],
plugins: [ ArticlePluginSet, Underline, Font, Mention, InlineWidget ],
toolbar: [
'heading',
'|', 'bulletedList', 'numberedList', 'blockQuote',
'|', 'bold', 'italic', 'underline', 'link',
'|', 'fontFamily', 'fontSize', 'fontColor', 'fontBackgroundColor',
'|', 'insertTable',
'|', 'insertTable', 'placeholder',
'|', 'undo', 'redo'
],
image: {
Expand Down
Loading