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

Commit

Permalink
Fix: Mention can now also be preceded by a bracket, quote or soft bre…
Browse files Browse the repository at this point in the history
…ak. Closes #44.
  • Loading branch information
mlewand authored May 13, 2019
2 parents 9ae7f30 + eba5b82 commit 86262d1
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 14 deletions.
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 {
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

0 comments on commit 86262d1

Please sign in to comment.