Draft-js: auto line break - javascript

I'm trying to implement an editor using facebook's Draft-JS framework which will be limited to a 50 characters line.
I don't want the lines to break using css, I want them to really break into separate blocks when reaching this 50 characters limit.
The line break should happen at the last space before the word exceeding the 50th character (similar to word-wrap: break-word in css).
Surely, I need the selection to stay at the end of the new line.
I'm not sure where to start with this. Any ideas?
I've created a draft-js codepen with a preview of the current editorState for easy start, just not sure where to start: https://codepen.io/adamtal/pen/pbqVrL?editors=0010
Update:
As a response to Jiang YD's answer, I don't think it's a good idea to take the text from the end of the block and create a new block with it.. Not sure how well the formatting state will be kept. I think the solution should use Modifier.splitBlock with some selection manipulation maybe.

I fought with this same issue where I had a split a block and had to give the new split block some metadata.
Getting the key of the next block that's been created after the split was the tricky part. Here's how I got it done.
const selection = editorState.getSelection();
let contentState = editorState.getCurrentContent();
contentState = Modifier.splitBlock(contentState, selection);
const currentBlock = contentState.getBlockForKey(selection.getEndKey());
const nextBlock = contentState
.getBlockMap()
.toSeq()
.skipUntil(function(v) {
return v === currentBlock;
})
.rest()
.first();
const nextBlockKey = nextBlock.getKey();
const nextBlockEmptySelection = new SelectionState({
anchorKey: nextBlockKey,
anchorOffset: 0,
focusKey: nextBlockKey,
focusOffset: 0
});
contentState = Modifier.setBlockData(
contentState,
nextBlockEmptySelection,
Map()
.set("data1", "Hello this is block data")
.set("data2", 3)
);

Related

Modifying how text is copied from a web page where whitespace has been rendered visible

I've been working on creating an HTML parser and formatter, and I've just added a feature to optionally render whitespace visible, by replacing spaces with · (middle dot) characters, adding arrows for tabs and newlines, etc.
The full in-progress source code is here: https://github.com/kshetline/html-parser, and the most relevant file that's doing the CSS styling of HTML is here: https://github.com/kshetline/html-parser/blob/master/src/stylizer.ts.
While it's nice to be able to visualize whitespace when you want to, you wouldn't want spaces to be turned into middle dot characters if you select and copy the text. But that's just what happens, at least without some intervention.
I have found a crude way to fix the problem with a bit of JavaScript, which I've put into a Code Pen here: https://codepen.io/kshetline/pen/NWKYZJg.
document.body.addEventListener('copy', (event) => {
let selection = document.getSelection().toString();
selection = selection.replace(/·|↵\n|↵/g, ch => ch === '·' ? ' ' : '\n');
event.clipboardData.setData('text/plain', selection);
event.preventDefault();
});
I'm wondering, however, if there's a better way to do this.
My first choice would be something that didn't rely on JavaScript at all, like if there's some way via CSS or perhaps some accessibility-related HTML attribute that would essentially say, "this is the real text that should be copied, not what you see on the screen".
My second choice would be if someone can point me to more detailed documentation of the JavaScript clipboard feature than I've been able to find, because if I have to rely on JavaScript, I'd at least like my JavaScript to be smarter. The quick-and-dirty solution turns every middle dot character into a space, even if it was truly supposed to be a middle dot in the first place.
Is there enough info in the clipboard object to figure out which of the selected text has what CSS styling, so I could know to convert only the text that's inside <span>s which have my whitespace class, and still also find the rest of the non-whitespace text, in proper order, to piece it all back together again?
I still couldn't find much documentation on how selection objects work, but I played around with them in the web console, and eventually figured out enough to get by.
This is the JavaScript I came up with:
function restoreWhitespaceStrict(s) {
return s.replace(/·|[\u2400-\u241F]|\S/g, ch => ch === '·' ? ' ' :
ch.charCodeAt(0) >= 0x2400 ? String.fromCharCode(ch.charCodeAt(0) - 0x2400) : '');
}
const wsReplacements = {
'·': ' ',
'→\t': '\t',
'↵\n': '\n',
'␍\r': '\r',
'␍↵\r\n': '\r\n'
}
function restoreWhitespace(s) {
return s.replace(/·|→\t|↵\n|␍\r|␍↵\r\n|→|↵|␍|[\u2400-\u241F]/g, ws =>
wsReplacements[ws] || (ws.charCodeAt(0) >= 0x2400 ? String.fromCharCode(ws.charCodeAt(0) - 0x2400) : ''));
}
document.body.addEventListener('copy', (event) => {
const selection = document.getSelection();
let newSelection;
let copied = false;
if (selection.anchorNode && selection.getRangeAt) {
try {
const nodes = selection.getRangeAt(0).cloneContents().childNodes;
let parts = [];
// nodes isn't a "real" array - no forEach!
for (let i = 0; i < nodes.length; ++i) {
const node = nodes[i];
if (node.classList && node.classList.contains('whitespace'))
parts.push(restoreWhitespaceStrict(node.innerText));
else if (node.localName === 'span')
parts.push(node.innerText);
else
parts.push(node.nodeValue);
}
newSelection = parts.join('');
copied = true;
}
catch (err) {}
}
if (!copied)
newSelection = restoreWhitespace(selection.toString());
event.clipboardData.setData('text/plain', newSelection);
event.preventDefault();
});
I've tried this on three browsers (Chrome, Firefox, and Safari), and it's working on all of them, but I still took the precaution of both testing for the presence of some of the expected object properties, and then also using try/catch, just in case I hit an incompatible browser, in which case the not-so-smart version of fixing the clipboard takes over.
It looks like the selection is handled as a list of regular DOM nodes. Chrome's selection object has both an anchorNode and an extentNode to mark the start and end of the selection, but Firefox only has the anchorNode (I didn't check Safari for extentNode). I couldn't find any way to get the full list of nodes directly, however. I could only get the full list using the cloneContents() method. The first and last nodes obtained this way are altered from the original start and end nodes by being limited to the portion of the text content that was selected in each node.

Implement a text highlighting feature in javascript

I have a task to create API using ExpressJS that will manage highlights which will be made on the frontend. How can I keep track of my highlighted text if someone updates a part of the text?
I was keeping the three starting and ending characters of the highlighted text. But a problem is, how will I manage those characters if the text is edited.
const { textH, allText } = req.body;
let chars = { };
const enclosingChars = (hLighted, theString) => {
let startingChars = null, endingChars = null;
const index = theString.indexOf(hLighted);
const last3 = index + hLighted.length;
if ((index - 3) > -1) startingChars = theString.substring(index - 3, index);
if ((index + 3) <= theString.length) endingChars = theString.substring(last3, last3 + 3);
return { startingChars, endingChars };
};
if (allText.includes(textH)) {
chars = enclosingChars(textH, allText);
}
chars.hLighted = textH;
If a part of the highlighted text is edited, I will delete the highlighted in my storage. If not, I want to check if my starting and ending characters have changed, then I change them accordingly.
But I don't know how to get that highlighted text if its index changed
so it will be not as easy as you think if you consider keeping track of edits outside of the highlighted text.
the text might contain more than one similar phrase/word (to the highlighted)
the newly inserted phrase might be similar to the highlighted one (worse if before the main one in terms of indices)
so if any of the above scenarios happens the indices will be of no use because you can fall to the wrong text
Do the following
a. when highlighting keep the following
1. the content : to make sure if the edit happens in there, you know it and you can act accordingly
2. keep track of the index range of your text even in case the shift is required (when the text before it is edited)
3. take a screenshot of the entire article and store it so that when an edit to the article happens you know exactly which part is edited. this will require to check word by word and see if any word in the new text is different from the word of the same index in the old (screenshot). and you can shift the ranges of the highlighted text accordingly.
Remember any edit that happens after the highlighted text is innocuous.
Hope this helps.

Javascript Editing Text file - Remove all empty lines but one

I just started learning Javascript and I am trying to implement a project that came into my mind. The project is a simple text editor that takes a text as an input does some corrections and posts an output with the fixed text.
The input text is imported as an array. Each line of the text is an object property. The inputText is for displaying the default text in the browser inside a div, and the outputText is the one I use for the editing.
inputText = loadStrings("v.txt");
outputText = inputText;
Now the input text has some sentences. The problem is that there are a lot of empty lines in between.
firstLine
secondLine
thirdLine
What I want to achieve, is to remove all empty lines, but one, so the output text looks like:
firstLine
secondLine
thirdLine
I am searching for a solution for almost 3 days now. I have managed to remove ALL empty lines, but that's not what i want. I want to remove empty lines if they are more than one, but don't make any change if there is only one empty line.
This piece of code is one of many i have tried. This will remove ALL empty lines:
(source: Remove empty elements from an array in Javascript )
outputText = outputText.filter(function(e){
return e.replace(/[\r\n]+/g, '\n')});
Here is the codepen with all HTML, CSS and the JS code I am using.
https://codepen.io/theokondak/pen/KrNNVz
Try this regex:
e.replace(/([\r\n]){2,}/g, '\n\n')});
This will only match two or more consecutive empty lines replacing by an empty line.
My test code:
var lines = 'firstLine\n\n\n\nsecodLine\n\n\n\n\n\n\nthirdLine';
/*
firstLine
secondLine
thirdLine
*/
console.log(lines.replace(/([\r\n]){2,}/g, '\n\n'));
/*
firstLine
secodLine
thirdLine
*/
Try:
outputText = outputText.filter(function(e){
return e.replace(/[\r\n]+/g, '\n\n')});
I am not sure about the filter function above. but try below one must work.
outputText = outputText.replace(/[\r\n]+/g, '\n\n');
replacing with two new lines is the simplest way. you will get what you want.
I don't know if loadStrings leaves newlines at the end of the line or not. I'll assume it does (based on some code I see on this page). If it ends up mangled, please say so.
It is definitely easier to do this on a single string, like Hélio Márcio Filho says. So in your case, where you start with an array, you could just join the array together,replace three or more newlines with just two, then split it back up into lines:
let outputText = inputText.join('').replace(/(?:\r?\n){3,}/g, '\n\n').
split(/\n/).map(line => line + "\n");
But you can also do it just with filter - you just need to know how many empty lines you just saw, and suppress the extra ones:
let empties = 0;
let outputText = inputText.filter(line => {
if (line.replace(/[\r\n]+/, '')) empties = 0;
else empties++;
return empties <= 1;
});
The other answers work with a single string, but you're using the loadStrings() function from Processing.js, which gives you an array of strings.
You could convert the array into a single string, or load the file as a single string, but it seems simplest to me if you just process the array. Something like this:
function reduceNewlines(inputArray){
var outputNewArray = [];
var previousLineWasNewline = false;
for(var i = 0; i < myArray.length; i++){
if(myArray[i] == ''){
if(!previousLineWasNewline){
outputArray.push('');
}
previousLineWasNewline = true;
}
else{
outputArray.push(myArray[i]);
previousLineWasNewline = true;
}
}
return outputArray;
}
Please note that I haven't tested this code, and there is probably a ton of room for improvement. But the general idea is there: you could write a function that processed the array and returned a new array with the consecutive newlines removed.
You might also consider pre-processing your text file so you don't have to do this at all.
First of all, I would like to thank everyone for trying to help. I studied all your replies, I've tested each of them and they all work (some with needed tweaking) as stand-alone code, but when I inserted them into my code, some things went wrong. So I struggled and came out with my own solution, which is greatly inspired by your comments.
So the code that worked in my case is :
function reduceNewlines(outputTextEditNewLines) {
for (let key = 0; key < outputTextEditNewLines.length; key++) {
if (outputTextEditNewLines[key] == '') outputTextEditNewLines[key] = '\n';
else outputTextEditNewLines[key] = outputTextEditNewLines[key];
}
arrayToString = outputTextEditNewLines.join(""); // convert object myArray to string
console.log(arrayToString.replace(/([\r\n]){1,}/g, '\n\n')); // exports the text as it should
return arrayToString.replace(/([\r\n]){1,}/g, '\n\n');
}
The console.log exports the text as it should. Now the next step for my project is to make this string print to the DOM as it prints in the console.log.

CSS transition lost when array changes

I have a simple react app that shifts and unshifts an array of letters. The transition animation occurs when a user hits next and back button. Functionally, the array is properly changed, but the transition only works for next. I have a hunch that this may be an issue more basic than React, but I did make sure that the key is unique to prevent redraw.
// this is the issue.
clickLeftRightHandler = () => {
const { list } = this.state;
// Does using slice or shifting the array cause a new redraw? Is it
the CSS?
const newList = [list[list.length-1], ...list.slice(0, -1)];
this.setState({list : newList});
}
Code Link:
https://stackblitz.com/edit/react-7nsrjg
Any help is appreciated!
Just use the unshift method:
clickLeftRightHandler = () => {
const { list } = this.state;
const newList = list.slice(0, -1);
newList.unshift(list[list.length-1]); // <-- right here
this.setState({list : newList});
}
working example
edit
I honsetly don't know why it works, but it seems like if the string is a bit longer, the animation works as you want it to, so this works:
newList.unshift(list[list.length-1]+' ');
example. I don't know why's that happening, truly.
It turns out that the reason the animation wasn't working properly was due to the key being supplied to the Alphabet Component. The solution adds an index state that makes sure the shifted key receives a new key that's different from the cycled key.
It should also be noted that, #yuvi's updated example also implicitly fixes the issue with a new string that'll be passed to the key causing it to be uniquely set.
See updated example

Regex to search html return, but not actual html jQuery

I'm making a highlighting plugin for a client to find things in a page and I decided to test it with a help viewer im still building but I'm having an issue that'll (probably) require some regex.
I do not want to parse HTML, and im totally open on how to do this differently, this just seems like the the best/right way.
http://oscargodson.com/labs/help-viewer
http://oscargodson.com/labs/help-viewer/js/jquery.jhighlight.js
Type something in the search... ok, refresh the page, now type, like, class or class=" or type <a you'll notice it'll search the actual HTML (as expected). How can I only search the text?
If i do .text() it'll vaporize all the HTML and what i get back will just be a big blob of text, but i still want the HTML so I dont lose formatting, links, images, etc. I want this to work like CMD/CTRL+F.
You'd use this plugin like:
$('article').jhighlight({find:'class'});
To remove them:
.jhighlight('remove')
==UPDATE==
While Mike Samuel's idea below does in fact work, it's a tad heavy for this plugin. It's mainly for a client looking to erase bad words and/or MS Word characters during a "publishing" process of a form. I'm looking for a more lightweight fix, any ideas?
You really don't want to use eval, mess with innerHTML or parse the markup "manually". The best way, in my opinion, is to deal with text nodes directly and keep a cache of the original html to erase the highlights. Quick rewrite, with comments:
(function($){
$.fn.jhighlight = function(opt) {
var options = $.extend($.fn.jhighlight.defaults, opt)
, txtProp = this[0].textContent ? 'textContent' : 'innerText';
if ($.trim(options.find.length) < 1) return this;
return this.each(function(){
var self = $(this);
// use a cache to clear the highlights
if (!self.data('htmlCache'))
self.data('htmlCache', self.html());
if(opt === 'remove'){
return self.html( self.data('htmlCache') );
}
// create Tree Walker
// https://developer.mozilla.org/en/DOM/treeWalker
var walker = document.createTreeWalker(
this, // walk only on target element
NodeFilter.SHOW_TEXT,
null,
false
);
var node
, matches
, flags = 'g' + (!options.caseSensitive ? 'i' : '')
, exp = new RegExp('('+options.find+')', flags) // capturing
, expSplit = new RegExp(options.find, flags) // no capturing
, highlights = [];
// walk this wayy
// and save matched nodes for later
while(node = walker.nextNode()){
if (matches = node.nodeValue.match(exp)){
highlights.push([node, matches]);
}
}
// must replace stuff after the walker is finished
// otherwise replacing a node will halt the walker
for(var nn=0,hln=highlights.length; nn<hln; nn++){
var node = highlights[nn][0]
, matches = highlights[nn][1]
, parts = node.nodeValue.split(expSplit) // split on matches
, frag = document.createDocumentFragment(); // temporary holder
// add text + highlighted parts in between
// like a .join() but with elements :)
for(var i=0,ln=parts.length; i<ln; i++){
// non-highlighted text
if (parts[i].length)
frag.appendChild(document.createTextNode(parts[i]));
// highlighted text
// skip last iteration
if (i < ln-1){
var h = document.createElement('span');
h.className = options.className;
h[txtProp] = matches[i];
frag.appendChild(h);
}
}
// replace the original text node
node.parentNode.replaceChild(frag, node);
};
});
};
$.fn.jhighlight.defaults = {
find:'',
className:'jhighlight',
color:'#FFF77B',
caseSensitive:false,
wrappingTag:'span'
};
})(jQuery);
If you're doing any manipulation on the page, you might want to replace the caching with another clean-up mechanism, not trivial though.
You can see the code working here: http://jsbin.com/anace5/2/
You also need to add display:block to your new html elements, the layout is broken on a few browsers.
In the javascript code prettifier, I had this problem. I wanted to search the text but preserve tags.
What I did was start with HTML, and decompose that into two bits.
The text content
Pairs of (index into text content where a tag occurs, the tag content)
So given
Lorem <b>ipsum</b>
I end up with
text = 'Lorem ipsum'
tags = [6, '<b>', 10, '</b>']
which allows me to search on the text, and then based on the result start and end indices, produce HTML including only the tags (and only balanced tags) in that range.
Have a look here: getElementsByTagName() equivalent for textNodes.
You can probably adapt one of the proposed solutions to your needs (i.e. iterate over all text nodes, replacing the words as you go - this won't work in cases such as <tag>wo</tag>rd but it's better than nothing, I guess).
I believe you could just do:
$('#article :not(:has(*))').jhighlight({find : 'class'});
Since it grabs all leaf nodes in the article it would require valid xhtml, that is, it would only match link in the following example:
<p>This is some paragraph content with a link</p>
DOM traversal / selector application could slow things down a bit so it might be good to do:
article_nodes = article_nodes || $('#article :not(:has(*))');
article_nodes.jhighlight({find : 'class'});
May be something like that could be helpful
>+[^<]*?(s(<[\s\S]*?>)?e(<[\s\S]*?>)?e)[^>]*?<+
The first part >+[^<]*? finds > of the last preceding tag
The third part [^>]*?<+ finds < of the first subsequent tag
In the middle we have (<[\s\S]*?>)? between characters of our search phrase (in this case - "see").
After regular expression searching you could use the result of the middle part to highlight search phrase for user.

Categories