Add custom autocomplete to react-ace EUI Kibana - javascript

I'm trying to add custom autocomplete for the editor inside the Kibana EUI framework.
Basically, I have this code:
<EuiCodeEditor
mode="yaml"
theme="github"
width="100%"
height="100%"
className="ace-tm"
value={configurationText}
onChange={(e) => onChangeEventText(e)}
setOptions={{
fontSize: '14px',
enableBasicAutocompletion: true,
enableSnippets: true,
enableLiveAutocompletion: true
}}
onBlur={() => {
console.log('blur');
}} // eslint-disable-line no-console
aria-label="Code Editor"
onLoad = {editor => {
console.log("EDITOR");
console.log(editor);
console.log(editor.completers);
// editor.completers = [staticWordCompleter];
/* var mode = editor.getMode();
mode.getCompletions = (state, session, pos, prefix) => {
return [];
} ;
editor.setMode(mode);
editor.completers = [staticWordCompleter];*/
}}
/>
And as you can see inside the onLoad I'm getting the editor pointer in order to add the custom autocomplete, but is not working. The ace editor documentation is not so good for the react-ace inside the Eui framework, so any help would be appreciated.
EDIT1
Resolved as you can read inside the answer, now I'm facing another problem: the autocomplete suggest either custom and keyword inside the document. I need only my custom suggestion. How can I do that?

Just resolved, I added this import:
import 'brace/ext/language_tools';
And wrote my onLoad as follow:
onLoad={editor => {
var mode = editor.getSession().getMode();
mode.getCompletions = (state, session, pos, prefix, callback) => {
var completions = [];
["example1", "example2"].forEach(function (w) {
completions.push({
value: w,
meta: "my completion",
snippet: `#{${w || ""}}`,
caption: w || ""
});
});
return completions;
}
editor.getSession().setMode(mode);
console.log("EDITOR");
console.log(editor.getSession().getMode().getCompletions());
}}
Now I'm facing another problem and I'm updating the question

Related

Error returning value when editing message

I've been thinking about how to do this for days and if you could help me.
I expose you, I have followed the CKEditor 5 tutorial to the point of including the mentions, this is where my problem begins.
Following the tutorial we come to the part of the output of the mention, this as they do in the tutorial I have transformed it from <span> to <a> together with its class, its URL and its data. Well the editor shows it fine until you want to edit the post.
That is, imagine this message:
Hello world I am the first code of #undercover
Well when I include it in the database everything is correct, but when we return that same message to the editor it becomes:
Hello world I am the first code of #undercover
Investigating and as my Javascript is quite low I have been trying things.
The conversion. I've tried but there is something I can't understand and it's like passing the values ​​to the function. Let me explain, when I pass that <a> that I save in the database, if I transform it into a <span> and then insert it if it tries to make the change to mention but the class attribute and the href attribute are "orphaned".
Well, I have 3 ideas and I can't do any of them at some point I get stuck, so I ask you for help.
My idea is to return the text I have in the database and the editor reads it fine.
Idea 1: Put the text in Javascript and identify and exchange the mentions that are in the database by the function of the mentions command, this is really complicated for me because it is very abstract, even so I am still looking for how to do it.
Idea 2: Save the value in the database in another way, this has been a last idea, how to search and put the of the mention but with the custom values. Even if {mention: {id: #undercover}} were saved in the database, I wouldn't care as long as it was later transformed correctly in the editor.
Idea 3: The use of conversions, I have managed to understand this and it has cost me that its function is to identify the mention within the editor and exchange it for the data you want. In this idea I can't understand how to pass the values ​​other than manually, that is, how to pass the class and href attributes.
Here I leave you the section of the code, I hope you can give me a hand and thank you very much.
function MentionCustomization( editor ) {
// The upcast converter will convert <a class="mention" href="" data-user-id="">
// elements to the model 'mention' attribute.
editor.conversion.for( 'upcast' ).elementToAttribute( {
view: {
name: 'a',
key: 'data-mention',
classes: 'mention',
attributes: {
href: true,
'data-user-id': true,
}
},
model: {
key: 'mention',
value: viewItem => {
// The mention feature expects that the mention attribute value
// in the model is a plain object with a set of additional attributes.
// In order to create a proper object, use the toMentionAttribute helper method:
const mentionAttribute = editor.plugins.get( 'Mention' ).toMentionAttribute( viewItem, {
// Add any other properties that you need.
link: viewItem.getAttribute( 'href' ),
userId: viewItem.getAttribute( 'data-user-id' )
} );
return mentionAttribute;
}
},
converterPriority: 'high'
} );
// Downcast the model 'mention' text attribute to a view <a> element.
editor.conversion.for( 'downcast' ).attributeToElement( {
model: 'mention',
view: ( modelAttributeValue, { writer } ) => {
// Do not convert empty attributes (lack of value means no mention).
if ( !modelAttributeValue ) {
return;
}
return writer.createAttributeElement( 'a', {
class: 'group-color-'+modelAttributeValue.group,
'data-mention': modelAttributeValue.id,
// 'data-user-id': modelAttributeValue.userId,
'href': '/member/profile/'+modelAttributeValue.user_id,
}, {
// Make mention attribute to be wrapped by other attribute elements.
priority: 20,
// Prevent merging mentions together.
id: modelAttributeValue.uid,
} );
},
converterPriority: 'high'
} );
}
$.ajax({
type: "POST",
dataType: "json",
url: "/members/list_json",
success: function(info){
ClassicEditor
.create( document.querySelector( '#comment' ), {
extraPlugins: [ MentionCustomization ],
updateSourceElementOnDestroy: true,
language: 'es',
toolbar: [ 'bold', 'italic', '|' , 'link', '|', 'bulletedList'],
mention: {
feeds: [
{
marker: '#',
feed: getFeedItems,
minimumCharacters: 2,
itemRenderer: customItemRenderer,
}
]
}
} )
.then( editor => {
window.editor = editor;
/*
*/
} )
.catch( err => {
console.error( err.stack );
} );
let list_members = [];
for(let i = 0; i < info.length; i++){
var member = info[i];
list_members.push(member);
}
function getFeedItems( queryText ) {
return new Promise( resolve => {
setTimeout( () => {
const itemsToDisplay = list_members
.filter( isItemMatching )
.slice( 0, 10 );
resolve( itemsToDisplay );
}, 100 );
} );
function isItemMatching( item ) {
const searchString = queryText.toLowerCase();
return (
item.username.toLowerCase().includes( searchString )
);
}
}
},
});
function customItemRenderer( item ) {
const itemElement = document.createElement( 'span' );
const avatar = document.createElement( 'img' );
const userNameElement = document.createElement( 'span' );
itemElement.classList.add( 'mention__item');
avatar.src = `${ item.avatar }`;
avatar.classList.add('image-fluid', 'img-thumbnail', 'rounded-circle');
userNameElement.classList.add( 'mention__item__user-name' );
userNameElement.style.cssText = 'color: '+ item.group_color +';';
userNameElement.textContent = item.id;
itemElement.appendChild( avatar );
itemElement.appendChild( userNameElement );
return itemElement;
}

Native undo role for the menu bar does not work after programmatic value changes

Code
I am building an application menu for the menu bar, and I am using roles to automate some pre-defined functionalities like so:
{
label: 'File',
submenu: []
},
{
label: 'Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' }
]
},
{
label: 'View',
submenu: []
},
...
Issue
The following code part renders the menu and works like magic: { role: 'undo' } but only for normal operations.
However, when you programmatically replace the value of a form control (textbox, texarea, etc), and then do some manual changes to that form control, the Undo feature will no longer work.
What could have I missed on this? Is this a bug or something? Below are the versions of software used.
Environment
Software
Version
Electron
12.2.3
Node.js
14.16.0
MacOS
10.13.6
As Electron's Roles are only an extension of Chrome's functionality, the real test is to see if the same occurs when not using Electron.
I have tested your problem in both a standard html code base and an Electron code base.
Standard HTML Code Base
In the below file I have two text fields. One is the input field and the other is to provide feedback, just to be sure.
The button is to dynamically append some text to the end of the input field.
To test:
Type some text into the input field.
Try undo and redo functionality. // Success
Windows: Ctrl-Z and Ctrl-Y respectively.
MacOS: Cmd-Z and Shift-Cmd-Z respectively.
Now click the button to append some text.
Try undo and redo again. // Failure
After dynamically adding the text, undo and redo no longer works. Why? I don't know why, but this appears to be standard functionality for Chrome.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Undo / Redo Test</title>
</head>
<body>
<div>
<label for="input">Input: </label>
<input type="text" id="input">
<input type="button" id="button" value="Append 'text'">
</div>
<div>
<label for="output">Output: </label>
<input type="text" id="output" disabled>
</div>
</body>
<script>
let input = document.getElementById('input');
let output = document.getElementById('output');
input.addEventListener('keyup', () => {
output.value = input.value;
});
document.getElementById('button').addEventListener('click', () => {
let value = input.value + ' text';
input.value = value;
output.value = value;
});
</script>
</html>
Electron Code Base
Now, let's move the exact same html code over into an Electron application, so we can test it with the Edit -> Undo / Redo menu as well.
Testing via the keyboard shortcuts yields the same result as above.
Let's test it with the menu:
Type some text into the input field.
Select edit -> undo and redo functionality. // Succees
Now click the button to append some text.
Try edit -> undo and redo again. // Failure
The same result. After dynamically adding the text, undo and redo no longer works. A reflection of the Standard HTML Code Base.
main.js (main thread)
'use strict';
const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;
const nodePath = require("path");
let window;
function createWindow() {
const window = new electronBrowserWindow({
x: 0,
y: 0,
width: 800,
height: 600,
fullscreen: false,
resizable: true,
movable: true,
minimizable: true,
maximizable: true,
enableLargerThanScreen: true,
closable: true,
focusable: true,
fullscreenable: true,
frame: true,
hasShadow: true,
backgroundColor: '#fff',
show: false,
icon: nodePath.join(__dirname, 'icon.png'),
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
worldSafeExecuteJavaScript: true,
enableRemoteModule: false,
devTools: (! electronApp.isPackaged),
preload: nodePath.join(__dirname, 'preload.js')
}
});
window.loadFile('index.html')
.then(() => { window.show(); });
return window;
}
electronApp.on('ready', () => {
window = createWindow();
});
electronApp.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
electronApp.quit();
}
});
electronApp.on('activate', () => {
if (electronBrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
preload.js (main thread)
'use strict';
// Import the necessary Electron components.
const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;
// White-listed channels.
const ipc = {
'render': {
// From render to main.
'send': [],
// From main to render.
'receive': [],
// From render to main and back again.
'sendReceive': []
}
};
// Exposed protected methods in the render process.
contextBridge.exposeInMainWorld(
// Allowed 'ipcRenderer' methods.
'ipcRender', {
// From render to main.
send: (channel, args) => {
let validChannels = ipc.render.send;
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, args);
}
},
// From main to render.
receive: (channel, listener) => {
let validChannels = ipc.render.receive;
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`.
ipcRenderer.on(channel, (event, ...args) => listener(...args));
}
},
// From render to main and back again.
invoke: (channel, args) => {
let validChannels = ipc.render.sendReceive;
if (validChannels.includes(channel)) {
return ipcRenderer.invoke(channel, args);
}
}
}
);
index.html (render thread)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Undo / Redo Test</title>
</head>
<body>
<div>
<label for="input">Input: </label>
<input type="text" id="input">
<input type="button" id="button" value="Append 'text'">
</div>
<div>
<label for="output">Output: </label>
<input type="text" id="output" disabled>
</div>
</body>
<script>
let input = document.getElementById('input');
let output = document.getElementById('output');
input.addEventListener('keyup', () => {
output.value = input.value;
});
document.getElementById('button').addEventListener('click', () => {
let value = input.value + ' text';
input.value = value;
output.value = value;
});
</script>
</html>
Tested using:
Electron: 17.1.0
Node: 16.13.0
Chrome: 98.04.4758.102
Tested on:
Windows: 10
MacOS: 10.15.7
The Memento & Command Design Patterns
You could try and hook your undo and redo with something like execCommand but I think, if you're wanting to do it right, make it scalable and readable then you should really implement something along the lines of the Memento and Command design patterns.
Though a fair bit of work to setup, once working, it could be applied to any <form> field on the page, not only <input type="text"> and <textarea>.
But why stop there, make it applicable to page or application wide functionality as well (where it can be applied). EG: 'delete', 'rename', 'move', 'post', 'settings update', etc.

Cannot get instance of CKEditor

I have several fields which need to be initialized with CKEditor, for this I have created an helper class that contains the initEditor method.
The method below should return the initialized editor but it doesn't:
window.CKEditorHelper = window.CKEditorHelper || {};
(function (exports) {
exports.initEditor = function (input, myEditor) {
ClassicEditor
.create(document.querySelector(input), {
language: {
ui: 'en'
content: 'en'
}
})
.then(editor => {
myEditor = editor;
});
};
})(window.CKEditorHelper);
this is called in the following way:
let editor = null;
CKEditorHelper.initEditor('#description', editor);
so when I click on a button:
$('#save').on('click', function(){
console.log(editor.getData());
});
I get:
Cannot read property 'getData' of null
what I did wrong?
There are some issues on your code
let editor = null;
the let keyword only define a variable within function scope, when you use editor on another scope (your click handle event), it could be undefined
Another line
myEditor = editor;
This just simple made the reference to your original editor object will gone
Here is my solution to fix it
Change the way you init an editor like bellow
window.editorInstance = {editor: null};
CKEditorHelper.initEditor('#description', editorInstance);
Change your CKEditorHelper to
window.CKEditorHelper = window.CKEditorHelper || {};
(function (exports) {
exports.initEditor = function (input, myEditorInstance) {
ClassicEditor
.create(document.querySelector(input), {
language: {
ui: 'en'
content: 'en'
}
})
.then(editor => {
myEditorInstance.editor = editor;
});
};
})(window.CKEditorHelper);
And when you want to use your editor
console.log(editorInstance.editor.getData());
You can give this in javascript
$(document).ready(function () {
CKEDITOR.replace('tmpcontent', { height: '100px' })
})
take the value by using following
$('#save').on('click', function(){
var textareaValue = CKEDITOR.instances.tmpcontent.getData();
});
<label class="control-label">Message</label>
<textarea name="tmpcontent" id="tmpcontent" class="form-control"></textarea>
//OR in latest version
var myEditor;
ClassicEditor
.create( document.querySelector( '#description' ) )
.then( editor => {
console.log( 'Editor was initialized', editor );
myEditor = editor;
} )
.catch( err => {
console.error( err.stack );
} );
and then get data using
myEditor.getData();

How to disable default title block in Gutenberg with Javascript

Is there a way to disable the default title block in the Gutenberg editor with JavaScript? Couldn't find anything in the official API.
Something like a property in the settings object that you pass when setting up the Gutenberg.
Currently, I do like so (with the title):
const window = this.iframe.contentWindow // this.iframe is a React ref
const wpData = window.wp ? window.wp.data : false
if (!wpData) {
return
}
const newPost = Object.assign({ content: { raw: '', rendered: '' } }, window._wpGutenbergDefaultPost)
const editorSettings = {
alignWide: false,
availableTemplates: [],
blockTypes: true,
disableCustomColors: false,
disablePostFormats: false,
titlePlaceholder: '',
bodyPlaceholder: 'Add content here'
}
const editor = wpData.dispatch('core/editor')
editor.setupEditor(newPost, editorSettings)

CKEditor5 Custom Modal Plugin

I followed the initial plugin tutorial and got the Image Insert to work, but I would like to display a custom modal with two input fields instead of the prompt to set some more attributes.
How would I best implement this?
I know how to implement a normal modal in plain JS/CSS but I am a bit confused as to where to put the HTML for the modal to be displayed on the button click.
class Test extends Plugin {
init() {
editor = this.editor
editor.ui.componentFactory.add('SampleView', (locale) => {
const view = new ButtonView(locale)
view.set({
label: 'test',
icon: imageIcon,
tooltip: true
})
view.on('execute', () => {
//here I would like to open the modal instead of the prompt
})
})
}
}
For example, you can try to use SweetAlert2 which is zero-dependency pure javascript replacement for default popups.
import swal from 'sweetalert2';
...
view.on( 'execute', () => {
swal( {
input: 'text',
inputPlaceholder: 'Your image URL'
} )
.then ( result => {
editor.model.change( writer => {
const imageElement = writer.createElement( 'image', {
src: result.value
} );
editor.model.insertContent( imageElement, editor.model.document.selection );
} );
} )
} );

Categories

Resources