CodeMirror2: How to format a pasted content? - javascript

Is it possible to format the inserted content after an event like "onPaste" in CodeMirror2? - I want to indent the content from the clipboard after pasting. I already know with JavaScript it's not possible to get access to the clipboard.
So I think there is also no possiblity to create a context menu with cut/copy/paste functions? - Could I create my own JS-clipboard or is there an existing solution?
Thanks!
leX

Took me a little while to work this out, so in case it helps anyone, here's how I'm intercepting pastes and replacing each tab with 2 spaces:
editor.on("beforeChange", (cm, change) => {
if(change.origin === "paste") {
const newText = change.text.map(line => line.replace(/\t/g, " "));
change.update(null, null, newText);
}
});

A CodeMirror has native "inputRead" event so you can do this:
editor.on('inputRead', function(cm, event) {
/* event -> object{
origin: string, can be '+input', '+move' or 'paste'
doc for origins >> http://codemirror.net/doc/manual.html#selection_origin
from: object {line, ch},
to: object {line, ch},
removed: array of removed strings
text: array of pasted strings
} */
if (event.origin == 'paste') {
console.log(event.text);
var text = event.text[0]; // pasted string
var new_text = '['+text+']'; // any operations here
cm.refresh();
// my first idea was
// note: for multiline strings may need more complex calculations
cm.replaceRange(new_text, event.from, {line: event.from.line, ch: event.from.ch + text.length});
// first solution did'nt work (before i guess to call refresh) so i tried that way, works too
/* cm.execCommand('undo');
cm.setCursor(event.from);
cm.replaceSelection(new_text); */
}
});
And there are also other events "cut", "copy" and "paste" (http://codemirror.net/doc/manual.html#events) so this will work:
editor.on('paste', function(cm, event) { ... } );
Andavtage is you can cancel event by calling event.preventDefault(); but retrieving pasted text is a problem.
Currently i am working on similar thing - hook copy/paste events and make some replacements. I found a solution to programmatically copy text to clipboard. This is clipboard.js
discussed here Does codemirror provide Cut, Copy and Paste API?. The great thing is you can trigger click event programmatically (that was a problem when i used ZeroClipboard with cross-browser flash shim) and it will work!
new Clipboard('.btn-copy', {
text: function(trigger) {
var text = editor.getValue(); // or editor.getSelection();
return text.replace(/\s+/g,' ');
}
});
editor.on('copy', function(cm, event) {
$('.btn-copy').click();
event.preventDefault();
});

The only solution I found was intercepting the even using inputRead and then manually changing the pasted contents. I made a generic funcion you could reuse:
class CodeMirrorExt {
// This function returns a callback to be used with codemirror's inputRead event.
// The intent is to intercept a pasted text, transform the pasted contents (each line) with the function
// you provide, and then have that transformed content be what ends up in the editor.
//
// Usage example that cuts each pasted line to 10 chars:
//
// this.codemirror.on("inputRead", CodeMirrorExt.replaceTextOnPasteFunc((line) => {
// return line.slice(0, 10);
// }));
static replaceTextOnPasteFunc(transformFunc) {
return ((doc, event) => {
if (event.origin !== "paste") {
return;
}
const firstText = event.text[0];
const lineNum = event.from.line;
const chNum = event.from.ch;
const newTexts = event.text.map(transformFunc);
newTexts.forEach((text, i) => {
if (i == 0) {
doc.replaceRange(text, {line: lineNum, ch: chNum}, {line: lineNum, ch: chNum + firstText.length});
}
else {
doc.replaceRange(text, {line: lineNum + i, ch: 0}, {line: lineNum + i, ch: event.text[i].length});
}
})
});
}
}
export default CodeMirrorExt;

Using js-beautify you can beautify the editors value during a paste event on the .CodeMirror like so:
$(document).on('paste', '.CodeMirror', function(e) {
var content = $(this).closest('.content');
var editor = content[0].editor;
// beautify the code
editor.setValue( js_beautify( editor.getValue(), { indent_size: 2 } ) );
});
Be aware that when inserting text into the editor, it listens to the first indentation, so if your first line is indented, all other lines will be indented accordingly.
ex:
function something() { // if the start is indented like so,
var blah = something; // next lines will follow
}

Related

Copy and paste text in selectizeInput in a Shiny application

I have a selectizeInput UI object in my Shiny app and I would like to be able to copy and paste a comma-delimited list of inputs (i.e. copy from and paste to the selectizeInput).
At the moment I can copy the comma-delimited list of inputs (i.e. A, B, C, D) from elsewhere and then paste it in my selectizeInput. Paste only works using "Ctrl + V", not "right-click + paste", but this is fine.
I would like to be able to also copy my inputs from the selectizeInput object so I can paste them elsewhere.
See code below (the first choice is an empty string, "", as I do not want anything to be selected at the beginning):
selectizeInput(
inputId = "genes_list",
label = "Genes",
width = "100%",
multiple = TRUE,
choices = c("", genes),
selected = "",
options = list(
delimiter = ',',
create = I("function(input, callback){
return {
value: input,
text: input };
}")))
I can select all inputs using "Ctrl + A" or specific inputs using "Ctrl + mouse-click" (I know inputs have been selected as they change color when selected) but then "Ctrl + C" or "Ctrl + X" do not work. Also, right-clicking on the selected inputs does not provide a "Copy" option.
Ideally, I would like to use "Ctrl + A" or "Ctrl + mouse-click" to select my inputs and then use "Ctrl + C" to copy them.
Thanks
This is a bit of a long-winded solution, but it works. It injects a javascript behaviour into your selectizeInput of copying into clipboard/pastebin when a person uses copy-paste shortcuts.
There are much cleaner ways to do it, but they require more advanced concepts, like separate .js files. So here is the essier, but messier way.
Below is the code, so you see roughly what it does (all the console.log() bits can be removed and are there for you to see all steps and how they happen. To see the 'console' onen dev tools in your browser, and there is a console panel there (it's sort of like a 'kitchen-door/gossip wall' of your app).
Here's the javascript that will do it, below is the explanation where to add it. In short it will:
once the page is loaded
add a new behaviour whenever someone tries to copy
when someone tries to copy, check if the field they copy from is a selectize-input
if it is, grab text in it, separate it by comas, and put thsat in the clipboard/pastebin
Javascript Code:
console.log("page will load now");
document.addEventListener("DOMContentLoaded", function(){
console.log("page loaded");
document.addEventListener("copy", (event) => {
console.log("coppying from item:", event.target);
const anchorNode = document.getSelection().anchorNode
if (anchorNode instanceof HTMLElement && anchorNode.classList.contains("selectize-input")) {
const items = Array.from(anchorNode.getElementsByClassName("item active"))
const selectedItemsAsString = items.map(i => i.innerText).join(", ")
console.log("coppied content:", selectedItemsAsString);
event.clipboardData.setData("text/plain", selectedItemsAsString)
event.preventDefault()
}
})
});
Explanation where to put it, at the end of your ui <- fluidPage( ..... )
ui <- fluidPage( some_components_of_yours,
selectizeInput(
"codeInput",
label = "Codes (if pasting, coma separated)",
choices = c("", genes),
multiple = T,
options = list(delimiter = ",", create = T),
),
some_components_of_yours,
tags$script(HTML(
'HERE IN THESE SINGLE QUOTES PUT THE JAVASCRIPT CODE FROM ABOVE'
)))
So it will look a bit like this:
ui <- fluidPage(
selectizeInput(
"codeInput",
label = "Codes (if pasting, coma separated)",
choices = c("", genes),
multiple = T,
options = list(delimiter = ",", create = T),
),
tags$script(HTML(
' console.log("page will load now");
document.addEventListener("DOMContentLoaded", function(){
console.log("page loaded");
document.addEventListener("copy", (event) => {
console.log("coppying from item:", event.target);
const anchorNode = document.getSelection().anchorNode
if (anchorNode instanceof HTMLElement && anchorNode.classList.contains("selectize-input")) {
const items = Array.from(anchorNode.getElementsByClassName("item active"))
const selectedItemsAsString = items.map(i => i.innerText).join(", ")
console.log("coppied content:", selectedItemsAsString);
event.clipboardData.setData("text/plain", selectedItemsAsString)
event.preventDefault()
}
})
});'
)))
My solution is based on these answers of other people:
https://stackoverflow.com/a/59773533/498201
https://github.com/selectize/selectize.js/issues/1177#issuecomment-480633418

Add event to Series.Toggle in Rickshaw

I want to add functionality when clicking on items in the legend in Rickshaw. I use the standard code to add toggle functionality, which I want to extend to run my own function:
shelving = new Rickshaw.Graph.Behavior.Series.Toggle( {
graph: graph,
legend: legend
}
Is there a way add a function of my own here? I also tried looking through and editing the code in Rickshaw.Graph.Behavior.Series.Toggle.js, but I couldn't get anything to run upon clicking items in the legend. (Might it be that the imported js file is cached so that my edits don't take effect?)
As you explicitly asked about what source code to modify, I highlighted it below.
If you modify the source, upgrading to a new version becomes much harder, extending/creating own version would be the preferred way to go.
In Rickshaw.Graph.Behavior.Series.Toggle.js
'Rickshaw.Graph.Behavior.Series.Toggle.js' change the following
Rickshaw.Graph.Behavior.Series.Toggle = function(args) {
...
this._addBehavior = function() {
this.graph.series.forEach( function(s) {
s.disable = function() {
if (self.graph.series.length <= 1) {
throw('only one series left');
}
s.disabled = true;
alert('disabling ' + s.name); //HERE
self.graph.update();
};
s.enable = function() {
s.disabled = false;
alert('enabling ' + s.name); //HERE
self.graph.update();
};
} );
};
...
};

CKEditor - remove script tag with data processor

I am quite new with CKEditor (starting to use it 2 days ago) and I am still fighting with some configuration like removing the tag from editor.
So for example, if a user type in source mode the following:
<script type="text/javascript">alert('hello');</script>
I would like to remove it.
Looking the documentation, I found that this can be done using an HTML filter. I so defined it but it does not work.
var editor = ev.editor;
var dataProcessor = editor.dataProcessor;
var htmlFilter = dataProcessor && dataProcessor.htmlFilter;
htmlFilter.addRules(
{
elements :
{
script : function(element)
{
alert('Found script :' + element.name);
element.remove();
},
img : function( element )
{
alert('Found script :' + element.name);
if ( !element.attributes.alt )
element.attributes.alt = 'Cookingfactory';
}
}
});
The img part is working well but not the script one. I guess I missed something. It even does not display the alert message for script.
Any help would be more than welcome :o)
You can use this :
CKEDITOR.replace('editor1', {
on: {
pluginsLoaded: function(event) {
event.editor.dataProcessor.dataFilter.addRules({
elements: {
script: function(element) {
return false;
}
}
});
}
}
});
If you are using CKEditor 4.1 or above, you may use Advanced Content Filter to allow the content you want.
If you are using CKEditor 4.4 or above, there is an easier way. You can use Disallowed Content to filter content you don't like .
config.disallowedContent = 'script';
As I'm having CKEditor 4, I did the next
CKEDITOR.instances.editor1.config.protectedSource.push( /{.*\".*}/g );
It will ignore quotes in smarty curly brackets

CodeMirror with spell checker

I would like to use the functionality of CodeMirror (such as linenumbering, wrapping, search, etc.) for plain text, without particular need of code highlightening but instead with Google Chrome spell checker or some other natural language (especially English) spell checking activated (I do not need to have it work on other browsers). How can I do this? Is it possible to write a plain text mode add-on that enables spell checking?
I actually integrated typo.js with CodeMirror while coding for NoTex.ch; you can have a look at it here CodeMirror.rest.js; I needed a way to get the reStructuredText markup spell checked, and since I use CodeMirror's excellent syntax highlighting capabilities, it was quite straight forward to do.
You can check the code at the provided link, but I'll summarize, what I've done:
Initialize the typo.js library; see also the author's blog/documentation:
var typo = new Typo ("en_US", AFF_DATA, DIC_DATA, {
platform: 'any'
});
Define a regular expression for your word separators:
var rx_word = "!\"#$%&()*+,-./:;<=>?#[\\\\\\]^_`{|}~";
Define an overlay mode for CodeMirror:
CodeMirror.defineMode ("myoverlay", function (config, parserConfig) {
var overlay = {
token: function (stream, state) {
if (stream.match (rx_word) &&
typo && !typo.check (stream.current ()))
return "spell-error"; //CSS class: cm-spell-error
while (stream.next () != null) {
if (stream.match (rx_word, false)) return null;
}
return null;
}
};
var mode = CodeMirror.getMode (
config, parserConfig.backdrop || "text/x-myoverlay"
);
return CodeMirror.overlayMode (mode, overlay);
});
Use the overlay with CodeMirror; see the user manual to figure out how exactly you do this. I've done it in my code so you could check it out there too, but I recommend the user manual.
Define CSS class:
.CodeMirror .cm-spell-error {
background: url(images/red-wavy-underline.gif) bottom repeat-x;
}
This approach works great for German, English and Spanish. With the French dictionary typo.js seems to have some (accent) problems, and languages like Hebrew, Hungarian, and Italian - where the number of affixes is long or the dictionary is quite extensive - it does not work really, since typo.js at its current implementation uses too much memory and is too slow.
With German (and Spanish) typo.js can block the JavaScript VM for a few hundred milliseconds (but only during initialization!), so you might want to consider background threads with HTML5 web workers (see CodeMirror.typo.worker.js for an example). Further typo.js does not seem to support Unicode (due to JavaScript restrictions): At least, I did not manage to get it to work with non-Latin languages like Russian, Greek, Hindi etc.
I've not refactored the described solution into a nice separate project apart from (now quite big) NoTex.ch, but I might do it quite soon; till then you've to patch your own solution based on the above description or hinted code. I hope this helps.
In CodeMirror 5.18.0 and above, you can set inputStyle: 'contenteditable' with spellcheck: true to be able to use your web browser's spellcheck features. For example:
var myTextArea = document.getElementById('my-text-area');
var editor = CodeMirror.fromTextArea(myTextArea, {
inputStyle: 'contenteditable',
spellcheck: true,
});
The relevant commits that made this solution possible are:
Make inputStyle an option, move relevant logic into TextareaInput (CodeMirror 5.0.0, February 2015)
Add experimental spellcheck option (CodeMirror 5.18.0, August 2016)
This is a working version of hsk81's answer. It uses CodeMirror's overlay mode, and looks for any word inside quotes, html tags, etc. It has a sample typo.check that should be replaced with something like Typo.js. It underlines unknown words with a red squiggly line.
This was tested using an IPython's %%html cell.
<style>
.CodeMirror .cm-spell-error {
background: url("https://raw.githubusercontent.com/jwulf/typojs-project/master/public/images/red-wavy-underline.gif") bottom repeat-x;
}
</style>
<h2>Overlay Parser Demo</h2>
<form><textarea id="code" name="code">
</textarea></form>
<script>
var typo = { check: function(current) {
var dictionary = {"apple": 1, "banana":1, "can't":1, "this":1, "that":1, "the":1};
return current.toLowerCase() in dictionary;
}
}
CodeMirror.defineMode("spell-check", function(config, parserConfig) {
var rx_word = new RegExp("[^\!\"\#\$\%\&\(\)\*\+\,\-\.\/\:\;\<\=\>\?\#\[\\\]\^\_\`\{\|\}\~\ ]");
var spellOverlay = {
token: function (stream, state) {
var ch;
if (stream.match(rx_word)) {
while ((ch = stream.peek()) != null) {
if (!ch.match(rx_word)) {
break;
}
stream.next();
}
if (!typo.check(stream.current()))
return "spell-error";
return null;
}
while (stream.next() != null && !stream.match(rx_word, false)) {}
return null;
}
};
return CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || "text/html"), spellOverlay);
});
var editor = CodeMirror.fromTextArea(document.getElementById("code"), {mode: "spell-check"});
</script>
CodeMirror is not based on an HTML textarea, so you can't use the built-in spell check
You could implement your own spell check for CodeMirror with something like typo.js
I don't believe anyone has done this yet.
I wrote a squiggly underline type spell checker a while ago. It needs a rewrite to be honest I was very new to JavaScript then. But the principles are all there.
https://github.com/jameswestgate/SpellAsYouType
I created a spellchecker with typos suggestions/corrections:
https://gist.github.com/kofifus/4b2f79cadc871a29439d919692099406
demo: https://plnkr.co/edit/0y1wCHXx3k3mZaHFOpHT
Below are relevant parts of the code:
First I make a promise to get load the dictionaries. I use typo.js for the dictionary, loading can take a while if they are not hosted locally, so it is better to start the loading as soon as your starts before login/CM initializing etc:
function loadTypo() {
// hosting the dicts on your local domain will give much faster results
const affDict='https://rawgit.com/ropensci/hunspell/master/inst/dict/en_US.aff';
const dicDict='https://rawgit.com/ropensci/hunspell/master/inst/dict/en_US.dic';
return new Promise(function(resolve, reject) {
var xhr_aff = new XMLHttpRequest();
xhr_aff.open('GET', affDict, true);
xhr_aff.onload = function() {
if (xhr_aff.readyState === 4 && xhr_aff.status === 200) {
//console.log('aff loaded');
var xhr_dic = new XMLHttpRequest();
xhr_dic.open('GET', dicDict, true);
xhr_dic.onload = function() {
if (xhr_dic.readyState === 4 && xhr_dic.status === 200) {
//console.log('dic loaded');
resolve(new Typo('en_US', xhr_aff.responseText, xhr_dic.responseText, { platform: 'any' }));
} else {
console.log('failed loading aff');
reject();
}
};
//console.log('loading dic');
xhr_dic.send(null);
} else {
console.log('failed loading aff');
reject();
}
};
//console.log('loading aff');
xhr_aff.send(null);
});
}
Second I add an overlay to detect and mark typos like this:
cm.spellcheckOverlay={
token: function(stream) {
var ch = stream.peek();
var word = "";
if (rx_word.includes(ch) || ch==='\uE000' || ch==='\uE001') {
stream.next();
return null;
}
while ((ch = stream.peek()) && !rx_word.includes(ch)) {
word += ch;
stream.next();
}
if (! /[a-z]/i.test(word)) return null; // no letters
if (startSpellCheck.ignoreDict[word]) return null;
if (!typo.check(word)) return "spell-error"; // CSS class: cm-spell-error
}
}
cm.addOverlay(cm.spellcheckOverlay);
Third I use a list box to show suggestions and fix typos:
function getSuggestionBox(typo) {
function sboxShow(cm, sbox, items, x, y) {
let selwidget=sbox.children[0];
let options='';
if (items==='hourglass') {
options='<option>⌛</option>'; // hourglass
} else {
items.forEach(s => options += '<option value="' + s + '">' + s + '</option>');
options+='<option value="##ignoreall##">ignore all</option>';
}
selwidget.innerHTML=options;
selwidget.disabled=(items==='hourglass');
selwidget.size = selwidget.length;
selwidget.value=-1;
// position widget inside cm
let cmrect=cm.getWrapperElement().getBoundingClientRect();
sbox.style.left=x+'px';
sbox.style.top=(y-sbox.offsetHeight/2)+'px';
let widgetRect = sbox.getBoundingClientRect();
if (widgetRect.top<cmrect.top) sbox.style.top=(cmrect.top+2)+'px';
if (widgetRect.right>cmrect.right) sbox.style.left=(cmrect.right-widgetRect.width-2)+'px';
if (widgetRect.bottom>cmrect.bottom) sbox.style.top=(cmrect.bottom-widgetRect.height-2)+'px';
}
function sboxHide(sbox) {
sbox.style.top=sbox.style.left='-1000px';
}
// create suggestions widget
let sbox=document.getElementById('suggestBox');
if (!sbox) {
sbox=document.createElement('div');
sbox.style.zIndex=100000;
sbox.id='suggestBox';
sbox.style.position='fixed';
sboxHide(sbox);
let selwidget=document.createElement('select');
selwidget.multiple='yes';
sbox.appendChild(selwidget);
sbox.suggest=((cm, e) => { // e is the event from cm contextmenu event
if (!e.target.classList.contains('cm-spell-error')) return false; // not on typo
let token=e.target.innerText;
if (!token) return false; // sanity
// save cm instance, token, token coordinates in sbox
sbox.codeMirror=cm;
sbox.token=token;
let tokenRect = e.target.getBoundingClientRect();
let start=cm.coordsChar({left: tokenRect.left+1, top: tokenRect.top+1});
let end=cm.coordsChar({left: tokenRect.right-1, top: tokenRect.top+1});
sbox.cmpos={ line: start.line, start: start.ch, end: end.ch};
// show hourglass
sboxShow(cm, sbox, 'hourglass', e.pageX, e.pageY);
// let the ui refresh with the hourglass & show suggestions
setTimeout(() => {
sboxShow(cm, sbox, typo.suggest(token), e.pageX, e.pageY); // typo.suggest takes a while
}, 100);
e.preventDefault();
return false;
});
sbox.onmouseleave=(e => {
sboxHide(sbox)
});
selwidget.onchange=(e => {
sboxHide(sbox)
let cm=sbox.codeMirror, correction=e.target.value;
if (correction=='##ignoreall##') {
startSpellCheck.ignoreDict[sbox.token]=true;
cm.setOption('maxHighlightLength', (--cm.options.maxHighlightLength) +1); // ugly hack to rerun overlays
} else {
cm.replaceRange(correction, { line: sbox.cmpos.line, ch: sbox.cmpos.start}, { line: sbox.cmpos.line, ch: sbox.cmpos.end});
cm.focus();
cm.setCursor({line: sbox.cmpos.line, ch: sbox.cmpos.start+correction.length});
}
});
document.body.appendChild(sbox);
}
return sbox;
}
Hope this helps !

BB Code Parser (in formatting phase) with jQuery jammed due to messed up loops most likely

Greetings everyone,
I'm making a BB Code Parser but I'm stuck on the JavaScript front. I'm using jQuery and the caret library for noting selections in a text field. When someone selects a piece of text a div with formatting options will appear.
I have two issues.
Issue 1. How can I make this work for multiple textfields? I'm drawing a blank as it currently will detect the textfield correctly until it enters the
$("#BBtoolBox a").mousedown(function() { }
loop. After entering it will start listing one field after another in a random pattern in my eyes.
!!! MAIN Issue 2. I'm guessing this is the main reason for issue 1 as well. When I press a formatting option it will work on the first action but not the ones afterwards. It keeps duplicating the variable parsed. (if I only keep to one field it will never print in the second)
Issue 3 If you find anything especially ugly in the code, please tell me how to improve myself.
I appriciate all help I can get. Thanks in advance
$(document).ready(function() {
BBCP();
});
function BBCP(el) {
if(!el) { el = "textarea"; }
// Stores the cursor position of selection start
$(el).mousedown(function(e) {
coordX = e.pageX;
coordY = e.pageY;
// Event of selection finish by using keyboard
}).keyup(function() {
BBtoolBox(this, coordX, coordY);
// Event of selection finish by using mouse
}).mouseup(function() {
BBtoolBox(this, coordX, coordY);
// Event of field unfocus
}).blur(function() {
$("#BBtoolBox").hide();
});
}
function BBtoolBox(el, coordX, coordY) {
// Variable containing the selected text by Caret
selection = $(el).caret().text;
// Ignore the request if no text is selected
if(selection.length == 0) {
$("#BBtoolBox").hide();
return;
}
// Print the toolbox
if(!document.getElementById("BBtoolBox")) {
$(el).before("<div id=\"BBtoolBox\" style=\"left: "+ ( coordX + 5 ) +"px; top: "+ ( coordY - 30 ) +"px;\"></div>");
// List of actions
$("#BBtoolBox").append("<img src=\"./icons/text_bold.png\" alt=\"B\" title=\"Bold\" />");
$("#BBtoolBox").append("<img src=\"./icons/text_italic.png\" alt=\"I\" title=\"Italic\" />");
} else {
$("#BBtoolBox").css({'left': (coordX + 3) +'px', 'top': (coordY - 30) +'px'}).show();
}
// Parse the text according to the action requsted
$("#BBtoolBox a").mousedown(function() {
switch($(this).children(":first").attr("alt"))
{
case "B": // bold
parsed = "[b]"+ selection +"[/b]";
break;
case "I": // italic
parsed = "[i]"+ selection +"[/i]";
break;
}
// Changes the field value by replacing the selection with the variable parsed
$(el).val($(el).caret().replace(parsed));
$("#BBtoolBox").hide();
return false;
});
}
This line: $("#BBtoolBox a").mousedown(function() attaches a function to the links. However, this line is run multiple times, and each time it runs it attaches another function to the links, leaving you with duplicated text.
An optimal solution is to use a plugin, for example (the first one I found): http://urlvars.com/code/example/19/using-jquery-bbcode-editor (demo)

Categories

Resources