N°2875 - Improve global implementation and integration with the trigger system

This commit is contained in:
Molkobain
2020-12-09 23:19:39 +01:00
parent c22f019ea1
commit e96400c43b
8 changed files with 656 additions and 34 deletions

View File

@@ -0,0 +1,454 @@
/**
* @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
'use strict';
( function() {
CKEDITOR._.mentions = {
cache: {}
};
var MARKER = '@',
MIN_CHARS = 2,
cache = CKEDITOR._.mentions.cache;
CKEDITOR.plugins.add( 'mentions', {
requires: 'autocomplete,textmatch,ajax',
instances: [],
init: function( editor ) {
var self = this;
editor.on( 'instanceReady', function() {
CKEDITOR.tools.array.forEach( editor.config.mentions || [], function( config ) {
self.instances.push( new Mentions( editor, config ) );
} );
} );
},
isSupportedEnvironment: function( editor ) {
return editor.plugins.autocomplete.isSupportedEnvironment( editor );
}
} );
/**
* The [Mentions](https://ckeditor.com/cke4/addon/mentions) plugin allows you to type a marker character and get suggested values for the
* text matches so that you do not have to write it on your own.
*
* The recommended way to add the mentions feature to an editor is by setting the {@link CKEDITOR.config#mentions config.mentions} option:
*
* ```javascript
* // Passing mentions configuration when creating the editor.
* CKEDITOR.replace( 'editor', {
* mentions: [ { feed: ['Anna', 'Thomas', 'John'], minChars: 0 } ]
* } );
*
* // Simple usage with the CKEDITOR.config.mentions property.
* CKEDITOR.config.mentions = [ { feed: ['Anna', 'Thomas', 'John'], minChars: 0 } ];
* ```
*
* @class CKEDITOR.plugins.mentions
* @since 4.10.0
* @constructor Creates a new instance of mentions and attaches it to the editor.
* @param {CKEDITOR.editor} editor The editor to watch.
* @param {CKEDITOR.plugins.mentions.configDefinition} config Configuration object keeping information about how to instantiate the mentions plugin.
*/
function Mentions( editor, config ) {
var feed = config.feed;
/**
* Indicates that a mentions instance is case-sensitive for simple items feed, i.e. an array feed.
*
* **Note:** This will take no effect on feeds using a callback or URLs, as in this case the results are expected to
* be already filtered.
*
* @property {Boolean} [caseSensitive=false]
* @readonly
*/
this.caseSensitive = config.caseSensitive;
/**
* The character that should trigger autocompletion.
*
* @property {String} [marker='@']
* @readonly
*/
this.marker = config.hasOwnProperty( 'marker' ) ? config.marker : MARKER;
/**
* The number of characters that should follow the marker character in order to trigger the mentions feature.
*
* @property {Number} [minChars=2]
* @readonly
*/
this.minChars = config.minChars !== null && config.minChars !== undefined ? config.minChars : MIN_CHARS;
/**
* The pattern used to match queries.
*
* The default pattern matches words with the query including the {@link #marker config.marker} and {@link #minChars config.minChars} properties.
*
* ```javascript
* // Match only words starting with "a".
* var pattern = /^a+\w*$/;
* ```
*
* @property {RegExp} pattern
* @readonly
*/
this.pattern = config.pattern || createPattern( this.marker, this.minChars );
/**
* Indicates if the URL feed responses will be cached.
*
* The cache is based on the URL request and is shared between all mentions instances (including different editors).
*
* @property {Boolean} [cache=true]
* @readonly
*/
this.cache = config.cache !== undefined ? config.cache : true;
/**
* @inheritdoc CKEDITOR.plugins.mentions.configDefinition#throttle
* @property {Number} [throttle=200]
* @readonly
*/
this.throttle = config.throttle !== undefined ? config.throttle : 200;
/**
* {@link CKEDITOR.plugins.autocomplete Autocomplete} instance used by the mentions feature to implement autocompletion logic.
*
* @property {CKEDITOR.plugins.autocomplete}
* @private
*/
this._autocomplete = new CKEDITOR.plugins.autocomplete( editor, {
textTestCallback: getTextTestCallback( this.marker, this.minChars, this.pattern ),
dataCallback: getDataCallback( feed, this ),
itemTemplate: config.itemTemplate,
outputTemplate: config.outputTemplate,
throttle: this.throttle,
itemsLimit: config.itemsLimit
} );
}
Mentions.prototype = {
/**
* Destroys the mentions instance.
*
* The view element and event listeners will be removed from the DOM.
*/
destroy: function() {
this._autocomplete.destroy();
}
};
function createPattern( marker, minChars ) {
// Match also diacritic characters (#2491).
var pattern = '\\' + marker + '[_a-zA-Z0-9À-ž]';
if ( minChars ) {
pattern += '{' + minChars + ',}';
} else {
pattern += '*';
}
pattern += '$';
return new RegExp( pattern );
}
function getTextTestCallback( marker, minChars, pattern ) {
return function( range ) {
if ( !range.collapsed ) {
return null;
}
return CKEDITOR.plugins.textMatch.match( range, matchCallback );
};
function matchCallback( text, offset ) {
var match = text.slice( 0, offset )
.match( pattern );
if ( !match ) {
return null;
}
// Do not proceed if a query is a part of word.
var prevChar = text[ match.index - 1];
if ( prevChar !== undefined && !prevChar.match( /\s+/ ) ) {
return null;
}
return {
start: match.index,
end: offset
};
}
}
function getDataCallback( feed, mentions ) {
return function( matchInfo, callback ) {
var query = matchInfo.query;
// We are removing marker here to give clean query result for the endpoint callback.
if ( mentions.marker ) {
query = query.substring( mentions.marker.length );
}
if ( CKEDITOR.tools.array.isArray( feed ) ) {
createArrayFeed();
} else if ( typeof feed === 'string' ) {
createUrlFeed();
} else {
feed( {
query: query,
marker: mentions.marker
}, resolveCallbackData );
}
function createArrayFeed() {
var data = indexArrayFeed( feed ).filter( function( item ) {
var itemName = item.name;
if ( !mentions.caseSensitive ) {
itemName = itemName.toLowerCase();
query = query.toLowerCase();
}
return itemName.indexOf( query ) === 0;
} );
resolveCallbackData( data );
}
function indexArrayFeed( feed ) {
var index = 1;
return CKEDITOR.tools.array.reduce( feed, function( current, name ) {
current.push( { name: name, id: index++ } );
return current;
}, [] );
}
function createUrlFeed() {
var encodedUrl = new CKEDITOR.template( feed )
.output( { encodedQuery: encodeURIComponent( query ) } );
if ( mentions.cache && cache[ encodedUrl ] ) {
return resolveCallbackData( cache[ encodedUrl ] );
}
CKEDITOR.ajax.load( encodedUrl, function( data ) {
var items = JSON.parse( data );
// Cache URL responses for performance improvement (#1969).
if ( mentions.cache && items !== null ) {
cache[ encodedUrl ] = items;
}
resolveCallbackData( items );
} );
}
function resolveCallbackData( data ) {
if ( !data ) {
return;
}
// We don't want to change item data, so lets create new one.
var newData = CKEDITOR.tools.array.map( data, function( item ) {
var name = mentions.marker + item.name;
return CKEDITOR.tools.object.merge( item, { name: name } );
} );
callback( newData );
}
};
}
CKEDITOR.plugins.mentions = Mentions;
/**
* A list of mentions configuration objects.
*
* For each configuration object a new {@link CKEDITOR.plugins.mentions mentions} plugin instance will be created and attached to the editor.
*
* ```javascript
* config.mentions = [
* { feed: [ 'Anna', 'Thomas', 'Jack' ], minChars: 0 },
* { feed: backendApiFunction, marker: '#' },
* { feed: '/users?query={encodedQuery}', marker: '$' }
* ];
*
* ```
*
* @cfg {CKEDITOR.plugins.mentions.configDefinition[]} [mentions]
* @since 4.10.0
* @member CKEDITOR.config
*/
/**
* Abstract class describing the definition of a {@link CKEDITOR.plugins.mentions mentions} plugin configuration.
*
* This virtual class illustrates the properties that the developers can use to define and create
* a mentions configuration definition.
* The mentions definition object represents an object as a set of properties defining a mentions
* data feed and its optional parameters.
*
* Simple usage:
*
* ```javascript
* var definition = { feed: ['Anna', 'Thomas', 'John'], minChars: 0 };
* ```
*
* @class CKEDITOR.plugins.mentions.configDefinition
* @abstract
* @since 4.10.0
*/
/**
* The feed of items to be displayed in the mentions plugin.
*
* Essential option which should be configured to create a correct mentions configuration definition.
* There are three different ways to create a data feed:
*
* * A simple array of text matches as a synchronous data feed.
* * A backend URL string responding with a list of items in the JSON format. This method utilizes an asynchronous data feed.
* * A function allowing to use an asynchronous callback to customize the data source.
* Gives the freedom to use any data source depending on your implementation.
*
* # An array of text matches
* The easiest way to configure the data feed is to provide an array of text matches.
* The mentions plugin will use a synchronous data feed and create item IDs by itself.
* The biggest advantage of this method is its simplicity, although it is limited to a synchronous data feed.
* Please see two other methods if you require more complex techniques to fetch the text matches.
*
*```javascript
* var definition = { feed: ['Anna', 'Thomas', 'John'], minChars: 0 };
* ```
*
* By default query matching for an array feed is case insensitive.
* You can change this behavior by setting the {@link #caseSensitive caseSensitive} property to `true`.
*
* # A backend URL string
* You can provide a backend URL string which will be used to fetch text matches from a custom endpoint service.
* Each time the user types matching text into an editor, your backend service will be queried for text matches.
* An Ajax URL request should response with an array of matches in the JSON format. A URL response will appear in the mentions dropdown.
*
* A backend URL string features the special `encodedQuery` variable replaced with a mentions query.
* The `encodedQuery` variable allows you to create a customized URL which can be both RESTful API compliant or any other
* URL which suits your needs. E.g. for the query `@anna` and the given URL `/users?name={encodedQuery}` your endpoint
* service will be queried with `/users?name=anna`.
*
* ```javascript
* var definition = { feed: '/users?query={encodedQuery}' };
* ```
*
* To avoid multiple HTTP requests to your endpoint service, each HTTP response is cached by default and shared globally.
* See the {@link #cache cache} property for more details.
*
* # Function feed
* This method is recommended for advanced users who would like to take full control of the data feed.
* It allows you to provide the data feed as a function that accepts two parameters: `options` and `callback`.
* The provided function will be called every time the user types matching text into an editor.
*
* The `options` object contains information about the current query and a {@link #marker marker}.
*
* ```javascript
* { query: 'anna', marker: '@' }
* ```
*
* The `callback` argument should be used to pass an array of text match items into the mentions instance.
*
* ```javascript
* callback( [ { id: 1, name: 'anna' }, { id: 2, name: 'annabelle' } ] );
* ```
*
* Depending on your use case, you can use this code as an example boilerplate to create your own function feed:
*
* ```javascript
* var definition = {
* feed: function( opts, callback ) {
* var xhr = new XMLHttpRequest();
*
* xhr.onreadystatechange = function() {
* if ( xhr.readyState == 4 ) {
* if ( xhr.status == 200 ) {
* callback( JSON.parse( this.responseText ) );
* } else {
* callback( [] );
* }
* }
* }
*
* xhr.open( 'GET', '/users?name=' + opts.query );
* xhr.send();
* }
* };
* ```
*
* **Other details**
*
* When using the asynchronous method, i.e. a backend URL string or a function,
* you should provide correct object structure containing a unique item ID and a name.
*
* ```javascript
* // Example of expected results from the backend API.
* // The `firstName` and `lastName` properties are optional.
* [
* { id: 1, name: 'anna87', firstName: 'Anna', lastName: 'Doe' },
* { id: 2, name: 'tho-mass', firstName: 'Thomas', lastName: 'Doe' },
* { id: 3, name: 'ozzy', firstName: 'John', lastName: 'Doe' }
* ]
* ```
*
* @property {String/String[]/Function} feed
*/
/**
* @inheritdoc CKEDITOR.plugins.autocomplete.configDefinition#itemTemplate
* @property {String} [itemTemplate]
*/
/**
* @inheritdoc CKEDITOR.plugins.autocomplete.configDefinition#outputTemplate
* @property {String} [outputTemplate]
*/
/**
* @inheritdoc CKEDITOR.plugins.autocomplete.configDefinition#throttle
* @property {Number} [throttle=200]
*/
/**
* @inheritdoc CKEDITOR.plugins.mentions#marker
* @property {String} [marker='@']
*/
/**
* @inheritdoc CKEDITOR.plugins.mentions#minChars
* @property {Number} [minChars=2]
*/
/**
* @inheritdoc CKEDITOR.plugins.mentions#caseSensitive
* @property {Boolean} [caseSensitive=false]
*/
/**
* @inheritdoc CKEDITOR.plugins.mentions#cache
* @property {Boolean} [cache=true]
*/
/**
* @inheritdoc CKEDITOR.plugins.mentions#pattern
* @property {RegExp} pattern
*/
/**
* @inheritdoc CKEDITOR.plugins.autocomplete.configDefinition#itemsLimit
* @property {Number} [itemsLimit]
*/
} )();

View File

@@ -0,0 +1,147 @@
<!DOCTYPE html>
<!--
Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
-->
<html lang="en">
<head>
<meta charset="utf-8">
<title>Mentions &mdash; CKEditor Sample</title>
<script src="../../../ckeditor.js"></script>
<link rel="stylesheet" href="../../../samples/css/samples.css">
<meta name="description" content="Try the latest sample of CKEditor 4 and learn more about customizing your WYSIWYG editor with endless possibilities.">
</head>
<body>
<style>
.adjoined-bottom:before {
height: 270px;
}
</style>
<nav class="navigation-a">
<div class="grid-container">
<ul class="navigation-a-left grid-width-70">
<li><a href="https://ckeditor.com">Project Homepage</a></li>
<li><a href="https://github.com/ckeditor/ckeditor4/issues">I found a bug</a></li>
<li><a href="https://github.com/ckeditor/ckeditor4" class="icon-pos-right icon-navigation-a-github">Fork CKEditor on GitHub</a></li>
</ul>
<ul class="navigation-a-right grid-width-30">
<li><a href="https://ckeditor.com/blog/">CKEditor Blog</a></li>
</ul>
</div>
</nav>
<header class="header-a">
<div class="grid-container">
<h1 class="header-a-logo grid-width-30">
<img src="../../../samples/img/logo.svg" onerror="this.src='../../../samples/img/logo.png'; this.onerror=null;" alt="CKEditor Sample">
</h1>
</div>
</header>
<main>
<div class="adjoined-top">
<div class="grid-container">
<div class="content grid-width-100">
<h1>Mentions Demo</h1>
<p>This sample shows Mentions feature in action. Type &#8220; @ &#8221; to open simple autocompletion with array feed, &#8220; $ &#8221; (min 1 character) to open asynchronous autocompletion with URL string feed or &#8220; # &#8221; (min 2 characters) to open asynchronous autocompletion with custom source of data.</p>
</div>
</div>
</div>
<div class="adjoined-bottom">
<div class="grid-container">
<div class="grid-width-100">
<div id="editor">
<h1>Mentions plugin</h1>
<p>Feel free to mention @anna, @cris, @thomas or anyone else.</p>
</div>
</div>
</div>
</div>
</main>
<footer class="footer-a grid-container">
<div class="grid-container">
<p class="grid-width-100">
CKEditor &ndash; The text editor for the Internet &ndash; <a class="samples" href="https://ckeditor.com/">https://ckeditor.com</a>
</p>
<p class="grid-width-100" id="copy">
Copyright &copy; 2003-2019, <a class="samples" href="https://cksource.com/">CKSource</a> &ndash; Frederico Knabben. All rights reserved.
</p>
</div>
</footer>
<script>
'use strict';
( function() {
var data = [
{ id: 1, name: 'john', fullName: 'John Doe' },
{ id: 2, name: 'thomas', fullName: 'Thomas Doe' },
{ id: 3, name: 'anna', fullName: 'Anna Doe' },
{ id: 4, name: 'annabelle', fullName: 'Annabelle Doe' },
{ id: 5, name: 'cris', fullName: 'Cris Doe' },
{ id: 6, name: 'julia', fullName: 'Julia Doe' }
];
CKEDITOR.replace( 'editor', {
height: 240,
extraPlugins: 'mentions,easyimage,sourcearea,toolbar,undo,wysiwygarea,basicstyles',
extraAllowedContent: 'h1',
toolbar: [
{ name: 'document', items: [ 'Source', 'Undo', 'Redo' ] },
{ name: 'basicstyles', items: [ 'Bold', 'Italic', 'Strike' ] },
],
mentions: [
{
feed: CKEDITOR.tools.array.map( data, function( item ) {
return item.name;
} ),
minChars: 0
},
{
feed: '{encodedQuery}',
marker: '$',
minChars: 1,
template: '<li data-id="{id}">{fullName}</li>'
},
{
feed: dataCallback,
marker: '#',
template: '<li data-id="{id}">{name} ({fullName})</li>'
}
],
on: {
instanceReady: function() {
// We have to stub ajax.load function.
CKEDITOR.ajax.load = function( query, callback ) {
var results = data.filter( function( item ) {
return item.name.indexOf( query ) === 0;
} );
setTimeout( function() {
callback( JSON.stringify( results ) );
}, Math.random() * 500 );
}
}
}
} );
function dataCallback( opts, callback ) {
setTimeout( function() {
callback(
data.filter( function( item ) {
return item.name.indexOf( opts.query ) === 0;
} )
);
}, Math.random() * 500 );
}
} )();
</script>
</body>
</html>