Get cursor position in a contenteditable div using innerHTML and pipes - html

I'm writing a simple WYSIWYG editor in Angular 5 to handle tags in the text. Those tags are like variables. For instance when doing: Hi (!--username--), welcome! it's rendered as Hi alex, welcome!. In order to be user-friendly for the non-technical, the WYSIWYG is transforming (!--username--) to a pretty HTML fragment showing directly "Alexandre" in its content.
This editor needs to handle simple HTML tags too (<b>, <i>, ...)
To do that, I've developed a component named editor which is using Angular's value accessors and showing a simple div like that:
<div class="editor" #editor [innerHTML]="content | prettytags: completions" (focus)="toogleToolbar()" (focusout)="toogleToolbar()"
(click)="onClick($event)" (keyup)="onKey($event)" [attr.contenteditable]="!readonly"></div>
The pipe looks like (for information, completions is the variable containing all tags values):
const pattern: RegExp = /(\(!--[^\s-]*--\))/;
#Pipe({
name: 'prettytags'
})
export class PrettyTagsPipe {
constructor(private sanitizer: DomSanitizer) {}
transform(value: string, completions: any[]): SafeHtml {
if (isNil(value)) return '';
const text = this.makeText(value, completions, 0);
return this.sanitizer.bypassSecurityTrustHtml(text);
}
private makeText(value: string, completions: any[], index: number): any {
const text = value
.split(pattern)
.map(word => {
const tag = completions.find(t => t.tag === word);
return isNil(tag)
? word
: this.getTagHtml(tag.value)
})
.join('');
return text;
}
private getTagHtml(text: any) {
return `<span class="chip" spellcheck="false">${text}</span> `;
}
}
In order to get the two-way data binding working as I'm using [innerHTML], I'm using the keyup event to get new characters but I need to get the caret position to append new characters. To do that I've copy/pasted a function found on Stack Overflow to get the caret position:
private getCaretPosition() {
const element = document.querySelector('.editor');
const range = window.getSelection().getRangeAt(0);
const preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(element);
preCaretRange.setEnd(range.endContainer, range.endOffset);
return preCaretRange.toString().length;
}
And on my onKeyUp: I do the following:
[...]
const position = this.getCaretPosition();
this.content += key.length === 1 ? this.content.slice(0, position) + key + this.content.slice(position) : '';
but it's not working as it gets the text position.
For instance, if the user wants to edit the content: from Hi (!--username--), welcome! to Hi (!--username--), I'm fine to see you back!, he will place his caret just after the comma, so I'll get 8 (for "Hi alex,") but with my content variable I'll get Hi (!--u.
I know I can get the position of the cursor with HTML tags, but I'll need to do many computations for each key pressed.
Do you have any idea to get this thing to work?

Related

Show a list of elements once an hashtag is written

I noticed that some social networks (such as Instagram, Twitter) highlight words with hashtags or # in blue and a list of tags or people comes up. I was wondering how I can replicate a similar effect in Angular.
By analyzing the page, I saw that everytime a new hastag is written, a new < span > tag is generated with the hashtag text inside, but I still can't understand how to implement this.
Even an idea on how to make it would be appreciated.
You can use a dropdown from bootstrap or material and fill it with a *ngFor with an array of filtered items from an array of possible tags.
You can even have a regex for the filtered tags to match anything you want, here is an example:
public search: string = ''; // You will change this with some input.
getTags(): string[] {
let searchRegex = new RegExp(this.search, 'gi');
return this.options.filter((o: string) => {
return o.match(searchRegex);
});
}
And the only thing you have to do in your html is to put your getTags() in your *ngFor() like this and check if the user has been started writing in your search input.
<ng-container *ngIf="search !== ''">
<span *ngFor="let tag of getTags()">
{{tag}}
</span>
</ng-container>
You can even get more of this by changing the input to start changing the search variable and get a message variable that will be checked for special characters to start filling the search with something like this:
public message: string = ''; // This will have the actual message.
public search: string = ''; // This will be the real variable that will get the
tags.
checkForSearchInMessage(){
if(this.message.includes('#')){ // The # character could be anything.
this.search = this.message.split('#')[1];
}
}

Dynamically add elements to the editable div with Angular and ANYWHERE

So, I am creating an HTML interface where user should be able to write a text and push it as a notification to our mobile app.
I am facing some troubleshoots with the text and the dynamic inserted elements using Angular 5;
The text can contain special elements like: phone number, Location and website URL. Those special elements will be inserted by pressing on a button that opens a dialog, and for each one its specific fields are displayed, like google maps for location and input fields for Web URL and mobile Phone. It is implemented this way in order to capture longitude, latitude and phone numbers on save button in order to add them as buttons to the received push on the devices.
Anyway, the above is implemented and could work successfully except the way of adding dynamically spans of special elements inside the div of the web interface. Spans added must have a class and a click event to display again the dialog in order to modify the data. Also they can be inserted anywhere inside the big div depending on user's choice.
Below is the image of above description.
The blue spans, are the ones that should be added dynamically inside the content editable div that can be filled by around 450 characters.
So how to solve the issue and enable the feature of adding clickable and designed spans with icons inside a content editable div, and be able in a final stage to retrieve data?
My code is the below, working but for a specific/predefined position:
Message.html
<div id="myMessage" contenteditable="true" dir="ltr" [innerHTML]="contentEN | safeHtml"
style=" height: 80px;border: 1px solid #c1c1c1; padding: 7px;">
</div>
<ng-container #vc>
</ng-container>
Message.ts
#ViewChild('vc', {read: ViewContainerRef}) target: ViewContainerRef;
createSpanPhone(spanIDNumber, phoneDescription, phoneValue ){
// here the span Phone is created dynamically outside the div
let phoneComponent = this.cfr.resolveComponentFactory(PhoneComponent);
this.componentRef = this.target.createComponent(phoneComponent);
}
PhoneComponent.ts
import { Component } from '#angular/core';
import { faPhone } from '#fortawesome/free-solid-svg-icons';
#Component({
selector: 'my-phone',
template: '<span contenteditable="false" (click) = "test()" class="BAN_Tags_IN_Text"> <fa-icon
[icon]="faPhone" class="faSpanIcon"> </fa-icon> <span class="phoneDesc"
data-attr="EN">hello</span> <span class="phoneVal" ><b>12346</b></span>
</span>'
})
export class PhoneComponent {
faPhone = faPhone; // trying the icon
constructor(){
}
test(){
console.log("Hiiii"); // trying the click event
}
}
The ViewContainerRef is filled successfully but I need to fill spans in the div above (id=myMessage) and not in a predefined position.
if your text are simple text (don't has html tags that can not enclosed by <span>, -I want to mean that is allowed e.g. <i> or <b>, but not <p> - you can create a component like
#Component({
selector: "html-content",
template: `
<span class="inline" [innerHTML]="value"></span>
`
})
export class HtmlComponent {
#Input() value;
constructor() {}
}
A directive like
#Directive({ selector: "[content]" })
export class ContentDirective {
#Input() set content(textHtml: string) {
this.viewContainerRef.clear();
if (!textHtml) return
//If not end with . or space, add an space
if (textHtml.slice(-1)!=" " && textHtml.slice(-1)!=".")
textHtml+=" "
//gets the "words"
//const parts = textHtml.match(/\ ?\S+\ |\ ?\S+\./gi);
const parts = textHtml.match(/<?[^\r\n\t\f\v< ]+\ ?/gi);
parts.forEach(h => {
let space = false;
let search = h.replace(/[\ .;,:]/gi, "")
let arg=null;
//to allow pass arguments to the components in the way, e.g.
// <phone=arguments -be carefull! the arguments can not contains spaces
//
if (search.match(/<phone=.+/))
{
arg=search.split("=")[1].split(">")[0]
search="<phone>"
}
if (search.match(/<location=.+/))
{
arg=search.split("=")[1].split(">")[0]
search="<location>"
}
switch (search) {
case "<phone>":
case "<location>":
const factory =
search == "<phone>"
? this.componentFactoryResolver.resolveComponentFactory(
PhoneComponent
)
: this.componentFactoryResolver.resolveComponentFactory(
LocationComponent
);
const phone=this.viewContainerRef.createComponent(factory);
//if our component has "#Input() arg"
(phone.instance as any).arg=arg||"";
break;
default:
const factoryHtml = this.componentFactoryResolver.resolveComponentFactory(
HtmlComponent
);
const html = this.viewContainerRef.createComponent(factoryHtml);
html.instance.value = h;
space = true;
break;
}
//this allow write space or dot after the component.
if (!space && h.match(/.+>[\ ;,:.]/gi)) {
const factoryDot = this.componentFactoryResolver.resolveComponentFactory(
HtmlComponent
);
const html = this.viewContainerRef.createComponent(factoryDot);
//we check if, after the component we has a "," or ";" or ":" or ". "
html.instance.value = h.slice(h.indexOf(">")+1)
}
});
//just for check the parts
console.log(textHtml, parts);
}
constructor(
private viewContainerRef: ViewContainerRef,
private componentFactoryResolver: ComponentFactoryResolver
) {}
}
You can see a stackblitz without warranty

How to create a string map "location" and "location Value Text" of this string text?

I developed project using anguler with ngRx framework. I used TypeScript with HTML for developing front-end.My db have saved 'HTML' format texts like below.
"<html><body>A.txt
B.txt
D.txt
www.facebook.com
</body></html>"
This text priviouly , I drectly render in html file using <dev INNERHTML ={{stringText }} \> like wise.
But my project using JXBrowser and as it's configuration , this can't be directly open in default browser clicking just link.
For that work ,I need to take href location as URL and when click it passed to .ts file.
I thought ,it change as like this <a role="button" click='getLink(myText)'> {{getLink(value}} </a>'. so ,create this ,I need that text put a array with contain 'location' and value.Next ,I though ,Iterate that array in HTML file.
I need some expert help to do this ? I am struggle with map above text to such kind of string array (eg :array[hrfeLink][value]). Hope some expert help me.
------------Updated---------------
According to the comment, I will try this way, and I can take the link location. But still couldn't take value.
let parser = new DOMParser();
let doc = parser.parseFromString(info, "text/html");
let x = doc.getElementsByTagName('a');
for (let i = 0; i < x.length ; i++) {
console.log(x[i].getAttribute('href'));
}
What is the value that you want? Is it the anchor text of the link?
We create an interface Link with the properties that we want from each link
interface Link {
location: string;
value: string;
}
Then we create a function that extracts all links from an html string and converts them to an array of Link objects.
function parseLinks( stringHTML: string ): Link[] {
// create a parser object
const parser = new DOMParser();
// turn the string into a Document
const doc = parser.parseFromString( stringHTML, "text/html" );
// get all links
const linkNodes = doc.getElementsByTagName('a');
// convert from HTMLCollection to array to use .map()
const linksArray = [...linkNodes];
// map from HTMLAnchorElement to Link object
return linksArray.map( element => ({
location: element.href,
value: element.innerText,
}))
}
Now you can do whatever with the links from your text
const text = `<html><body>A.txt
B.txt
D.txt
www.facebook.com
</body></html>`;
const links: Link[] = parseLinks( text );
// can use like this
links.map( ({location, value}) => {
// do something here
})
Typescript Playground Link

Custom Angular directive is not working with async value

We have a custom directive in our project which we use when we want trim the long text in some UI elements. In one case it fails to work. There are no errors, no warnings, it's just no there. Checking the code in the DevTools shows no signs of this directive triggering (no HTML changes, no CSS added). The directive looks like this:
ngAfterViewInit() {
let text = <string>this.elt.nativeElement.innerHTML.trim();
if (!text || text !== (<HTMLElement>this.elt.nativeElement).innerText) {
return;
}
const limit = this.value || DEFAULT_VISIBLE_ENDING_LENGTH; // default length = 4
if (text.length > limit && this.elt.nativeElement.scrollWidth > this.elt.nativeElement.clientWidth) {
const startText = text.substr(0, text.length - limit);
const endText = text.substr(-limit);
this.renderer.setProperty(
this.elt.nativeElement,
'innerHTML',
`<div class="part1"><span>${startText}</span></div><div class="part2"><span><span>${endText}</span></span></div>`
);
}
}
It fails to work when the text to display & trim is obtained from observable (store selector). It doesn't matter if I use Observable + async pipe or if I map the value to the component property in selector subscribe.
#Component({
...
changeDetection: ChangeDetectionStrategy.OnPush,
})
this.sampleInProgress$: Observable<string>;
this.sampleInProgress: string;
...
this.sampleInProgress$ = this.store.select(fromAutomation.getInfoPanelData).pipe(
map(({ sample, experiment }) => {
this.sampleInProgress = sample?.sampleName; // does not work either
this.experimentInProgress = experiment?.parameterSet;
return sample?.sampleName;
}),
);
And the HTML:
<span class="label" gs-ellipsis>{{ sampleInProgress$ | async }}</span>
<!-- In this case, subscribe is done in the component -->
<span class="label" gs-ellipsis>{{ sampleInProgress }}</span>
Sorry for the bit messy code, I just didn't wanted to post almost the same code twice. I'm either subscribing explicitly or assigning the observable using async with it. Not doing both at the same time. The other place in the code where we use this ellipsis (and where it works) also uses OnPush Detection strategy the but that the value is provided by #Input.
I have a feeling that it has something to do with the ngAfterViewInit() in the directive itself, but I'm not sure. Directives are not my strongest field.
Any idea what can be the cause and how to fix it?
your directive handling happends too early. I assume you can hack it a bit and render element just when its content is ok with the help of ngIf directive.
<span class="label" gs-ellipsis *ngIf="sampleInProgress$ | async as value">{{ value }}</span>

HTML.TextAreaFor - removing html tags for display only

In an MVC application I have to use #HTML.TextAreaFor to display some text from a database, the trouble is sometimes that text may have HTML tags within it and I can't see a way to remove those for display only.
Is it possible to do this in the view (maybe with CSS?) without having to strip the tags in the controller first?
EDIT
The data coming from the controller contains html tags which I do not want to remove, I just don't want to display them
Normally I would use #HTML.Raw but it has to work in a #HTML.TextAreaFor control.
If you want to decode Html returned from the Controller you can use the following JavaScript method:
This method decodes "Chris&apos; corner" to "Chris' corner".
var decodeEntities = (function () {
// this prevents any overhead from creating the object each time
var element = document.createElement('div');
function decodeHTMLEntities(str) {
if (str && typeof str === 'string') {
// strip script/html tags
str = str.replace(/<script[^>]*>([\S\s]*?)<\/script>/gmi, '');
str = str.replace(/<\/?\w(?:[^"'>]|"[^"]*"|'[^']*')*>/gmi, '');
element.innerHTML = str;
str = element.textContent;
element.textContent = '';
}
return str;
}
return decodeHTMLEntities;
})();
You can do this by using a razor code in your view.
#Html.Raw(HttpUtility.HtmlDecode(Model.Content))
if I set Model.Content to this string "<strong>This is me</strong><button>click</button>", the code above will render it like HTML code and will have a strong text next to a button as an output like the image below:
There's some nice rich text editors libraries like CK Editor, Quill, or TinyMCE that can display HTML while still maintaining the editor capabilities of being a text editor. All of these libraries have capabilities of being read-only as well if that's necessary.
Example from Quill -
Sorted this by changing TextAreaFor toTextBoxFor and setting a formatted value.
#Html.TextBoxFor(x => Model.MyItem, new { #class = "form-control", #required = "true", Value = Regex.Replace(Model.MyItem, "<.*?>", String.Empty) })