nmitchko's picture
Upload 77 files
1e4c25f
raw
history blame contribute delete
No virus
23.3 kB
/**
* @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
( function() {
'use strict';
var stylesLoaded = false,
arrTools = CKEDITOR.tools.array,
htmlEncode = CKEDITOR.tools.htmlEncodeAttr,
EmojiDropdown = CKEDITOR.tools.createClass( {
$: function( editor, plugin ) {
var lang = this.lang = editor.lang.emoji,
self = this,
ICON_SIZE = 21;
this.listeners = [];
this.plugin = plugin;
this.editor = editor;
this.groups = [
{
name: 'people',
sectionName: lang.groups.people,
svgId: 'cke4-icon-emoji-2',
position: {
x: -1 * ICON_SIZE,
y: 0
},
items: []
},
{
name: 'nature',
sectionName: lang.groups.nature,
svgId: 'cke4-icon-emoji-3',
position: {
x: -2 * ICON_SIZE,
y: 0
},
items: []
},
{
name: 'food',
sectionName: lang.groups.food,
svgId: 'cke4-icon-emoji-4',
position: {
x: -3 * ICON_SIZE,
y: 0
},
items: []
},
{
name: 'travel',
sectionName: lang.groups.travel,
svgId: 'cke4-icon-emoji-6',
position: {
x: -2 * ICON_SIZE,
y: -1 * ICON_SIZE
},
items: []
},
{
name: 'activities',
sectionName: lang.groups.activities,
svgId: 'cke4-icon-emoji-5',
position: {
x: -4 * ICON_SIZE,
y: 0
},
items: []
},
{
name: 'objects',
sectionName: lang.groups.objects,
svgId: 'cke4-icon-emoji-7',
position: {
x: 0,
y: -1 * ICON_SIZE
},
items: []
},
{
name: 'symbols',
sectionName: lang.groups.symbols,
svgId: 'cke4-icon-emoji-8',
position: {
x: -1 * ICON_SIZE,
y: -1 * ICON_SIZE
},
items: []
},
{
name: 'flags',
sectionName: lang.groups.flags,
svgId: 'cke4-icon-emoji-9',
position: {
x: -3 * ICON_SIZE,
y: -1 * ICON_SIZE
},
items: []
}
];
// Keeps html elements references to not find them again.
this.elements = {};
// Below line might be removable
editor.ui.addToolbarGroup( 'emoji', 'insert' );
// Name is responsible for icon name also.
editor.ui.add( 'emoji', CKEDITOR.UI_PANELBUTTON, {
label: 'emoji',
title: lang.title,
modes: { wysiwyg: 1 },
editorFocus: 0,
toolbar: 'insert',
panel: {
css: [
CKEDITOR.skin.getPath( 'editor' ),
plugin.path + 'skins/default.css'
],
attributes: {
role: 'listbox',
'aria-label': lang.title
},
markFirst: false
},
onBlock: function( panel, block ) {
var keys = block.keys,
rtl = editor.lang.dir === 'rtl';
keys[ rtl ? 37 : 39 ] = 'next'; // ARROW-RIGHT
keys[ 40 ] = 'next'; // ARROW-DOWN
keys[ 9 ] = 'next'; // TAB
keys[ rtl ? 39 : 37 ] = 'prev'; // ARROW-LEFT
keys[ 38 ] = 'prev'; // ARROW-UP
keys[ CKEDITOR.SHIFT + 9 ] = 'prev'; // SHIFT + TAB
keys[ 32 ] = 'click'; // SPACE
self.blockElement = block.element;
self.emojiList = self.editor._.emoji.list;
self.addEmojiToGroups();
block.element.getAscendant( 'html' ).addClass( 'cke_emoji' );
block.element.getDocument().appendStyleSheet( CKEDITOR.getUrl( CKEDITOR.basePath + 'contents.css' ) );
block.element.addClass( 'cke_emoji-panel_block' );
block.element.setHtml( self.createEmojiBlock() );
block.element.removeAttribute( 'title' );
panel.element.addClass( 'cke_emoji-panel' );
self.items = block._.getItems();
self.blockObject = block;
self.elements.emojiItems = block.element.find( '.cke_emoji-outer_emoji_block li > a' );
self.elements.sectionHeaders = block.element.find( '.cke_emoji-outer_emoji_block h2' );
self.elements.input = block.element.findOne( 'input' );
self.inputIndex = self.getItemIndex( self.items, self.elements.input );
self.elements.emojiBlock = block.element.findOne( '.cke_emoji-outer_emoji_block' );
self.elements.navigationItems = block.element.find( 'nav li' );
self.elements.statusIcon = block.element.findOne( '.cke_emoji-status_icon' );
self.elements.statusDescription = block.element.findOne( 'p.cke_emoji-status_description' );
self.elements.statusName = block.element.findOne( 'p.cke_emoji-status_full_name' );
self.elements.sections = block.element.find( 'section' );
self.registerListeners();
},
onOpen: self.openReset()
} );
},
proto: {
registerListeners: function() {
arrTools.forEach( this.listeners, function( item ) {
var root = this.blockElement,
selector = item.selector,
listener = item.listener,
event = item.event,
ctx = item.ctx || this;
arrTools.forEach( root.find( selector ).toArray(), function( node ) {
node.on( event, listener, ctx );
} );
}, this );
},
createEmojiBlock: function() {
var output = [];
// (#2607)
this.loadSVGNavigationIcons();
output.push( this.createGroupsNavigation() );
output.push( this.createSearchSection() );
output.push( this.createEmojiListBlock() );
output.push( this.createStatusBar() );
return '<div class="cke_emoji-inner_panel">' + output.join( '' ) + '</div>';
},
createGroupsNavigation: function() {
var itemTemplate,
items,
imgUrl,
useAttr;
if ( !this.editor.plugins.emoji.isSVGSupported() ) {
imgUrl = CKEDITOR.getUrl( this.plugin.path + 'assets/iconsall.png' );
itemTemplate = new CKEDITOR.template(
'<li class="cke_emoji-navigation_item" data-cke-emoji-group="{group}">' +
'<a href="#" draggable="false" _cke_focus="1" title="{name}">' +
'<span style="background-image:url(' + imgUrl + ');' +
'background-repeat:no-repeat;background-position:{positionX}px {positionY}px;"></span>' +
'</a></li>'
);
items = arrTools.reduce( this.groups, function( acc, item ) {
if ( !item.items.length ) {
return acc;
} else {
return acc + itemTemplate.output( {
group: htmlEncode( item.name ),
name: htmlEncode( item.sectionName ),
positionX: item.position.x,
positionY: item.position.y
} );
}
}, '' );
} else {
// iOS has problem with reading `href` attribute, that's why,
// its necessary to use `xlink:href` even its deprecated: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href
useAttr = CKEDITOR.env.safari ? 'xlink:href="#{svgId}"' : 'href="#{svgId}"';
itemTemplate = new CKEDITOR.template(
'<li class="cke_emoji-navigation_item" data-cke-emoji-group="{group}"><a href="#" title="{name}" draggable="false" _cke_focus="1">' +
'<svg viewBox="0 0 34 34" aria-labelledby="{svgId}-title">' +
'<title id="{svgId}-title">{name}</title><use ' + useAttr + '></use></svg></a></li>'
);
items = arrTools.reduce( this.groups, function( acc, item ) {
if ( !item.items.length ) {
return acc;
} else {
return acc + itemTemplate.output( {
group: htmlEncode( item.name ),
name: htmlEncode( item.sectionName ),
svgId: htmlEncode( item.svgId ),
translateX: item.translate && item.translate.x ? htmlEncode( item.translate.x ) : 0,
translateY: item.translate && item.translate.y ? htmlEncode( item.translate.y ) : 0
} );
}
}, '' );
}
this.listeners.push( {
selector: 'nav',
event: 'click',
listener: function( event ) {
var activeElement = event.data.getTarget().getAscendant( 'li', true );
if ( !activeElement ) {
return;
}
arrTools.forEach( this.elements.navigationItems.toArray(), function( node ) {
if ( node.equals( activeElement ) ) {
node.addClass( 'active' );
} else {
node.removeClass( 'active' );
}
} );
this.clearSearchAndMoveFocus( activeElement );
event.data.preventDefault();
}
} );
return '<nav aria-label="' + htmlEncode( this.lang.navigationLabel ) + '"><ul>' + items + '</ul></nav>';
},
createSearchSection: function() {
var self = this;
this.listeners.push( {
selector: 'input',
event: 'input',
listener: ( function() {
var buffer = CKEDITOR.tools.throttle( 200, self.filter, self );
return buffer.input;
} )()
} );
this.listeners.push( {
selector: 'input',
event: 'click',
listener: function() {
this.blockObject._.markItem( this.inputIndex );
}
} );
return '<label class="cke_emoji-search">' + this.getLoupeIcon() +
'<input placeholder="' + htmlEncode( this.lang.searchPlaceholder ) +
'" type="search" aria-label="' + htmlEncode( this.lang.searchLabel ) + '" role="search" _cke_focus="1"></label>';
},
createEmojiListBlock: function() {
var self = this;
this.listeners.push( {
selector: '.cke_emoji-outer_emoji_block',
event: 'scroll',
listener: ( function() {
var buffer = CKEDITOR.tools.throttle( 150, self.refreshNavigationStatus, self );
return buffer.input;
} )()
} );
this.listeners.push( {
selector: '.cke_emoji-outer_emoji_block',
event: 'click',
listener: function( event ) {
if ( event.data.getTarget().data( 'cke-emoji-name' ) ) {
this.editor.execCommand( 'insertEmoji', { emojiText: event.data.getTarget().data( 'cke-emoji-symbol' ) } );
}
}
} );
this.listeners.push( {
selector: '.cke_emoji-outer_emoji_block',
event: 'mouseover',
listener: function( event ) {
this.updateStatusbar( event.data.getTarget() );
}
} );
this.listeners.push( {
selector: '.cke_emoji-outer_emoji_block',
event: 'keyup',
listener: function() {
this.updateStatusbar( this.items.getItem( this.blockObject._.focusIndex ) );
}
} );
return '<div class="cke_emoji-outer_emoji_block">' + this.getEmojiSections() + '</div>';
},
createStatusBar: function() {
return '<div class="cke_emoji-status_bar">' +
'<div class="cke_emoji-status_icon"></div>' +
'<p class="cke_emoji-status_description"></p><p class="cke_emoji-status_full_name"></p>' +
'</div>';
},
getLoupeIcon: function() {
var loupePngUrl = CKEDITOR.getUrl( this.plugin.path + 'assets/iconsall.png' ),
useAttr;
if ( !this.editor.plugins.emoji.isSVGSupported() ) {
return '<span class="cke_emoji-search_loupe" aria-hidden="true" style="background-image:url(' + loupePngUrl + ');"></span>';
} else {
useAttr = CKEDITOR.env.safari ? 'xlink:href="#cke4-icon-emoji-10"' : 'href="#cke4-icon-emoji-10"';
return '<svg viewBox="0 0 34 34" role="img" aria-hidden="true" class="cke_emoji-search_loupe"><use ' + useAttr + '></use></svg>';
}
},
getEmojiSections: function() {
return arrTools.reduce( this.groups, function( acc, item ) {
// If group is empty skip it.
if ( !item.items.length ) {
return acc;
} else {
return acc + this.getEmojiSection( item );
}
}, '', this );
},
getEmojiSection: function( item ) {
var groupName = htmlEncode( item.name ),
sectionName = htmlEncode( item.sectionName ),
group = this.getEmojiListGroup( item.items );
return '<section data-cke-emoji-group="' + groupName + '" ><h2 id="' + groupName + '">' + sectionName + '</h2><ul>' + group + '</ul></section>';
},
getEmojiListGroup: function( items ) {
var emojiTpl = new CKEDITOR.template( '<li class="cke_emoji-item">' +
'<a draggable="false" data-cke-emoji-full-name="{id}" data-cke-emoji-name="{name}" data-cke-emoji-symbol="{symbol}" data-cke-emoji-group="{group}" ' +
'data-cke-emoji-keywords="{keywords}" title="{name}" href="#" _cke_focus="1">{symbol}</a>' +
'</li>' );
return arrTools.reduce(
items,
function( acc, item ) {
addEncodedName( item );
return acc + emojiTpl.output( {
symbol: htmlEncode( item.symbol ),
id: htmlEncode( item.id ),
name: item.name,
group: htmlEncode( item.group ),
keywords: htmlEncode( ( item.keywords || [] ).join( ',' ) )
} );
},
'',
this
);
},
filter: function( evt ) {
// Apply filters to emoji items in dropdown.
// Hiding not searched one.
// Can accept input event or string
var groups = {},
query = typeof evt === 'string' ? evt : evt.sender.getValue();
arrTools.forEach( this.elements.emojiItems.toArray(), function( element ) {
if ( isNameOrKeywords( query, element.data( 'cke-emoji-name' ), element.data( 'cke-emoji-keywords' ) ) || query === '' ) {
element.removeClass( 'hidden' );
element.getParent().removeClass( 'hidden' );
groups[ element.data( 'cke-emoji-group' ) ] = true;
} else {
element.addClass( 'hidden' );
element.getParent().addClass( 'hidden' );
}
function isNameOrKeywords( query, name, keywordsString ) {
var keywords,
i;
if ( name.indexOf( query ) !== -1 ) {
return true;
}
if ( keywordsString ) {
keywords = keywordsString.split( ',' );
for ( i = 0; i < keywords.length; i++ ) {
if ( keywords[ i ].indexOf( query ) !== -1 ) {
return true;
}
}
}
return false;
}
} );
arrTools.forEach( this.elements.sectionHeaders.toArray(), function( element ) {
if ( groups[ element.getId() ] ) {
element.getParent().removeClass( 'hidden' );
element.removeClass( 'hidden' );
} else {
element.addClass( 'hidden' );
element.getParent().addClass( 'hidden' );
}
} );
this.refreshNavigationStatus();
},
clearSearchInput: function() {
this.elements.input.setValue( '' );
this.filter( '' );
},
openReset: function() {
// Resets state of emoji dropdown.
// Clear filters, reset focus, etc.
var self = this,
firstCall;
return function() {
if ( !firstCall ) {
self.filter( '' );
firstCall = true;
}
self.elements.emojiBlock.$.scrollTop = 0;
self.refreshNavigationStatus();
// Clear search results:
self.clearSearchInput();
// Reset focus:
CKEDITOR.tools.setTimeout( function() {
self.elements.input.focus( true );
self.blockObject._.markItem( self.inputIndex );
}, 0, self );
// Remove statusbar icons:
self.clearStatusbar();
};
},
refreshNavigationStatus: function() {
var containerOffset = this.elements.emojiBlock.getClientRect().top,
section,
groupName;
section = arrTools.filter( this.elements.sections.toArray(), function( element ) {
var rect = element.getClientRect();
if ( !rect.height || element.findOne( 'h2' ).hasClass( 'hidden' ) ) {
return false;
}
return rect.height + rect.top > containerOffset;
} );
groupName = section.length ? section[ 0 ].data( 'cke-emoji-group' ) : false;
arrTools.forEach( this.elements.navigationItems.toArray(), function( node ) {
if ( node.data( 'cke-emoji-group' ) === groupName ) {
node.addClass( 'active' );
} else {
node.removeClass( 'active' );
}
} );
},
updateStatusbar: function( element ) {
if ( element.getName() !== 'a' || !element.hasAttribute( 'data-cke-emoji-name' ) ) {
return;
}
this.elements.statusIcon.setText( htmlEncode( element.getText() ) );
this.elements.statusDescription.setText( htmlEncode( element.data( 'cke-emoji-name' ) ) );
this.elements.statusName.setText( htmlEncode( element.data( 'cke-emoji-full-name' ) ) );
},
clearStatusbar: function() {
this.elements.statusIcon.setText( '' );
this.elements.statusDescription.setText( '' );
this.elements.statusName.setText( '' );
},
clearSearchAndMoveFocus: function( activeElement ) {
this.clearSearchInput();
this.moveFocus( activeElement.data( 'cke-emoji-group' ) );
},
moveFocus: function( groupName ) {
var firstSectionItem = this.blockElement.findOne( 'a[data-cke-emoji-group="' + htmlEncode( groupName ) + '"]' ),
itemIndex;
if ( !firstSectionItem ) {
return;
}
itemIndex = this.getItemIndex( this.items, firstSectionItem );
firstSectionItem.focus( true );
firstSectionItem.getAscendant( 'section' ).getFirst().scrollIntoView( true );
this.blockObject._.markItem( itemIndex );
},
getItemIndex: function( nodeList, item ) {
return arrTools.indexOf( nodeList.toArray(), function( element ) {
return element.equals( item );
} );
},
// To avoid CORS issues due to XML-based SVG icons, they should be loaded into the panel document.
// This method ensures that the icons are loaded locally.
loadSVGNavigationIcons: function() {
if ( !this.editor.plugins.emoji.isSVGSupported() ) {
return;
}
var doc = this.blockElement.getDocument();
CKEDITOR.ajax.load( CKEDITOR.getUrl( this.plugin.path + 'assets/iconsall.svg' ), function( html ) {
var container = new CKEDITOR.dom.element( 'div' );
container.addClass( 'cke_emoji-navigation_icons' );
container.setHtml( html );
doc.getBody().append( container );
} );
},
addEmojiToGroups: function() {
var groupObj = {};
arrTools.forEach( this.groups, function( group ) {
groupObj[ group.name ] = group.items;
}, this );
arrTools.forEach( this.emojiList, function( emojiObj ) {
groupObj[ emojiObj.group ].push( emojiObj );
}, this );
}
}
} );
CKEDITOR.plugins.add( 'emoji', {
requires: 'autocomplete,textmatch,ajax,panelbutton,floatpanel',
lang: 'cs,da,de,de-ch,el,en,en-au,et,fa,fr,gl,hr,hu,it,nl,pl,pt-br,sk,sr,sr-latn,sv,tr,uk,zh,zh-cn', // %REMOVE_LINE_CORE%
icons: 'emojipanel',
hidpi: true,
isSupportedEnvironment: function() {
return !CKEDITOR.env.ie || CKEDITOR.env.version >= 11;
},
beforeInit: function() {
if ( !this.isSupportedEnvironment() ) {
return;
}
if ( !stylesLoaded ) {
CKEDITOR.document.appendStyleSheet( this.path + 'skins/default.css' );
stylesLoaded = true;
}
},
init: function( editor ) {
if ( !this.isSupportedEnvironment() ) {
return;
}
var emojiListUrl = editor.config.emoji_emojiListUrl || 'plugins/emoji/emoji.json',
arrTools = CKEDITOR.tools.array;
CKEDITOR.ajax.load( CKEDITOR.getUrl( emojiListUrl ), function( data ) {
if ( data === null ) {
return;
}
if ( editor._.emoji === undefined ) {
editor._.emoji = {};
}
if ( editor._.emoji.list === undefined ) {
editor._.emoji.list = JSON.parse( data );
}
var emojiList = editor._.emoji.list,
charactersToStart = editor.config.emoji_minChars === undefined ? 2 : editor.config.emoji_minChars;
if ( editor.status !== 'ready' ) {
editor.once( 'instanceReady', initPlugin );
} else {
initPlugin();
}
// HELPER FUNCTIONS:
function initPlugin() {
editor._.emoji.autocomplete = new CKEDITOR.plugins.autocomplete( editor, {
textTestCallback: getTextTestCallback(),
dataCallback: dataCallback,
itemTemplate: '<li data-id="{id}" class="cke_emoji-suggestion_item"><span>{symbol}</span> {name}</li>',
outputTemplate: '{symbol}'
} );
}
function getTextTestCallback() {
return function( range ) {
if ( !range.collapsed ) {
return null;
}
return CKEDITOR.plugins.textMatch.match( range, matchCallback );
};
}
function matchCallback( text, offset ) {
var left = text.slice( 0, offset ),
// Emoji should be started with space or newline, but space shouldn't leak to output, hence it is in non captured group (#2195).
match = left.match( new RegExp( '(?:\\s\|^)(:\\S{' + charactersToStart + '}\\S*)$' ) );
if ( !match ) {
return null;
}
// In case of space preceding colon we need to return the last index (#2394) of capturing group.
return { start: left.lastIndexOf( match[ 1 ] ), end: offset };
}
function dataCallback( matchInfo, callback ) {
var emojiName = matchInfo.query.substr( 1 ).toLowerCase(),
data = arrTools.filter( emojiList, function( item ) {
// Comparing lowercase strings, because emoji should be case insensitive (#2167).
return item.id.toLowerCase().indexOf( emojiName ) !== -1;
} ).sort( function( a, b ) {
var aStartsWithEmojiName = !a.id.substr( 1 ).indexOf( emojiName ),
bStartsWithEmojiName = !b.id.substr( 1 ).indexOf( emojiName );
if ( aStartsWithEmojiName != bStartsWithEmojiName ) {
return aStartsWithEmojiName ? -1 : 1;
} else {
return a.id > b.id ? 1 : -1;
}
} );
data = arrTools.map( data, addEncodedName );
callback( data );
}
} );
editor.addCommand( 'insertEmoji', {
exec: function( editor, data ) {
editor.insertHtml( data.emojiText );
}
} );
if ( editor.plugins.toolbar ) {
new EmojiDropdown( editor, this );
}
},
isSVGSupported: function() {
return !CKEDITOR.env.ie || CKEDITOR.env.edge;
}
} );
function addEncodedName( item ) {
if ( !item.name ) {
item.name = htmlEncode( item.id.replace( /::.*$/, ':' ).replace( /^:|:$/g, '' ) );
}
return item;
}
} )();
/**
* A number that defines how many characters are required to start displaying emoji's autocomplete suggestion box.
* Delimiter `:`, which activates the emoji suggestion box, is not included in this value.
*
* ```js
* editor.emoji_minChars = 0; // Emoji suggestion box appears after typing ':'.
* ```
*
* @since 4.10.0
* @cfg {Number} [emoji_minChars=2]
* @member CKEDITOR.config
*/
/**
* Address of the JSON file containing the emoji list. The file is downloaded through the {@link CKEDITOR.ajax#load} method
* and the URL address is processed by {@link CKEDITOR#getUrl}.
* Emoji list has to be an array of objects with the `id` and `symbol` properties. These keys represent the text to match and the
* UTF symbol for its replacement.
* An emoji has to start with the `:` (colon) symbol.
*
* ```json
* [
* {
* "id": ":grinning_face:",
* "symbol":"😀"
* },
* {
* "id": ":bug:",
* "symbol":"🐛"
* },
* {
* "id": ":star:",
* "symbol":"⭐"
* }
* ]
* ```
*
* ```js
* editor.emoji_emojiListUrl = 'https://my.custom.domain/ckeditor/emoji.json';
* ```
*
* @since 4.10.0
* @cfg {String} [emoji_emojiListUrl='plugins/emoji/emoji.json']
* @member CKEDITOR.config
*/