Angular 2 sentence highlighting on click event - javascript

So I use PDF.js to render pdf to html. On top there is a text layer.
What I want to implement is that when you click on a sentence there will be a class added to this sentence.And I want to do this in Angular 4 Component.
I have stumbled upon a problem here because the pdf is rendered to html by lines(every line is in a different div).
Example of pdf in html:
<div style="left: 86.0208px; top: 481.589px; font-size: 8.03709px; font-
family: serif; transform: scaleX(1.00581);">
timestamp server to generate computational proof of the chronological
order of transactions. The
</div>
<div style="left: 86.0208px; top: 490.899px; font-size: 8.03709px; font-
family: serif; transform: scaleX(0.9335);">
system is secure as long
as honest nodes collectively control more CPU
power than any
</div>
Any idea how should I implement this functionality?
Main goal is to highlight the exact sentence what is clicked and doing it by
manipulating html.

Here's an approach that highly depends on how your html looks like and for which part you want to implement the sentence highlighting. If it's just one text block with multiple lines on top of the page and nothing more, I'd say that you can replace the whole block with an updated HTML block, maybe even a single <p>.
combining all to one big string
You should find this part in the HTML created by PDF.js, iterate over all the child divs and combine every text part of it to one big string by adding it up all together, just string concatenation. One problem might be the access of the child divs. If the HTML is rendered by an angular application, you can reference DOM elements by giving them attribute names like #textBlock. Then you can access those elements with #ViewChild which brings some fancy functions with it to walk down an elements subtree like childNodes and data. This may be helpful to extract the text and concatenate the string.
split the text into sentences
Next thing to do is split this big text block string into sentences. Having fixed punctuation marks like . ! ? we can use someting like a regular expression to split it on the right spot. The string function replace in combination with a regular expression should do the job here. As a result we want to have an array of sentences. The regex may look something likes this, also I'm not 100% if it works, because I just found it in this answer:
var bigTextBlock="Big text block. No more divs. Only a string";
var sentences = bigTextBlock.match( /[^\.!\?]+[\.!\?]+/g );
remove the current divs
Now that's not too bad for a start. We now want to remove the current divs and create new html tags. There are multiple ways to do this. In both cases we might need to have a reference to the parent div of the text block divs from before, that we probably already have.
First option is to set something like [innerHTML]. This removes the old divs and creates new ones, but gets tricky when you want to implement an onclick action, because this way we bypass angular.
The other way is to manipulate its children through your reference element. For this we can use a so called Renderer2 that is injected as a service. You can do different stuff with it like creating new tags, removing children and also creating onClick listeners on nodes, which is what we probably need to do anyway. For now we only want to remove the old childNodes.
create adjusted html
As we now have every sentence isolated, we can create one big <p>div that contains a <span> div for every sentence that we have. This way we can give the span just another css class if the user clicks inside of this text part and therefore having a highlight for every sentence. As stated before the html could be placed through [innerHTML] or by creating them as children of our reference. In both cases we need to use Renderer2 to make the <span> listen to an onclick action. Here's some code that combines the span creation and adding the listener both through Renderer2.
#ViewChild('textBlock') textBlock: ElementRef;
constructor(private renderer: Renderer2, private router: Router) { }
createSpans(sentences: string[]){
sentences.forEach(sentence=>{
// create elements
const span = this.renderer.createElement('span');
const spanText = this.renderer.createText(sentence);
// append the sentence to the span div
this.renderer.appendChild(span, spanText);
// append the span div to the parent
this.renderer.appendChild(this.textBlock.nativeElement, span);
// listen to the onClick
this.renderer.listen(span, 'click', (event) => {
// set a highlight class
span.class.highlighted = true;
});
});
}
I know this is a lot to do and it gets tricky at some parts, but this is probably how I would handle it. But again it depends highly on how your HTML currently looks like and how you want it to look like after the changes.

Related

How to create editable inline elements in CKEditor5 with actual nodes not text attributes

We work with a backend that tags words like Persons or locations. I send text to the backend and it tags me words. I have to create tags in the ckeditor frontend.
So I have the task to create editable inline elements, means they should be addable to where text is addable.
The tag must be editable.
The tag should also be visible if its empty (should be a own tree node not text attribute).
The tag should have visual highlighting, like a background-color and border radius.
If the tag is empty, its type should be added via css' ::before pseudo class and content property.
It would be nice if you can toggle it's edtiable part. Since we also need readonly tags, their are developed as seperate elements so far.
My approach so far was using text attributes applying:
writer.createText('my tag', {tag: tagType})
I was basically creating tags like you would create bold text. I was applying css styles like background-color and border radius to make it look like a tag, but this approach came to it's limits. Also with this approach you cannot have editable and non-editable tags be the same ckeditor entity, since you cannot have non-editable text, I guess.
Then I found editableElement's in the view side. The Problem is you cannot have emtpy tags since empty text is nothing. You also cannot modify the "tag" at index 0 because then you are outside of the tag, see bold behavior for this. I mean I could somehow fix it all, but I would like the tags to be their own element on model side. so I have tried this approach:
// in editingDowncast conversion:
viewWriter.createEditableElement('div', {class: 'inline'})
// this is the whole code in the ui:
this.editor.model.schema.register( 'test-tag', {
allowChildren: '$text',
allowWhere: '$text',
allowAttributesOf: '$text',
isObject: true
});
// if it is isInline: true it behaves mostly like my approach with text attributes
this.editor.conversion.for('editingDowncast').elementToElement({
model: 'test-tag',
view: (modelItem, conversionApi) => {
let { writer: viewWriter } = conversionApi;
const tagView = viewWriter.createEditableElement( 'div', {
class: 'inline'
});
return tagView;
}
})
basically EdtiableElement's work only with block elements, so I have tried to make them inline via css, setting their display property to inline-block. Here I have again the problem that when the element is empty you cannot access it anymore via cursor. So it will stay empty forever. Generelly it seems that it's behavior is kind of buggy because I guess you should not use it as inline. Basically I have many similiar issues like with the approach above.
I will keep implementing it with the first solution but I wanted to ask the community if there is any other way, maybe a less hacky way to create inline editable elements that are actual nodes in the model. Something like a span tag but on model side.
Any Ideas?

How to find a unique string within html and wrap it with a tag, but exclude links and urls

I'm looking for a way to look for a specific string within a page in the visible text and then wrap that string in <em> tags. I have tried used HTML Agility Pack and had some success with a Regex.Replace but if the string is included within a url it also gets replaced which I do not want, if it's within an image name, it gets replaced and this obviously breaks the link or image url.
An example attempt:
var markup = Encoding.UTF8.GetString(buffer);
var replaced = Regex.Replace(markup, "product-xs", " <em>product</em>-xs", RegexOptions.IgnoreCase);
var output = Encoding.UTF8.GetBytes(replaced);
_stream.Write(output, 0, output.Length);
This does not work as it would replace a <a href="product/product-xs"> with <a href="product/<em>product</em>-xs"> - which I don't want.
The string is coming from a text string value within a CMS so the user can't wrap the words there and ideally, I want to catch all instances of the word that are already published.
Ideally I would want to exclude <title> tags, <img> tags and <a> tags, everything else should get the wrapped tag.
Before I used the HTML Agility Pack, a fellow front end dev tried it with JavaScript but that had an unexpected impact on dropdown menus.
If you need any more info, just ask.
You can use HTML Agility Pack to select only the text nodes (i.e. the text that exists between any two tags) with a bit of XPath and modify them like this.
Looking only in body will exclude <title>, <meta> etc. The not excludes script tags, you can exclude others in the same way (or check the parent node in the loop).
foreach (HtmlNode node in htmlDoc.DocumentNode.SelectNodes("//body//*[not(self::script)]/text()"))
{
var newNode = htmlDoc.CreateTextNode(node.InnerText.Replace("product-xs", "<em>product</em>-xs"));
node.ParentNode.ReplaceChild(newNode, node);
}
I've used a simple replace, regex will work fine too, prob best to check the performance of each approach and choose which works best for your use case.

Replacing text with javascript?

After much trial an error and some progress I still can't mange to change every instance of ,- with kr on my website.
I'm very much a beginner at JS and have pieced together the following code from several sources. Is the code the problem or something else?
function skrivkr() {
var skrivkr = $('div').text().replace(/\,-/g, 'kr');
$('p').html(skrivkr);
}
window.onload = skrivkr();
Update:
Thanks for the replies. The site loads jquery 1.10.7.
#Niet the Dark Absol: No, I don't want to put anything in p elements. How do I remove that part? I just want to find all ,- and simply replace with kr without changing any formatting.
Update
OK! Progress, kind of. The ENTIRE content of every <strong> and <dd> now changes to (0), instead of kr. With the odd exception of those tags including ,-. I haven't designed the site myself.
If it helps, one of the ,- appears in the following markup:
<a href="xxxx" rel="nofollow">
<span class="amount">1</span>
<span class="photo">
<img src="xxxx" alt="product name" width="62" height="42">
</span>
<span class="description">
Prtoduct name
<strong>4444,-</strong>
</span>
</a>
And the lastest script I'm applying is:
$(document).ready(function() {
$('strong, dd').html($('strong, dd').html().replace(/,-/g, 'kr'));
});
As has been mentioned innumerable times here on SO, do not try to manipulate the DOM as a string. Pain awaits.
Instead, traverse the DOM, finding text nodes, and perform whatever transformation you want to make on each text node. There are many ways to do that.
In your case, you have many problems, as mentioned already by some of the commenters and responders:
You're setting window.onload to undefined (the result of calling skrivkr), instead of to skrivkr itself.
You're extracting the text value of an element, which consists of the concatenation of all text down all levels, performing the replacement, then sticking it back in with html. This will wipe out all the element structure below.
Minor point, but there's no need to escape the comma in the regexp.
You're extracting the textual content of all div elements in the entire document, transforming it, then adding that back as the content of all p elements in the entire document. It's hard to imagine that's what you want to do.
You can update the content of each div like this
$(document).ready(function() {
$('div').each(function(){
var newText = $(this).html().replace(/,-/g, 'kr');
$(this).html(newText);
});
});
You can remove the var "newText = " and replace it with $(this).html($(this).html().replace(/,-/g, 'kr'));
The first example is easier to understand perhaps if you are new to programming.
You will, however only change the content of text placed in tags.
I would place the text to replace in a div with some predefine class, like "autoKronor", it would then look like this:
<div class="autoKronor">123,-</div>
and
$(document).ready(function() {
$('.autoKronor').each(function(){
var newText = $(this).html().replace(/,-/g, 'kr');
$(this).html(newText);
});
});
to en sure that only text you intended to change gets changed..
Example here: http://jsfiddle.net/akm1uw8h/2/
Also note the use of $(document).ready(); instead of window.onload. It does what you intended to do with window.onload.
if you really want to change EVERY single instance of ",-" to "kr" then you could do this:
$(document).ready(function() {
$('body').html($('body').html().replace(/,-/g, 'kr'));
});
But i strongly advice against the last example because it will be the slowest to compute and more importantly you might change stuff you don't intend to, like any script block inside the page body (with that i mean other javascripts)

Place tags around certain text within contenteditable without moving cursor

I am working on a simple (I thought) word processor. It uses contenteditable. I have a list of words that I want to always appear highlighted.
<article contenteditable="true" class="content">
<p>Once upon a time, there were a couple of paragraphs. Some things were <b>bold</b>, and other things were <i>italic.</i></p>
<p>Then down here there was the word highlight. It should have a different color background.</p>
</article>
So basically what I need is a way to wrap a word in <span> tags. This has proven more difficult than I expected.
Here was what I tried first:
var text = document.querySelector('article.content').innerHTML
start = text.indexOf("highlight"),
end = start + "highlight".length;
text = text.splice(end, 0, "</span>");
text = text.splice(start, 0, "<span>");
document.querySelector('article.content').innerHTML = text;
It uses the splice method found here.
And it does exactly what I need it to do, with one big issue: the cursor gets moved. Because all the text is replaced, the cursor loses its place, which isn't a good thing for a text editor.
I've also tried a couple times using document.createRange, but the issue is that while given the start and end points of a range only includes visible characters, text.indexOf("highlight") gives the index including the tags and such.
A few ideas which I'm not sure how to execute:
Figure out where the cursor begins and place it there again after using the code above
Find the difference in indexes between createRange and indexOf
Maybe there's already a library with this kind of functionality that I just can't find
Thank you for your help!
Firstly, I would recommend against doing this by manipulating innerHTML. It's inefficient and error-prone (think of the case where the content contains an element with a class of "highlight", for example). Here's an example of doing this using DOM methods to manipulate the text nodes directly:
https://stackoverflow.com/a/10618517/96100
Maintaining the caret position can be achieved a number of ways. You could use a character offset-based approach, which has some disadvantages due to not considering line breaks implied by <br> and block elements but is relatively simple. Alternatively, you could use the selection save and restore module of my Rangy library, which may be overkill for your needs, but the same approach could be used.
Here is an example using the first approach:
http://jsbin.com/suwogaha/1

Select the second word

I have this HTML:
<span>Lies Hendriks</span>
How can I select the second word with CSS. I can not change the HTML of this document.
I want give "Hendriks" another style. Or can I do it with JavaScript?
You will need to split up into seperate entities to apply different styles to each word.
<span style="style1">Lies</span><span style="style2">Hendriks</span>
There is no way to selectively apply styles to a single word using JS or CSS without changing the HTML itself.
You can't do with CSS, but if you know your structure (for instance, that it's always the second word you need to style differently) you can do it with JavaScript (assuming that you can inject your JS into the page, of course).
This is an example that may be a bit convoluted, but if your HTML has:
<span>Lies Hendriks</span>
then you can do it like so (upon your DOM loading event):
var span1 = document.getElementById('span1');
span1.innerHTML = span1.innerHTML.replace(/(\s+)(.*)/, '$1<span class="red">$2</span>');
assuming you have
.red
{
color: red;
}
in your CSS
The above code allows me to make everything after the first word red. That should give you an idea how to do that, depending on your structure.

Categories