Nuevos componentes en CKEditor para gestionar macros y enlaces a páginas de wikis de manera más amigable

This commit is contained in:
Manuel Cillero 2019-01-27 13:49:09 +01:00
parent cb188df1ae
commit 164ab377d1
15 changed files with 3037 additions and 47 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,162 @@
<!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>
<head>
<meta charset="utf-8">
<title>Autocomplete Custom View &mdash; CKEditor Sample</title>
<script src="../../../ckeditor.js"></script>
<script src="utils.js"></script>
<link rel="stylesheet" href="../../../samples/css/samples.css">
<link href="../skins/moono/autocomplete.css" rel="stylesheet">
</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/ckeditor-dev/issues">I found a bug</a></li>
<li><a href="https://github.com/ckeditor/ckeditor-dev" 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>Autocomplete Custom View Demo</h1>
<p>This sample shows the progress of work on Autocomplete with custom View. Type &#8220; @ &#8221; (at least 2 characters) to start autocompletion.</p>
</div>
</div>
</div>
<div class="adjoined-bottom">
<div class="grid-container">
<div class="grid-width-100">
<div id="editor">
<h1>Apollo 11</h1>
<figure class="image easyimage">
<img alt="Saturn V carrying Apollo 11" src="../../../samples/img/logo.png">
</figure>
<p><strong>Apollo 11</strong> was the spaceflight that landed the first humans, Americans <a href="http://en.wikipedia.org/wiki/Neil_Armstrong">Neil Armstrong</a> and <a href="http://en.wikipedia.org/wiki/Buzz_Aldrin">Buzz Aldrin</a>, on the Moon on July 20, 1969, at 20:18 UTC. Armstrong became the first to step onto the lunar surface 6 hours later on July 21 at 02:56 UTC.</p>
<figure class="easyimage easyimage-side">
<img alt="Saturn V carrying Apollo 11" src="../../image2/samples/assets/image1.jpg">
<figcaption>Saturn V carrying Apollo 11</figcaption>
</figure>
<p>Armstrong spent about <s>three and a half</s> two and a half hours outside the spacecraft, Aldrin slightly less; and together they collected 47.5 pounds (21.5&nbsp;kg) of lunar material for return to Earth. A third member of the mission, <a href="http://en.wikipedia.org/wiki/Michael_Collins_(astronaut)">Michael Collins</a>, piloted the <a href="http://en.wikipedia.org/wiki/Apollo_Command/Service_Module">command</a> spacecraft alone in lunar orbit until Armstrong and Aldrin returned to it for the trip back to Earth.</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() {
// For simplicity we define the plugin in the sample, but normally
// it would be extracted to a separate file.
CKEDITOR.plugins.add( 'customautocomplete', {
requires: 'autocomplete',
onLoad: function() {
var View = CKEDITOR.plugins.autocomplete.view,
Autocomplete = CKEDITOR.plugins.autocomplete;
function CustomView( editor ) {
// Call the parent class constructor.
View.call( this, editor );
}
// Inherit the view methods.
CustomView.prototype = CKEDITOR.tools.prototypedCopy( View.prototype );
// Change the positioning of the panel, so it is stretched
// to 100% of the editor container width and is positioned
// according to the editor container.
CustomView.prototype.updatePosition = function( range ) {
var caretRect = this.getViewPosition( range ),
container = this.editor.container;
this.setPosition( {
// Position the panel according to the editor container.
left: container.$.offsetLeft,
top: caretRect.top,
bottom: caretRect.bottom
} );
// Stretch the panel to 100% of the editor container width.
this.element.setStyle( 'width', container.getSize( 'width' ) + 'px' );
};
function CustomAutocomplete( editor, textTestCallback, dataCallback ) {
// Call the parent class constructor.
Autocomplete.call( this, editor, textTestCallback, dataCallback );
}
// Inherit the autocomplete methods.
CustomAutocomplete.prototype = CKEDITOR.tools.prototypedCopy( Autocomplete.prototype );
CustomAutocomplete.prototype.getView = function() {
return new CustomView( this.editor );
}
// Expose the custom autocomplete so it can be used later.
CKEDITOR.plugins.customAutocomplete = CustomAutocomplete;
}
} );
var editor = CKEDITOR.replace( 'editor', {
height: 600,
extraPlugins: 'customautocomplete,autocomplete,textmatch,easyimage,sourcearea,toolbar,undo,wysiwygarea,basicstyles',
toolbar: [
{ name: 'document', items: [ 'Source', 'Undo', 'Redo' ] },
{ name: 'basicstyles', items: [ 'Bold', 'Italic', 'Strike' ] },
]
} );
editor.on( 'instanceReady', function() {
var prefix = '@',
minChars = 2,
requireSpaceAfter = true,
data = autocompleteUtils.generateData( CKEDITOR.dom.element.prototype, prefix );
// Use the custom autocomplete class.
new CKEDITOR.plugins.customAutocomplete(
editor,
autocompleteUtils.getTextTestCallback( prefix, minChars, requireSpaceAfter ),
autocompleteUtils.getAsyncDataCallback( data )
);
} );
} )();
</script>
</body>
</html>

View file

@ -0,0 +1,172 @@
<!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>
<head>
<meta charset="utf-8">
<title>Autocomplete Smileys &mdash; CKEditor Sample</title>
<script src="../../../ckeditor.js"></script>
<script src="utils.js"></script>
<link rel="stylesheet" href="../../../samples/css/samples.css">
<link href="../skins/moono/autocomplete.css" rel="stylesheet">
</head>
<body>
<style>
.adjoined-bottom:before {
height: 270px;
}
.cke_autocomplete_icon
{
vertical-align: middle;
}
</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/ckeditor-dev/issues">I found a bug</a></li>
<li><a href="https://github.com/ckeditor/ckeditor-dev" 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>Autocomplete Smileys Demo</h1>
<p>This sample shows the progress of work on Autocomplete with Smileys integration. Type &#8220; : &#8221; to start smileys autocompletion.</p>
</div>
</div>
</div>
<div class="adjoined-bottom">
<div class="grid-container">
<div class="grid-width-100">
<div id="editor">
<h1>Apollo 11</h1>
<figure class="image easyimage">
<img alt="Saturn V carrying Apollo 11" src="../../../samples/img/logo.png">
</figure>
<p><strong>Apollo 11</strong> was the spaceflight that landed the first humans, Americans <a href="http://en.wikipedia.org/wiki/Neil_Armstrong">Neil Armstrong</a> and <a href="http://en.wikipedia.org/wiki/Buzz_Aldrin">Buzz Aldrin</a>, on the Moon on July 20, 1969, at 20:18 UTC. Armstrong became the first to step onto the lunar surface 6 hours later on July 21 at 02:56 UTC.</p>
<figure class="easyimage easyimage-side">
<img alt="Saturn V carrying Apollo 11" src="../../image2/samples/assets/image1.jpg">
<figcaption>Saturn V carrying Apollo 11</figcaption>
</figure>
<p>Armstrong spent about <s>three and a half</s> two and a half hours outside the spacecraft, Aldrin slightly less; and together they collected 47.5 pounds (21.5&nbsp;kg) of lunar material for return to Earth. A third member of the mission, <a href="http://en.wikipedia.org/wiki/Michael_Collins_(astronaut)">Michael Collins</a>, piloted the <a href="http://en.wikipedia.org/wiki/Apollo_Command/Service_Module">command</a> spacecraft alone in lunar orbit until Armstrong and Aldrin returned to it for the trip back to Earth.</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() {
// For simplicity we define the plugin in the sample, but normally
// it would be extracted to a separate file.
CKEDITOR.plugins.add( 'smileyautocomplete', {
requires: 'autocomplete,textmatch,smiley',
onLoad: function() {
var that = this,
View = CKEDITOR.plugins.autocomplete.view,
Autocomplete = CKEDITOR.plugins.autocomplete;
function SmileyView( editor ) {
// Call the parent class constructor.
View.call( this, editor );
this.itemTemplate = new CKEDITOR.template(
'<li data-id="{id}"><img src="{src}" alt="{id}" class="cke_autocomplete_icon"> {name}</li>'
);
}
// Inherit the view methods.
SmileyView.prototype = CKEDITOR.tools.prototypedCopy( View.prototype );
function SmileyAutocomplete( editor ) {
var data = that.getData( editor );
// Call the parent class constructor.
Autocomplete.call(
this, editor,
autocompleteUtils.getTextTestCallback( ':', 0, false ),
autocompleteUtils.getSyncDataCallback( data )
);
}
// Inherit the autocomplete methods.
SmileyAutocomplete.prototype = CKEDITOR.tools.prototypedCopy( Autocomplete.prototype );
SmileyAutocomplete.prototype.getHtmlToInsert = function( item ) {
return '<img src=' + item.src + ' alt="' + item.id + '" />';
};
SmileyAutocomplete.prototype.getView = function() {
return new SmileyView( this.editor );
}
// Expose the smiley autocomplete so it can be used later.
CKEDITOR.plugins.smileyAutocomplete = SmileyAutocomplete;
},
getData: function( editor ) {
var descriptions = editor.config.smiley_descriptions,
images = editor.config.smiley_images,
path = editor.config.smiley_path,
data = [];
for ( var i = 0; i < descriptions.length; ++i ) {
data.push( {
id: descriptions[ i ],
name: ':' + descriptions[ i ],
src: CKEDITOR.tools.htmlEncode( path + images[ i ] )
} );
}
return data;
}
} );
var editor = CKEDITOR.replace( 'editor', {
height: 600,
extraPlugins: 'smileyautocomplete,autocomplete,textmatch,easyimage,sourcearea,toolbar,undo,wysiwygarea,basicstyles',
toolbar: [
{ name: 'document', items: [ 'Source', 'Undo', 'Redo' ] },
{ name: 'basicstyles', items: [ 'Bold', 'Italic', 'Strike' ] },
]
} );
editor.on( 'instanceReady', function() {
// Use the smiley autocomplete class.
new CKEDITOR.plugins.smileyAutocomplete( editor );
} );
} )();
</script>
</body>
</html>

View file

@ -0,0 +1,97 @@
/**
* @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/* exported autocompleteUtils */
'use strict';
var autocompleteUtils = {
generateData: function( object, prefix ) {
return Object.keys( object ).sort().map( function( prop, index ) {
return {
id: index,
name: prefix + prop
};
} );
},
getAsyncDataCallback: function( data ) {
return function( query, range, callback ) {
setTimeout( function() {
callback(
data.filter( function( item ) {
return item.name.indexOf( query ) === 0;
} )
);
}, Math.random() * 500 );
};
},
getSyncDataCallback: function( data ) {
return function( query, range, callback ) {
callback(
data.filter( function( item ) {
return item.name.indexOf( query ) === 0;
} )
);
};
},
getTextTestCallback: function( prefix, minChars, requireSpaceAfter ) {
var matchPattern = createPattern();
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 ),
right = text.slice( offset ),
match = left.match( matchPattern );
if ( !match ) {
return null;
}
if ( requireSpaceAfter ) {
// Require space (or end of text) after the caret.
if ( right && !right.match( /^\s/ ) ) {
return null;
}
}
return { start: match.index, end: offset };
}
function createPattern() {
var pattern = prefix + '\\w';
if ( minChars ) {
pattern += '{' + minChars + ',}';
} else {
pattern += '*';
}
pattern += '$';
return new RegExp( pattern );
}
}
};

View file

@ -0,0 +1,38 @@
/*
Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
.cke_autocomplete_panel
{
position: absolute;
display: none;
box-sizing: border-box;
width: 200px;
max-height: 300px;
overflow: auto;
padding: 0;
margin: 0;
list-style: none;
background: #FFF;
border: 1px solid #b6b6b6;
border-bottom-color: #999;
border-radius: 3px;
font: 12px Arial, Helvetica, Tahoma, Verdana, Sans-Serif;
}
.cke_autocomplete_opened
{
display: block;
}
.cke_autocomplete_panel > li
{
padding: 5px;
}
.cke_autocomplete_panel > li:hover
{
cursor: pointer;
}
.cke_autocomplete_selected, .cke_autocomplete_panel > li:hover
{
background-color: #EFF0EF;
}

View file

@ -0,0 +1,38 @@
'use strict';
CKEDITOR.dialog.add( 'redmineMacro', function( editor ) {
var lang = editor.lang.redmine,
generalLabel = editor.lang.common.generalTab,
validNameRegex = /^[^\[\]<>]+$/;
return {
title: lang.editMacro,
minWidth: 300,
minHeight: 80,
contents: [
{
id: 'info',
label: generalLabel,
title: generalLabel,
elements: [
{
id: 'name',
type: 'text',
style: 'width: 100%;',
label: lang.editMacroProperties,
'default': '',
required: true,
validate: CKEDITOR.dialog.validate.regex( validNameRegex, lang.invalidMacro ),
setup: function( widget ) {
this.setValue( widget.data.name );
},
commit: function( widget ) {
widget.setData( 'name', this.getValue() );
}
}
]
}
]
};
} );

View file

@ -0,0 +1,44 @@
'use strict';
CKEDITOR.dialog.add( 'redmineWikilink', function( editor ) {
var lang = editor.lang.redmine,
generalLabel = editor.lang.common.generalTab,
validNameRegex = /^[^\[\]<>]+$/;
return {
title: lang.wikiPageLink,
minWidth: 300,
minHeight: 80,
contents: [
{
id: 'info',
label: generalLabel,
title: generalLabel,
elements: [
{
id: 'name',
type: 'text',
style: 'width: 100%;',
label: lang.wikiPage,
'default': '',
required: true,
validate: CKEDITOR.dialog.validate.regex( validNameRegex, lang.invalidWikiPageLink ),
setup: function( widget ) {
this.setValue( widget.data.name );
},
commit: function( widget ) {
widget.setData( 'name', this.getValue() );
}
},
{
type: 'html',
html: '- ' + lang.help_line1 + '<br />'
+ '- ' + lang.help_line2 + '<br />'
+ '- ' + lang.help_line3
}
]
}
]
};
} );

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

View file

@ -0,0 +1,32 @@

CKEDITOR.plugins.setLang( 'redmine', 'en', {
// Dialogs.
wikiPageLink: 'Link to WikiPage',
invalidWikiPageLink: 'The link can not be empty and can not contain any of following characters: [, ], <, >',
help_line1: 'Also WikiPage#Anchor; or',
help_line2: 'WikiPage|TextLink or WikiPage#Anchor|TextLink; or',
help_line3: 'link to pages of another project using project:WikiPage.',
editMacro: 'Edit Macro',
editMacroProperties: 'Edit Macro Properties',
invalidMacro: 'It can not be empty and can not contain any of following characters: [, ], <, >',
// Plugin.
pathName: 'redmineWidget',
// Autocomplete WikiLinks.
wikiPage: 'WikiPage',
anchor: 'Anchor',
textLink: 'TextLink',
project: 'project',
wikipage_linkTo: 'Link to ',
// Autocomplete Macros.
toc: 'Table of contents.',
child_pages: 'List of child pages.',
child_pages_depth: 'List of child pages with 2 levels.',
include_page: 'Page',
include: 'Include a wiki page.',
include_project: 'project',
include_from_project: 'Include a page of a specific project wiki.'
});

View file

@ -0,0 +1,32 @@

CKEDITOR.plugins.setLang( 'redmine', 'es', {
// Dialogs.
wikiPageLink: 'Enlace a Página del Wiki',
invalidWikiPageLink: 'El enlace no puede estar vacío y no puede contener ninguno de los siguientes caracteres: [, ], <, >',
help_line1: 'También Página#Referencia; o',
help_line2: 'Página|TextoEnlace o Página#Referencia|TextoEnlace; o',
help_line3: 'enlazar a páginas de otro proyecto usando proyecto:Página.',
editMacro: 'Editar Macro',
editMacroProperties: 'Editar Propiedades de la Macro',
invalidMacro: 'No puede estar vacío y no puede contener ninguno de los siguientes caracteres: [, ], <, >',
// Plugin.
pathName: 'redmineWidget',
// Autocomplete WikiLinks.
wikiPage: 'Página',
anchor: 'Referencia',
textLink: 'TextoEnlace',
project: 'proyecto',
wikipage_linkTo: 'Enlace a ',
// Autocomplete Macros.
toc: 'Tabla de contenidos.',
child_pages: 'Lista de páginas hijas.',
child_pages_depth: 'Lista de páginas hijas con 2 niveles.',
include_page: 'Página',
include: 'Incluye una página del wiki.',
include_project: 'proyecto',
include_from_project: 'Incluye una página del wiki de otro proyecto.'
});

View file

@ -1,59 +1,322 @@
'use strict';
(function(){
function preservedPattern(i) {
/*
function preservedPattern( i ) {
return "____preserved_" + i + "____";
}
function wrapConversion(f) {
return function(data) {
function wrapConversion( f ) {
return function( data ) {
var preserved = [];
// preserve Redmine macro
data = data.replace(/\{\{.*?\}\}/g, function(match) {
preserved.push(decodeEntities(match));
return preservedPattern(preserved.length);
// Preserve Redmine macro.
data = data.replace( /\{\{.*?\}\}/g, function( match ) {
preserved.push( decodeEntities( match ) );
return preservedPattern( preserved.length );
});
// convert
// Convert.
arguments[0] = data;
data = f.apply(this, arguments);
data = f.apply( this, arguments );
// restore
for (var i = 0; i < preserved.length; i++) {
data = data.replace(preservedPattern(i + 1), preserved[i]);
// Restore.
for ( var i = 0; i < preserved.length; i++ ) {
data = data.replace( preservedPattern( i + 1 ), preserved[i] );
}
return data;
};
}
var element = document.createElement('div');
function decodeEntities(html) {
*/
var element = document.createElement( 'div' );
function decodeEntities( html ) {
element.innerHTML = html;
html = element.textContent;
element.textContent = '';
return html;
}
function onText(text, node) {
return (node.parent.name == "a") ?
text : text.replace(/(^|\s)https?:\/\/\S*/g, decodeEntities);
function onText( text, node ) {
return ( node.parent.name == "a" ) ? text : text.replace( /(^|\s)https?:\/\/\S*/g, decodeEntities );
}
CKEDITOR.plugins.add('redmine', {
afterInit: function(editor) {
// Register the plugin in the editor.
CKEDITOR.plugins.add( 'redmine', {
requires: 'widget,dialog,autocomplete,textmatch',
lang: 'en,es', // %REMOVE_LINE_CORE%
icons: 'wikipage', // %REMOVE_LINE_CORE%
hidpi: true, // %REMOVE_LINE_CORE%
onLoad: function() {
// Register styles for placeholder widget frame.
CKEDITOR.addCss( '.cke_placeholder{padding:0 4px;margin:0 2px;background-color:#ff0}' );
},
init: function( editor ) {
var lang = editor.lang.redmine;
// Register Wikilink Widget.
CKEDITOR.dialog.add( 'redmineWikilink', this.path + 'dialogs/redmineWikilink.js' );
editor.widgets.add( 'redmineWikilink', {
dialog: 'redmineWikilink',
pathName: lang.pathName,
template: '<span class="cke_placeholder">[[]]</span>',
downcast: function() {
return new CKEDITOR.htmlParser.text( '[[' + this.data.name + ']]' );
},
init: function() {
this.setData( 'name', this.element.getText().slice( 2, -2 ) );
},
data: function() {
this.element.setText( '[[' + this.data.name + ']]' );
},
getLabel: function() {
return this.editor.lang.widget.label.replace( /%1/, this.data.name + ' ' + this.pathName );
}
} );
editor.ui.addButton && editor.ui.addButton( 'LinkWikiPage', {
label: lang.wikiPageLink,
command: 'redmineWikilink',
toolbar: 'insert,5',
icon: 'wikipage'
} );
// Register Macro Widget.
CKEDITOR.dialog.add( 'redmineMacro', this.path + 'dialogs/redmineMacro.js' );
editor.widgets.add( 'redmineMacro', {
dialog: 'redmineMacro',
pathName: lang.pathName,
template: '<span class="cke_placeholder">{{}}</span>',
downcast: function() {
return new CKEDITOR.htmlParser.text( '{{' + this.data.name + '}}' );
},
init: function() {
this.setData( 'name', this.element.getText().slice( 2, -2 ) );
},
data: function() {
this.element.setText( '{{' + this.data.name + '}}' );
},
getLabel: function() {
return this.editor.lang.widget.label.replace( /%1/, this.data.name + ' ' + this.pathName );
}
} );
editor.on( 'instanceReady', function() {
var config = {};
// Called when the user types in the editor or moves the caret.
// The range represents the caret position.
function textTestCallback( range ) {
// You do not want to autocomplete a non-empty selection.
if ( !range.collapsed ) {
return null;
}
// Use the text match plugin which does the tricky job of performing
// a text search in the DOM. The "matchCallback" function should return
// a matching fragment of the text.
return CKEDITOR.plugins.textMatch.match( range, matchCallback );
}
// Returns the position of the matching text.
// It matches a word starting from '{{' or the '#' character
// up to the caret position.
function matchCallback( text, offset ) {
var match;
// Will look for a '[[' characters followed by a wiki page name.
match = text.slice( 0, offset ).match( /\[{2}([A-z]|\])*$/ );
if ( match ) {
return {
start: match.index,
end: offset
};
}
// Will look for a '{{' characters followed by a macro name.
match = text.slice( 0, offset ).match( /\{{2}([A-z]|\})*$/ );
if ( match ) {
return {
start: match.index,
end: offset
};
}
/* Will look for a '#' character followed by a issue number.
match = text.slice( 0, offset ).match( /#\d*$/ );
if ( match ) {
return {
start: match.index,
end: offset
};
} */
return null;
}
config.textTestCallback = textTestCallback;
// WikiPage links sugestions.
var itemsWikiPageArray = [
{
id: 1,
title: '[[' + lang.wikiPage + ']]',
description: lang.wikipage_linkTo + lang.wikiPage + '.'
},
{
id: 2,
title: '[[' + lang.wikiPage + '#' + lang.anchor + ']]',
description: lang.wikipage_linkTo + lang.wikiPage + '#' + lang.anchor + '.'
},
{
id: 3,
title: '[[' + lang.wikiPage + '|' + lang.textLink + ']]',
description: lang.wikipage_linkTo + lang.wikiPage + '|' + lang.textLink + '.'
},
{
id: 4,
title: '[[' + lang.project + ':' + lang.wikiPage + ']]',
description: lang.wikipage_linkTo + lang.project + ':' + lang.wikiPage + '.'
}
];
// Macro sugestions.
var itemsMacroArray = [
{
id: 1,
title: '{{toc}}',
description: lang.toc
},
{
id: 2,
title: '{{child_pages}}',
description: lang.child_pages
},
{
id: 3,
title: '{{child_pages(depth=2)}}',
description: lang.child_pages_depth
},
{
id: 4,
title: '{{include(' + lang.include_page + ')}}',
description: lang.include
},
{
id: 5,
title: '{{include(' + lang.include_project + ':' + lang.include_page + ')}}',
description: lang.include_from_project
}
];
// Returns (through its callback) the suggestions for the current query.
function dataCallback( matchInfo, callback ) {
var itemsArray;
if (matchInfo.query.substring( 0, 2 ) == '[[' ) {
itemsArray = itemsWikiPageArray;
}
else if (matchInfo.query.substring( 0, 2 ) == '{{' ) {
itemsArray = itemsMacroArray;
}
var suggestions = itemsArray.filter( function( item ) {
return item.title.indexOf( matchInfo.query.toLowerCase() ) == 0;
});
callback( suggestions );
/* Remove the '#' tag.
var query = matchInfo.query.substring( 1 );
// Simple search.
// Filter the entire items array so only the items that start
// with the query remain.
var suggestions = itemsArray.filter( function( item ) {
return String( item.id ).indexOf( query ) == 0;
} );
// Note: The callback function can also be executed asynchronously
// so dataCallback can do an XHR request or use any other asynchronous API.
callback( suggestions ); */
}
config.dataCallback = dataCallback;
// Define the templates of the autocomplete suggestions dropdown and output text.
config.itemTemplate = '<li data-id="{id}"><strong>{title}</strong><br />{description}</li>';
config.outputTemplate = "{title}";
// Attach autocomplete to the editor.
new CKEDITOR.plugins.autocomplete( editor, config );
} );
},
afterInit: function( editor ) {
var processor = editor.dataProcessor;
processor.toHtml = wrapConversion(processor.toHtml);
processor.toDataFormat = wrapConversion(processor.toDataFormat);
processor.htmlFilter.addRules({text: onText}, 11);
processor.dataFilter.addRules({text: onText}, 11);
}
});
processor.dataFilter.addRules( {
text: function( text, node ) {
var dtd = node.parent && CKEDITOR.dtd[ node.parent.name ];
CKEDITOR.on('dialogDefinition', function(e) {
if (e.data.name == 'table') {
var width = e.data.definition.getContents('info').get('txtWidth');
if ( dtd && !dtd.span )
return;
return text.replace( /\[\[([^\[\]])+\]\]/g, function( match ) {
// Creating widget code.
var widgetWrapper = null,
innerElement = new CKEDITOR.htmlParser.element( 'span', {
'class': 'cke_placeholder'
} );
// Adds placeholder identifier as innertext.
innerElement.add( new CKEDITOR.htmlParser.text( match ) );
widgetWrapper = editor.widgets.wrapElement( innerElement, 'redmineWikilink' );
// Return outerhtml of widget wrapper so it will be placed as replacement.
return widgetWrapper.getOuterHtml();
} );
}
} );
processor.dataFilter.addRules( {
text: function( text, node ) {
var dtd = node.parent && CKEDITOR.dtd[ node.parent.name ];
if ( dtd && !dtd.span )
return;
return text.replace( /\{\{([^\{\}])+\}\}/g, function( match ) {
// Creating widget code.
var widgetWrapper = null,
innerElement = new CKEDITOR.htmlParser.element( 'span', {
'class': 'cke_placeholder'
} );
// Adds placeholder identifier as innertext.
innerElement.add( new CKEDITOR.htmlParser.text( match ) );
widgetWrapper = editor.widgets.wrapElement( innerElement, 'redmineMacro' );
// Return outerhtml of widget wrapper so it will be placed as replacement.
return widgetWrapper.getOuterHtml();
} );
}
} );
processor.htmlFilter.addRules( { text: onText }, 11 );
processor.dataFilter.addRules( { text: onText }, 11 );
/*
processor.toHtml = wrapConversion( processor.toHtml );
processor.toDataFormat = wrapConversion( processor.toDataFormat );
*/
}
} );
CKEDITOR.on( 'dialogDefinition', function( e ) {
if ( e.data.name == 'table' ) {
var width = e.data.definition.getContents( 'info' ).get( 'txtWidth' );
width['default'] = "100%";
}
});
} );
})();

View file

@ -0,0 +1,331 @@
/**
* @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.plugins.add( 'textmatch', {} );
/**
* A global namespace for methods exposed by the [Text Match](https://ckeditor.com/cke4/addon/textmatch) plugin.
*
* The most important function is {@link #match} which performs a text
* search in the DOM.
*
* @singleton
* @class
* @since 4.10.0
*/
CKEDITOR.plugins.textMatch = {};
/**
* Allows to search in the DOM for matching text using a callback which operates on strings instead of text nodes.
* Returns {@link CKEDITOR.dom.range} and the matching text.
*
* ```javascript
* var range = editor.getSelection().getRanges()[ 0 ];
*
* CKEDITOR.plugins.textMatch.match( range, function( text, offset ) {
* // Let's assume that text is 'Special thanks to #jo.' and offset is 21.
* // The offset "21" means that the caret is between '#jo' and '.'.
*
* // Get the text before the caret.
* var left = text.slice( 0, offset ),
* // Will look for a literal '#' character and at least two word characters.
* match = left.match( /#\w{2,}$/ );
*
* if ( !match ) {
* return null;
* }
*
* // The matching fragment is the '#jo', which can
* // be identified by the following offsets: { start: 18, end: 21 }.
* return { start: match.index, end: offset };
* } );
* ```
*
* @member CKEDITOR.plugins.textMatch
* @param {CKEDITOR.dom.range} range A collapsed range &mdash; the position from which the scanning starts.
* Usually the caret position.
* @param {Function} testCallback A callback executed to check if the text matches.
* @param {String} testCallback.text The full text to check.
* @param {Number} testCallback.rangeOffset An offset of the `range` in the `text` to be checked.
* @param {Object} [testCallback.return] The position of the matching fragment (`null` if nothing matches).
* @param {Number} testCallback.return.start The offset of the start of the matching fragment.
* @param {Number} testCallback.return.end The offset of the end of the matching fragment.
*
* @returns {Object/null} An object with information about the matching text or `null`.
* @returns {String} return.text The matching text.
* The text does not reflect the range offsets. The range could contain additional,
* browser-related characters like {@link CKEDITOR.dom.selection#FILLING_CHAR_SEQUENCE}.
* @returns {CKEDITOR.dom.range} return.range A range in the DOM for the text that matches.
*/
CKEDITOR.plugins.textMatch.match = function( range, callback ) {
var textAndOffset = CKEDITOR.plugins.textMatch.getTextAndOffset( range ),
fillingCharSequence = CKEDITOR.dom.selection.FILLING_CHAR_SEQUENCE,
fillingSequenceOffset = 0;
if ( !textAndOffset ) {
return;
}
// Remove filling char sequence for clean query (#2038).
if ( textAndOffset.text.indexOf( fillingCharSequence ) == 0 ) {
fillingSequenceOffset = fillingCharSequence.length;
textAndOffset.text = textAndOffset.text.replace( fillingCharSequence, '' );
textAndOffset.offset -= fillingSequenceOffset;
}
var result = callback( textAndOffset.text, textAndOffset.offset );
if ( !result ) {
return null;
}
return {
range: CKEDITOR.plugins.textMatch.getRangeInText( range, result.start, result.end + fillingSequenceOffset ),
text: textAndOffset.text.slice( result.start, result.end )
};
};
/**
* Returns a text (as a string) in which the DOM range is located (the function scans for adjacent text nodes)
* and the offset of the caret in that text.
*
* ## Examples
*
* * `{}` is the range position in the text node (it means that the text node is **not** split at that position).
* * `[]` is the range position in the element (it means that the text node is split at that position).
* * `.` is a separator for text nodes (it means that the text node is split at that position).
*
* Examples:
*
* ```
* Input: <p>he[]llo</p>
* Result: { text: 'hello', offset: 2 }
*
* Input: <p>he.llo{}</p>
* Result: { text: 'hello', offset: 5 }
*
* Input: <p>{}he.ll<i>o</i></p>
* Result: { text: 'hell', offset: 0 }
*
* Input: <p>he{}<i>ll</i>o</p>
* Result: { text: 'he', offset: 2 }
*
* Input: <p>he<i>ll</i>o.m{}y.friend</p>
* Result: { text: 'omyfriend', offset: 2 }
* ```
*
* @member CKEDITOR.plugins.textMatch
* @param {CKEDITOR.dom.range} range
* @returns {Object/null}
* @returns {String} return.text The text in which the DOM range is located.
* @returns {Number} return.offset An offset of the caret.
*/
CKEDITOR.plugins.textMatch.getTextAndOffset = function( range ) {
if ( !range.collapsed ) {
return null;
}
var text = '', offset = 0,
textNodes = CKEDITOR.plugins.textMatch.getAdjacentTextNodes( range ),
nodeReached = false,
elementIndex,
startContainerIsText = ( range.startContainer.type != CKEDITOR.NODE_ELEMENT );
if ( startContainerIsText ) {
// Determining element index in textNodes array.
elementIndex = indexOf( textNodes, function( current ) {
return range.startContainer.equals( current );
} );
} else {
// Based on range startOffset decreased by first text node index.
elementIndex = range.startOffset - ( textNodes[ 0 ] ? textNodes[ 0 ].getIndex() : 0 );
}
var max = textNodes.length;
for ( var i = 0; i < max; i += 1 ) {
var currentNode = textNodes[ i ];
text += currentNode.getText();
// We want to increase text offset only when startContainer is not reached.
if ( !nodeReached ) {
if ( startContainerIsText ) {
if ( i == elementIndex ) {
nodeReached = true;
offset += range.startOffset;
} else {
offset += currentNode.getText().length;
}
} else {
if ( i == elementIndex ) {
nodeReached = true;
}
// In below example there are three text nodes in p element and four possible offsets ( 0, 1, 2, 3 )
// We are going to increase offset while iteration:
// index 0 ==> 0
// index 1 ==> 3
// index 2 ==> 3 + 3
// index 3 ==> 3 + 3 + 2
// <p> foo bar ba </p>
// 0^^^1^^^2^^3
if ( i > 0 ) {
offset += textNodes[ i - 1 ].getText().length;
}
// If element index at last element we also want to increase offset.
if ( max == elementIndex && i + 1 == max ) {
offset += currentNode.getText().length;
}
}
}
}
return {
text: text,
offset: offset
};
};
/**
* Transforms the `start` and `end` offsets in the text generated by the {@link #getTextAndOffset}
* method into a DOM range.
*
* ## Examples
*
* * `{}` is the range position in the text node (it means that the text node is **not** split at that position).
* * `.` is a separator for text nodes (it means that the text node is split at that position).
*
* Examples:
*
* ```
* Input: <p>f{}oo.bar</p>, 0, 3
* Result: <p>{foo}.bar</p>
*
* Input: <p>f{}oo.bar</p>, 1, 5
* Result: <p>f{oo.ba}r</p>
* ```
*
* @member CKEDITOR.plugins.textMatch
* @param {CKEDITOR.dom.range} range
* @param {Number} start A start offset.
* @param {Number} end An end offset.
* @returns {CKEDITOR.dom.range} Transformed range.
*/
CKEDITOR.plugins.textMatch.getRangeInText = function( range, start, end ) {
var resultRange = new CKEDITOR.dom.range( range.root ),
elements = CKEDITOR.plugins.textMatch.getAdjacentTextNodes( range ),
startData = findElementAtOffset( elements, start ),
endData = findElementAtOffset( elements, end );
resultRange.setStart( startData.element, startData.offset );
resultRange.setEnd( endData.element, endData.offset );
return resultRange;
};
/**
* Creates a collection of adjacent text nodes which are between DOM elements, starting from the given range.
* This function works only for collapsed ranges.
*
* ## Examples
*
* * `{}` is the range position in the text node (it means that the text node is **not** split at that position).
* * `.` is a separator for text nodes (it means that the text node is split at that position).
*
* Examples:
*
* ```
* Input: <p>he.llo{}</p>
* Result: [ 'he', 'llo' ]
*
* Input: <p>{}he.ll<i>o</i></p>
* Result: [ 'he', 'll' ]
*
* Input: <p>he{}<i>ll</i>o.</p>
* Result: [ 'he' ]
*
* Input: <p>he<i>ll</i>{}o.my.friend</p>
* Result: [ 'o', 'my', 'friend' ]
* ```
*
* @member CKEDITOR.plugins.textMatch
* @param {CKEDITOR.dom.range} range
* @return {CKEDITOR.dom.text[]} An array of text nodes.
*/
CKEDITOR.plugins.textMatch.getAdjacentTextNodes = function( range ) {
if ( !range.collapsed ) {
throw new Error( 'Range must be collapsed.' ); // %REMOVE_LINE%
// Reachable in prod mode.
return []; // jshint ignore:line
}
var collection = [],
siblings,
elementIndex,
node, i;
if ( range.startContainer.type != CKEDITOR.NODE_ELEMENT ) {
siblings = range.startContainer.getParent().getChildren();
elementIndex = range.startContainer.getIndex();
} else {
siblings = range.startContainer.getChildren();
elementIndex = range.startOffset;
}
i = elementIndex;
while ( node = siblings.getItem( --i ) ) {
if ( node.type == CKEDITOR.NODE_TEXT ) {
collection.unshift( node );
} else {
break;
}
}
i = elementIndex;
while ( node = siblings.getItem( i++ ) ) {
if ( node.type == CKEDITOR.NODE_TEXT ) {
collection.push( node );
} else {
break;
}
}
return collection;
};
function findElementAtOffset( elements, offset ) {
var max = elements.length,
currentOffset = 0;
for ( var i = 0; i < max; i += 1 ) {
var current = elements[ i ];
if ( offset >= currentOffset && currentOffset + current.getText().length >= offset ) {
return {
element: current,
offset: offset - currentOffset
};
}
currentOffset += current.getText().length;
}
return null;
}
function indexOf( arr, checker ) {
for ( var i = 0; i < arr.length; i++ ) {
if ( checker( arr[ i ] ) ) {
return i;
}
}
return -1;
}
} )();

View file

@ -0,0 +1,295 @@
/**
* @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.plugins.add( 'textwatcher', {} );
/**
* API exposed by the [Text Watcher](https://ckeditor.com/cke4/addon/textwatcher) plugin.
*
* Class implementing the text watcher &mdash; a base for features like
* autocomplete. It fires the {@link #matched} and {@link #unmatched} events
* based on changes in the text and the position of the caret in the editor.
*
* To check whether the text matches some criteria, the text watcher uses
* a callback function which should return the matching text and a {@link CKEDITOR.dom.range}
* for that text.
*
* Since the text watcher works on the DOM where searching for text
* is pretty complicated, it is usually recommended to use the {@link CKEDITOR.plugins.textMatch#match}
* function.
*
* Example:
*
* ```javascript
* function textTestCallback( range ) {
* // You do not want to autocomplete a non-empty selection.
* if ( !range.collapsed ) {
* return null;
* }
*
* // Use the text match plugin which does the tricky job of doing
* // a text search in the DOM. The matchCallback function should return
* // a matching fragment of the text.
* return CKEDITOR.plugins.textMatch.match( range, matchCallback );
* }
*
* function matchCallback( text, offset ) {
* // Get the text before the caret.
* var left = text.slice( 0, offset ),
* // Will look for an '@' character followed by word characters.
* match = left.match( /@\w*$/ );
*
* if ( !match ) {
* return null;
* }
* return { start: match.index, end: offset };
* }
*
* // Initialize the text watcher.
* var textWatcher = new CKEDITOR.plugins.textWatcher( editor, textTestCallback );
* // Start listening.
* textWatcher.attach();
*
* // Handle text matching.
* textWatcher.on( 'matched', function( evt ) {
* autocomplete.setQuery( evt.data.text );
* } );
* ```
*
* @class CKEDITOR.plugins.textWatcher
* @since 4.10.0
* @mixins CKEDITOR.event
* @constructor Creates the text watcher instance.
* @param {CKEDITOR.editor} editor The editor instance to watch in.
* @param {Function} callback Callback executed when the text watcher
* thinks that something might have changed.
* @param {Number} [throttle=0] Throttle interval, see {@link #throttle}.
* @param {CKEDITOR.dom.range} callback.range The range representing the caret position.
* @param {Object} [callback.return=null] Matching text data (`null` if nothing matches).
* @param {String} callback.return.text The matching text.
* @param {CKEDITOR.dom.range} callback.return.range A range in the DOM for the text that matches.
*/
function TextWatcher( editor, callback, throttle ) {
/**
* The editor instance which the text watcher watches.
*
* @readonly
* @property {CKEDITOR.editor}
*/
this.editor = editor;
/**
* The last matched text.
*
* @readonly
* @property {String}
*/
this.lastMatched = null;
/**
* Whether the next check should be ignored. See the {@link #consumeNext} method.
*
* @readonly
*/
this.ignoreNext = false;
/**
* The callback passed to the {@link CKEDITOR.plugins.textWatcher} constructor.
*
* @readonly
* @property {Function}
*/
this.callback = callback;
/**
* Keys that should be ignored by the {@link #check} method.
*
* @readonly
* @property {Number[]}
*/
this.ignoredKeys = [
16, // Shift
17, // Ctrl
18, // Alt
91, // Cmd
35, // End
36, // Home
37, // Left
38, // Up
39, // Right
40, // Down
33, // PageUp
34 // PageUp
];
/**
* Listeners registered by this text watcher.
*
* @private
*/
this._listeners = [];
/**
* Indicates throttle threshold mitigating text checks.
*
* Higher levels of the throttle threshold will create a delay for text watcher checks
* but also improve its performance.
*
* See the {@link CKEDITOR.tools#throttle throttle} feature for more information.
*
* @readonly
* @property {Number} [throttle=0]
*/
this.throttle = throttle || 0;
/**
* The {@link CKEDITOR.tools#throttle throttle buffer} used to mitigate text checks.
*
* @private
*/
this._buffer = CKEDITOR.tools.throttle( this.throttle, testTextMatch, this );
/**
* Event fired when the text is no longer matching.
*
* @event matched
* @param {Object} data The value returned by the {@link #callback}.
* @param {String} data.text
* @param {CKEDITOR.dom.range} data.range
*/
/**
* Event fired when the text stops matching.
*
* @event unmatched
*/
function testTextMatch( selectionRange ) {
var matched = this.callback( selectionRange );
if ( matched ) {
if ( matched.text == this.lastMatched ) {
return;
}
this.lastMatched = matched.text;
this.fire( 'matched', matched );
} else if ( this.lastMatched ) {
this.unmatch();
}
}
}
TextWatcher.prototype = {
/**
* Attaches the text watcher to the {@link #editor}.
*
* @chainable
*/
attach: function() {
var editor = this.editor;
this._listeners.push( editor.on( 'contentDom', onContentDom, this ) );
this._listeners.push( editor.on( 'blur', unmatch, this ) );
this._listeners.push( editor.on( 'beforeModeUnload', unmatch, this ) );
this._listeners.push( editor.on( 'setData', unmatch, this ) );
this._listeners.push( editor.on( 'afterCommandExec', unmatch, this ) );
// Attach if editor is already initialized.
if ( editor.editable() ) {
onContentDom.call( this );
}
return this;
function onContentDom() {
var editable = editor.editable();
this._listeners.push( editable.attachListener( editable, 'keyup', check, this ) );
}
// CKEditor's event system has a limitation that one function (in this case this.check)
// cannot be used as listener for the same event more than once. Hence, wrapper function.
function check( evt ) {
this.check( evt );
}
function unmatch() {
this.unmatch();
}
},
/**
* Triggers a text check. Fires the {@link #matched} and {@link #unmatched} events.
* The {@link #matched} event will not be fired twice in a row for the same text
* unless the text watcher is {@link #unmatch reset}.
*
* @param {CKEDITOR.dom.event/CKEDITOR.eventInfo} [evt]
*/
check: function( evt ) {
if ( this.ignoreNext ) {
this.ignoreNext = false;
return;
}
// Ignore control keys, so they don't trigger the check.
if ( evt && evt.name == 'keyup' && ( CKEDITOR.tools.array.indexOf( this.ignoredKeys, evt.data.getKey() ) != -1 ) ) {
return;
}
var sel = this.editor.getSelection();
if ( !sel ) {
return;
}
var selectionRange = sel.getRanges()[ 0 ];
if ( !selectionRange ) {
return;
}
this._buffer.input( selectionRange );
},
/**
* Ignores the next {@link #check}.
*
* @chainable
*/
consumeNext: function() {
this.ignoreNext = true;
return this;
},
/**
* Resets the state and fires the {@link #unmatched} event.
*
* @chainable
*/
unmatch: function() {
this.lastMatched = null;
this.fire( 'unmatched' );
return this;
},
/**
* Destroys the text watcher instance. The DOM event listeners will be cleaned up.
*/
destroy: function() {
CKEDITOR.tools.array.forEach( this._listeners, function( obj ) {
obj.removeListener();
} );
this._listeners = [];
}
};
CKEDITOR.event.implementOn( TextWatcher.prototype );
CKEDITOR.plugins.textWatcher = TextWatcher;
} )();

View file

@ -695,6 +695,10 @@ div.wiki ul.toc li {
min-width: auto;
padding-top: 1em;
}
#preview span.cke_widget_drag_handler_container {
display: none;
}
/**********************************************************/
/* 11. DOCUMENTS */