I'm all new to this, but after spending a week trying to find an answer, I thought I would try asking directly.
I am building a text editor using javascript and jquery. I have a textarea (with contenteditable), a stylesheet and a js script. What I want is that for each letter pressed, the kerning will be random. I achieved that with a simple function, but I don't want ALL textarea text to have this kerning, only the last letter pressed and so on and so on, so this type of thing would be the result:
simulation
There is what I have so far in my js file:
$(document).ready(
function() {
$('#textarea').keypress(function(){
var KerningRandom = Math.floor((Math.random()*90)-20);
$(this).css('letter-spacing',KerningRandom);
});
Here is my jsfiddle that actually doesn't work in jsfiddle and I don't get why as it works fine in local...?
Thanks!
You cannot address individual characters ( and so glyphs ) in CSS. Only ::first-letter.
Options you have:
convert all characters to individual spans. That's too much I think.
use <canvas> to render text and so to implement text flow layout from scratch.
You can find a working plunker of what you want to achieve there (I forked yours).
https://jsfiddle.net/1gesLgsa/2/
Full code :
//Code from https://stackoverflow.com/questions/1125292/how-to-move-cursor-to-end-of-contenteditable-entity
//Namespace management idea from http://enterprisejquery.com/2010/10/how-good-c-habits-can-encourage-bad-javascript-habits-part-1/
(function( cursorManager ) {
//From: http://www.w3.org/TR/html-markup/syntax.html#syntax-elements
var voidNodeTags = ['AREA', 'BASE', 'BR', 'COL', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'MENUITEM', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR', 'BASEFONT', 'BGSOUND', 'FRAME', 'ISINDEX'];
//From: https://stackoverflow.com/questions/237104/array-containsobj-in-javascript
Array.prototype.contains = function(obj) {
var i = this.length;
while (i--) {
if (this[i] === obj) {
return true;
}
}
return false;
}
//Basic idea from: https://stackoverflow.com/questions/19790442/test-if-an-element-can-contain-text
function canContainText(node) {
if(node.nodeType == 1) { //is an element node
return !voidNodeTags.contains(node.nodeName);
} else { //is not an element node
return false;
}
};
function getLastChildElement(el){
var lc = el.lastChild;
while(lc && lc.nodeType != 1) {
if(lc.previousSibling)
lc = lc.previousSibling;
else
break;
}
return lc;
}
//Based on Nico Burns's answer
cursorManager.setEndOfContenteditable = function(contentEditableElement)
{
while(getLastChildElement(contentEditableElement) &&
canContainText(getLastChildElement(contentEditableElement))) {
contentEditableElement = getLastChildElement(contentEditableElement);
}
var range,selection;
if(document.createRange)//Firefox, Chrome, Opera, Safari, IE 9+
{
range = document.createRange();//Create a range (a range is a like the selection but invisible)
range.selectNodeContents(contentEditableElement);//Select the entire contents of the element with the range
range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start
selection = window.getSelection();//get the selection object (allows you to change selection)
selection.removeAllRanges();//remove any selections already made
selection.addRange(range);//make the range you have just created the visible selection
}
else if(document.selection)//IE 8 and lower
{
range = document.body.createTextRange();//Create a range (a range is a like the selection but invisible)
range.moveToElementText(contentEditableElement);//Select the entire contents of the element with the range
range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start
range.select();//Select the range (make it the visible selection
}
}
}( window.cursorManager = window.cursorManager || {}));
// ACTUAL CODE MADE FOR THIS ANSWER
$('#textarea').keypress(function(event) {
event.preventDefault();
var KerningRandom = Math.floor((Math.random() * 90));
if ($("#last").length > 0)
{
var previousLast = $("#textarea #last").html();
$("#textarea #last").remove();
}
else
var previousLast = "";
$("#textarea").html($("#textarea").html().slice() + previousLast + "<span id='last'>" + String.fromCharCode(event.which) + "</span>")
$("#last").css('margin-left', KerningRandom + "px");
var editableDiv = document.getElementById("textarea");
cursorManager.setEndOfContenteditable(editableDiv)
});
var editableDiv = document.getElementById("textarea");
cursorManager.setEndOfContenteditable(editableDiv)
Point by point explanation :
$('#textarea').keypress(function(event) {
event.preventDefault();
var KerningRandom = Math.floor((Math.random() * 90));
if ($("#last").length > 0)
{
var previousLast = $("#textarea #last").html();
$("#textarea #last").remove();
}
else
var previousLast = "";
$("#textarea").html($("#textarea").html() + previousLast + "<span id='last'>" + String.fromCharCode(event.which) + "</span>")
$("#last").css('margin-left', KerningRandom + "px");
var editableDiv = document.getElementById("textarea");
cursorManager.setEndOfContenteditable(editableDiv)
});
The event.preventDefault() prevent the letter to be added when pressing a key.
Then, we calculate our left margin value, save the previous last letter we had and remove the span that contains the last letter as it's not the last letter anymore.
We append the previous last letter , and the span that has a random left margin (to simulate the kerning) and the value of the pressed key (thanks to
How to find out what character key is pressed?) to the actual content.
After that, we needed to move the carret at the end of the textarea manually, because it would stay at the beginning otherwise.
For that, I used the code from
How to move cursor to end of contenteditable entity so goes there for explanation.
Related
First Time I am asking something here I hope I will be precise enough.
I am trying to make a simple extension for chrome that highlight the selected text when I press "H" on my keyboard, but I have some issue :
Use Case
The user select with his mouse a piece of text.
The user press H on his keyboard
Bibbidi-Bobbidi-Boo the selected text is highlighted.
Code so far
To detect when user press H and to get the piece of text he selected :
window.addEventListener('keydown', e => {
if(e.code == "KeyH")
{
var selected = window.getSelection()
//SimpleHighLight(selected);
ComplexHighLight(selected);
}
});
I have coded coded a simple way to do what I want like this :
function SimpleHighLight(selected){
var selectedText = selected.toString();
if(selectedText.length != 0)
{
var range = selected.getRangeAt(0);
var element = selected.anchorNode.parentNode;
var highlited = "<span style='background: rgb(255,255,0)'>" + selectedText + "</span>";
var reg = new RegExp(selectedText,"g");
var text = element.innerHTML.replace(reg, highlited);
element.innerHTML = text;
}
}
It work fine for piece of text in an unique DOM element and when there is no other occurrence of the selected text but I want it to always work, like in a case of my selected text comes from 2 different paragraphs.
So I did this :
function ComplexHighLight(selected){
var selectedText = selected.toString();
if(selectedText.length != 0)
{
console.log(" Selection : " + selectedText);
var range = selected.getRangeAt(0);
if(!range.collapsed)
{
var startNode = range.startContainer;
var startOffset = range.startOffset;
var endNode = range.endContainer;
var endOffset = range.endOffset;
if(startNode == endNode) //Means that its in the same node element
{
var highlited = "<span style='background: rgb(255,255,0)'>" + selectedText + "</span>";
startNode.replaceData(startOffset, endOffset-startOffset, highlited);
startNode.parentNode.innerHTML = startNode.nodeValue;
}
}
}
}
That's only a part of the problem where I handle when a piece of text is in the same element (I am already too much in trouble to handle when the selected text comes from multiples elements :( ).
Issue
On paper, it should work, but the main issue is that when I do :
startNode.parentNode.innerHTML = startNode.nodeValue;
the <span> division is given to innerHTML as a string and not some HTML stuff.
I have worked around this for about the whole evening but I can't fix it, does anyone have an idea of how I should do that ?
I'm creating a markdown editor and I need to check if neighbor characters are specific characters, then remove them, else append them.
For e.g I want to check selected-text, tow neighbor characters are **, then remove them, else append them around selected text.
I can get selected text using this approach:
function getSelection(elem) {
var selectedText;
if (document.selection != undefined) { // IE
elem.focus();
var sel = document.selection.createRange();
selectedText = sel.text;
} else if (elem.selectionStart != undefined) { // Firefox
var startPos = elem.selectionStart;
var endPos = elem.selectionEnd;
selectedText = elem.value.substring(startPos, endPos)
}
return selectedText;
}
$(document).on('mousedown', 'button', function(e) {
var selection = getSelection( $('#txtarea').get(0) );
alert(selection);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<textarea id="txtarea">this is a test</textarea>
<button>Bold (toggle)</button>
Now I need when user clicks on that button, it checks if selected text is between ** like this **selectedtext**, then remove them like this selected text else append them like this **selectedtext**. How can I do that?
Before anything I would like to refer to all the markdown editors out there: https://www.google.de/webhp?sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8#q=javascript%20markdown%20library
So: do not try to reinvent the the wheel, and so on.
But for the sake of learning, my approach would look like this:
function toggleMarker(marker, el) {
var markerLength = marker.length;
var startPos, endPos, selection, range;
if (document.selection != undefined) { // IE
el.focus();
range = document.selection.createRange();
selection = range.text;
} else if (el.selectionStart != undefined) { // Firefox
startPos = el.selectionStart;
endPos = el.selectionEnd;
selection = el.value.substring(startPos, endPos);
}
if (!selection.length){
return;
}
if (el.value.substring(startPos-markerLength,startPos) === marker
&& el.value.substring(endPos,endPos+markerLength) === marker
){
el.value = el.value.substring(0,startPos-markerLength) +
selection +
el.value.substring(endPos+markerLength);
}
else{
el.value = el.value.substring(0,startPos) + marker +
selection + marker + el.value.substring(endPos);
}
}
$(document).on('mousedown', 'button', function(e) {
toggleMarker( $(this).data('marker'), $('#txtarea').get(0) ).text;
});
See it in action: https://jsfiddle.net/t4ro53v8/4/
The solution takes a very generic approach: the marker to toggle is set as a custom data attribute to make it easy to reuse the code.
The functionality is only implemented for the non-IE case. You will have to check, how to determine startPos and endPos for a range in IE.
In all other browsers:
the selection is identified
nothing is done if nothing is selected
sourroundings of the selection are checked against the given marker
if both markers are present, they get deleted
otherwise the markers are inserted
As a proof of concept this example works like a charm.
But there are some shortcomings:
How to distinguish between bold text(**) and italics(*)?
How to handle markers that just appear just on one side of the selection
What to do, if a marker is selected?
But that is for you to solve now ...
You could use regex to find the occurance of a ** ** pattern.This regex will help you find the pattern similar to what you have.
[*][*][a-z]*[*][*] .
Using the exec() method, will help you extract that particular text.
Check the length of this using .length, if it is 4, then there is nothing in between, and you can replace it with the new text surrounded by **,
"**"+ newtext+"**"
For removing the **, you can use the replace() method, where you replace ** with whitespace or so.
I have seen a few questions about this on StackOverflow, but it seems hard to find a jQuery-based solution. Hence, I would like to ask this question.
I would like to replace text on-the-fly inside a div with the attribute contenteditable="true".
I'm looking for a jQuery-based solution that will do the following:
Automatically replace written text on-the-fly (as you type)
Being able to continue writing (whilst the replacement is being done)
I looked at SC Editor (http://www.sceditor.com/), it seems like it does exactly that (for instance, if you try type :) it gets replaced by an emoticon.
I think a good start would be an array with all the elements to replace:
$.settings = {
path: 'https://example.com/images/',
emoticons: {
':(' : 'stupid.jpg',
':)' : 'smart.jpg',
}
}
I have been unable to find good examples of this. Would be happy if somebody can share their thoughts and any code regarding this.
How would the replacement get done in the best possible way, with the above code?
I found this. If you adjust it, it might suit your needs. It replaces { with {} and ( with () and the cursor ends up in the middle.
<script type="text/javascript">
$(document).ready(function () {
$("#d").keypress(function (e) {
var charTyped = String.fromCharCode(e.which);
if (charTyped == "{" || charTyped == "(") {
// Handle this case ourselves
e.preventDefault();
var sel = window.getSelection();
if (sel.rangeCount > 0) {
// First, delete the existing selection
var range = sel.getRangeAt(0);
range.deleteContents();
// Insert a text node with the braces/parens
var text = (charTyped == "{") ? "{}" : "()";
var textNode = document.createTextNode(text);
range.insertNode(textNode);
// Move the selection to the middle of the text node
range.setStart(textNode, 1);
range.setEnd(textNode, 1);
sel.removeAllRanges();
sel.addRange(range);
}
}
});
});
</script>
</head>
<body>
<div id="d" contentEditable="true">....</div>
</body>
</html>
$('div').keyup(function(){
//make here for loop which replace all emoticons
$(this).text().replace(':(', 'stupid.jpg');
});
Posting what I eventually wrote after failing to find an answer to this question. I hope that it will be helpful to someone else who comes to this question looking for an answer (:
I'm going to post a much more general find and replace solution (contained in a class). This is for content editable divs and works while the user is typing, additionally it does not affect the caret position. This implementation uses a case insensitive search (although it would be trivial to disable this in the code). Another advantage this has is that it will work even if you are typing in the middle of a paragraph (not just at the end of the line) and will work on pasted text. Give it a go!
class FindAndReplace {
constructor($contentEditable, findAndReplaceData) {
var self = this;
$contentEditable.on('input blur', function () {
var textNodes = self.getTextNodes($contentEditable);
textNodes.each(function (i) {
// Perform all replacements on text
findAndReplaceData.forEach(function (findAndReplaceDatum) {
var find = findAndReplaceDatum.find;
var replace = findAndReplaceDatum.replace;
var regexEscapedFind = self.escapeRegExp(find);
var regexEscapedReplace = self.escapeRegExp(replace);
var regexEscapedCaseInsensitiveFind = self.makeRegexCaseInsensitive(regexEscapedFind);
// Case insensitive search for the find with a negative lookahead to check its not a case sensitive match of the replacement (aka to check its actually going to make a difference)
var regexString = `(?!${regexEscapedReplace})${regexEscapedCaseInsensitiveFind}`;
do {
// Get the latest version of the text node
textNode = self.getTextNodes($contentEditable)[i];
var text = textNode.data;
var regex = new RegExp(regexString);
var matchIndex = text.search(regex);
var matchFound = (matchIndex !== -1);
if (matchFound) {
// Select the match
var range = document.createRange();
range.setStart(textNode, matchIndex);
range.setEnd(textNode, matchIndex + find.length);
// Delete it
range.deleteContents();
// Create the replacement node
var textNode = document.createTextNode(replace);
// Insert it
range.insertNode(textNode);
// Set the range to the end of the selected node
range.collapse(false);
// Set the user selection the range
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
// Make sure there a no adjacent or empty text nodes
$contentEditable[0].normalize();
}
} while (matchFound)
});
});
});
}
escapeRegExp(string) {
// https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
getTextNodes($contentEditable) {
return $contentEditable.contents().filter(function () {
return this.nodeType == 3; // Text node
});
}
makeRegexCaseInsensitive(string) {
var stringArray = string.split('');
stringArray = stringArray.map(function (char) {
if (char.toLowerCase() !== char.toUpperCase())
return '[' + char.toLowerCase() + char.toUpperCase() + ']';
else
return char;
});
return stringArray.join('');
}
}
div {
border: 1px solid black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script>
$(function(){
var findAndReplaceData = [
{
'find': 'find me',
'replace': 'and replace with me!'
},
{
'find': 'foo',
'replace': 'bar'
},
{
'find': 'no',
'replace': 'yes'
}
];
$contentEditable = $('div');
new FindAndReplace($contentEditable,findAndReplaceData);
});
</script>
<div contenteditable="true"></div>
I'm trying to make a simple syntax highlighter with JavaScript, but I always end up having the same problem. The program works as follows: as the user types enter (without the shift key) the program will replace the keyword var with another one with red color (this is still so basic). The problem is that whenever you press enter, the text gets highlighted but the cursor returns to the first word of the first line. How do you think I can prevent this from happening?
<div class="container">
<pre class="text"><code contenteditable="true" id="format">
</code></pre>
</div>
JS
var editor = document.getElementById('format');
var npatt = / *var +/igm
editor.addEventListener('keyup', highlight);
function highlight(e){
var content = editor.innerHTML;
if(e.which === 13 && e.shiftKey===false){
editor.innerHTML = content.replace(npatt, '<span style="color:red">var</span> ');
console.log(editor.innerHTML);
}
}
Moving the cursor to the end of a contenteditable element can be done according to the method in this answer. That approach uses the window.getSelection() method to find the cursor position.
I made a few changes to your code.
Added a test check to see if the regular expression even matches the content to avoid calling replace and setting editor.innerHTML on every Enter keystroke as the original code did.
Added a call to the cursorManager.setEndOfContenteditable method (from the answer referenced above) to reset the cursor to the end of the editor after the replace operation.
Here is the updated code.
var editor = document.getElementById('format');
var npatt = / *var +/igm;
editor.addEventListener('keyup', highlight);
function highlight(e){
var content = editor.innerHTML;
if(e.which === 13 && e.shiftKey === false && npatt.test(content)) {
editor.innerHTML = content.replace(npatt, '<span style="color:red">var</span> ');
cursorManager.setEndOfContenteditable(editor);
}
}
And here is a working example.
var editor = document.getElementById('format');
var npatt = / *var +/igm;
editor.addEventListener('keyup', highlight);
function highlight(e){
var content = editor.innerHTML;
if(e.which === 13 && e.shiftKey === false && npatt.test(content)) {
editor.innerHTML = content.replace(npatt, '<span style="color:red">var</span> ');
cursorManager.setEndOfContenteditable(editor);
}
}
//Code to set the cursor position modified from this answer: https://stackoverflow.com/a/19588665/830125
//Namespace management idea from http://enterprisejquery.com/2010/10/how-good-c-habits-can-encourage-bad-javascript-habits-part-1/
(function( cursorManager ) {
//From: http://www.w3.org/TR/html-markup/syntax.html#syntax-elements
var voidNodeTags = ['AREA', 'BASE', 'BR', 'COL', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'MENUITEM', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR', 'BASEFONT', 'BGSOUND', 'FRAME', 'ISINDEX'];
//From: https://stackoverflow.com/questions/237104/array-containsobj-in-javascript
Array.prototype.contains = function(obj) {
var i = this.length;
while (i--) {
if (this[i] === obj) {
return true;
}
}
return false;
}
//Basic idea from: https://stackoverflow.com/questions/19790442/test-if-an-element-can-contain-text
function canContainText(node) {
if(node.nodeType == 1) { //is an element node
return !voidNodeTags.contains(node.nodeName);
} else { //is not an element node
return false;
}
};
function getLastChildElement(el){
var lc = el.lastChild;
while(lc && lc.nodeType != 1) {
if(lc.previousSibling)
lc = lc.previousSibling;
else
break;
}
return lc;
}
//Based on Nico Burns's answer
cursorManager.setEndOfContenteditable = function(contentEditableElement)
{
var range,selection;
if(document.createRange)//Firefox, Chrome, Opera, Safari, IE 9+
{
range = document.createRange();//Create a range (a range is a like the selection but invisible)
range.selectNodeContents(contentEditableElement);//Select the entire contents of the element with the range
range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start
selection = window.getSelection();//get the selection object (allows you to change selection)
selection.removeAllRanges();//remove any selections already made
selection.addRange(range);//make the range you have just created the visible selection
}
else if(document.selection)//IE 8 and lower
{
range = document.body.createTextRange();//Create a range (a range is a like the selection but invisible)
range.moveToElementText(contentEditableElement);//Select the entire contents of the element with the range
range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start
range.select();//Select the range (make it the visible selection
}
}
}( window.cursorManager = window.cursorManager || {}));
<div class="container">
<pre class="text"><code contenteditable="true" id="format">
</code></pre>
</div>
Assuming the cursor position in a tinymce editor is inside a paragraph.
When a user hits backspace i need to know which character will get deleted.
It is necessary to know this before the character gets removed (onKeyDown is ok, onKeyUp is too late).
How can i find out which character gets deleted on backspace (before it actually gets deleted)?
The code above doesn't take into account backspacing in the middle of a paragraph, or backspacing a whole selection. Try something like the a-tools plugin (although there are several others like it) in combination with the following event handler:
jQuery('input, textarea').keydown(function(e) {
if(e.keyCode === 8) {
var selection = jQuery(this).getSelection();
var selStart = (selection.length) ? selection.start : selection.start - 1;
var selEnd = selection.end;
alert(jQuery(this).val().slice(selStart, selEnd));
}
});
in one of my plugins i set onKeyDown
ed.onKeyDown.add(function(ed, evt) {
if (paragraph && evt.keyCode == 8 && ed.selection.isCollapsed()) {
//insert special marker char
var value = '<span id="__ircaret" class="ircaret">\u2060</span>';
ed.selection.setContent(value, {format : 'raw', no_events: 1});
// node is the dom node the caret is placed in
var node = ed.selection.getNode();
var node_content = $(node).text();
var position = node_content.search('\u2060');
// this is the character
var char_vor_cursor = position != 0 ? node_content.slice(position - 1, position) : '';
// Test for soft-hyphen
if (char_vor_cursor != '' && char_vor_cursor.charCodeAt(0) == 173) {
// correct innerHTML
var text_after_backspace = node_content.slice(0, position - 1) + '<span id="__ircaret" class="ircaret">\u2060</span>' + node_content.slice(position + 1);
node.innerHTML = text_after_backspace;
}
var caret_node = $(node).find('#__ircaret').get(0);
// select caretnode and remove
ed.selection.select(caret_node);
$(ed.getBody()).find('.ircaret').remove();
}
}