Borak - software developer illustrational
Blog
12.6. 2019 / 00:00

javascript

Froala

Angular 4+

Froala editor and code highlighting in Angular 4

Froala editor Is a rich framework for WYSIWYG solutions on the web. It also offers great number of ways for the incorporations of the extensions needed. To create an extension it have to be registered in the Froala setup object, which after initialization occurs as the property of global JQuery object. Yes, it is true, JQuery, or rather its lighter subset version is a needed dependency for Froala to work.
There are many ways to add froala solution to the angular application. I choose the simplest modular way as Froala is at disposal as exported directive.
All you need to do, is install the froala via npm

Npm install angular-froala-wysiwyg –save
And then register it in the injector in your main module of the Angular 2+ application

import { FroalaEditorModule, FroalaViewModule } from 'angular-froala-wysiwyg';
...
imports: […
   FroalaEditorModule.forRoot(),
   FroalaViewModule.forRoot(),
…]
 You also are free to setup the editor through config object during its initialization. I had choosen the location of the config to be a part of my environments files. Hovever, I had some common properties, that did not differ based on the environment. So I choose to isolate this common part to a separate location. My common part looks like this:


export default {
  enter: $.FroalaEditor.ENTER_BR,
  toolbarButtons: ['fullscreen', 'bold', 'italic', 'underline', 'strikeThrough', 'subscript', 'superscript', '|', 'fontFamily', 'fontSize', 'color', 'inlineClass', 'inlineStyle', 'paragraphStyle', 'lineHeight', '|', 'paragraphFormat', 'align', 'formatOL', 'formatUL', 'outdent', 'indent', 'quote', '-', 'insertLink', 'insertImage', 'insertVideo', 'embedly', 'insertFile', 'insertTable', '|', 'emoticons', 'fontAwesome', 'specialCharacters', 'insertHR', 'selectAll', 'clearFormatting', '|', 'print', 'getPDF', 'spellChecker', 'help', 'html', '|', 'undo', 'redo', 'highlight'],
  toolbarButtonsMD: ['fullscreen', 'bold', 'italic', 'underline', 'strikeThrough', 'subscript', 'superscript', '|', 'fontFamily', 'fontSize', 'color', 'inlineClass', 'inlineStyle', 'paragraphStyle', 'lineHeight', '|', 'paragraphFormat', 'align', 'formatOL', 'formatUL', 'outdent', 'indent', 'quote', '-', 'insertLink', 'insertImage', 'insertVideo', 'embedly', 'insertFile', 'insertTable', '|', 'emoticons', 'fontAwesome', 'specialCharacters', 'insertHR', 'selectAll', 'clearFormatting', '|', 'print', 'getPDF', 'spellChecker', 'help', 'html', '|', 'undo', 'redo', 'highlight'],
  toolbarButtonsXS: ['fullscreen', 'bold', 'italic', 'underline', 'strikeThrough', 'subscript', 'superscript', '|', 'fontFamily', 'fontSize', 'color', 'inlineClass', 'inlineStyle', 'paragraphStyle', 'lineHeight', '|', 'paragraphFormat', 'align', 'formatOL', 'formatUL', 'outdent', 'indent', 'quote', '-', 'insertLink', 'insertImage', 'insertVideo', 'embedly', 'insertFile', 'insertTable', '|', 'emoticons', 'fontAwesome', 'specialCharacters', 'insertHR', 'selectAll', 'clearFormatting', '|', 'print', 'getPDF', 'spellChecker', 'help', 'html', '|', 'undo', 'redo', 'highlight'],
  toolbarButtonsSM: ['fullscreen', 'bold', 'italic', 'underline', 'strikeThrough', 'subscript', 'superscript', '|', 'fontFamily', 'fontSize', 'color', 'inlineClass', 'inlineStyle', 'paragraphStyle', 'lineHeight', '|', 'paragraphFormat', 'align', 'formatOL', 'formatUL', 'outdent', 'indent', 'quote', '-', 'insertLink', 'insertImage', 'insertVideo', 'embedly', 'insertFile', 'insertTable', '|', 'emoticons', 'fontAwesome', 'specialCharacters', 'insertHR', 'selectAll', 'clearFormatting', '|', 'print', 'getPDF', 'spellChecker', 'help', 'html', '|', 'undo', 'redo', 'highlight'],
}

The are as many as possible common features featured in this config. You are free to setup your own shape of the editor. According to your needs.

Then, individual environments are created. They consist of the spread common part and environment specific parts merged together. Here is the possible shape of the environment.cofing

import froalaCommon from './froalaCommon';

export const environment  = {
  production: true,
  host: {
    url: "http://borakpetr.cz"
  },
  froala: {
    ...froalaCommon,
    imageManagerLoadURL: "http://borakpetr.cz/api/images/froala",
    imageUploadURL: "http://borakpetr.cz/api/images/image",
    imageUploadMethod: "POST"
  }
}
After this setup, we can use froala in our Angular 2+ SPA as a simple directive. For example like this

[froalaEditor]="froalaConfig" [(froalaModel)]="content[language.id] && content[language.id].content"
Important parts here are the config input directive and the bidirectional froala model directive.

ADDING THE EXTENSION

To add our highlight extension, we can leverage the two methods on the JQuery Froala object.

To add a simple button

$.FroalaEditor.DefineIcon('highlight', {NAME: 'highlight'});
To add a functionality to linked to this button

$.FroalaEditor.RegisterCommand('highlight', {
  title: 'Highlight Code',
  focus: false,
  undo: false,
  refreshAfterCallback: true,

  callback: function() {
Implemetation here
})
This setup can be, for example, within our SPA, isolated to a simple injectable provider like this:

@Injectable()
export class Highlight {
  public listener: any = null;
  public registerHighlightPlugin(){
    $.FroalaEditor.RegisterCommand('highlight', {  title: 'Highlight Code',
  focus: false,
  undo: false,
  refreshAfterCallback: true,

  callback: function() {
     ...Implemetation here...
  })
 }
}



To use and setup our extension, we now need only to inject this provider at the right moment and call the registerHigilightPlugin method.

Let’s say, for example, we have a simple article component. The the initialization could look like this.

import { Highlight } from "../helpers/highlight/highlight";

@Component({
  selector: 'app-article',
  templateUrl: './article.component.html',
  styleUrls: ['./article.component.css']
})
export class ArticleComponent implements OnInit {
  public froalaConfig: any;
constructor(
  public highlight: Highlight,
) {
  this.highlight.registerHighlightPlugin();
  this.froalaConfig = Object.assign({},environment.froala
    }

}

THE PLUGIN IMPLEMENTATION

The highlight plugin calls the Froala’s registerCommand and DefineIcon method as stated earlier.
The registerCommand method is used as the plugin implementation. The most important part of the registerCommand method is its setup object we pass it.
In this setup object there is, besides others, also the callback property.
It is the definition of the callback, that is called when the linked Button is being clicked.
Here we now implement the code highliht functionality.


THE CODE HIGHLIGHT FUNCTIONALITY

For the code highlight functionality I choose the highlight.js library. It is really huge project. Supporting over 89 styles and 120 languages. It could easily become an overload for our project as we need only javascript subset and one style pattern. Luckily the interface provides a way to import just the subset we need.
All we need to do, is import the highlight library into our highlight module, where the Highlight injector is located, and call the initialization routine

import hljs from 'highlight.js/lib/highlight';
import javascript from 'highlight.js/lib/languages/javascript';
hljs.registerLanguage('javascript', javascript);


BINDING IT ALL TOGETHER

Now is the right time to tie the highlight and Froala functionalities together. Fundamental is the callback property that is passed to the

$.FroalaEditor.RegisterCommand(options)
options object.

Now we need to solve the fact, that we want the highlighting functionality work on every inputed character to the highlighted region.
We have a keydown event. At our disposal. We bind the keydown events to the blocks that are meant to be highlighted individually.

The highlight.js library has a method

hljs.highlightBlock(block)

which does all the magic for us.

However calling it multiple times on once already highlighted block would break our code. Highlight js does not handle this situation for us. But we want to reformat the block we are typing into on every keydown event.

So, we need to clear the formatted code back to the original shape and format it again. The highlight code looks for
<pre> and <code>.

Elements, whose content is subsequently formatted into mischmasch of spans with various class names according to the syntax of the chosen language. Thus we need to extract the original text information within the parent element, before using highlight.js again.
Other issue is, that by reformatting the content of the Froala editor, we automatically loose the position of the cursor in the original text. However Froala gives us way to solve this. The solution is the following method

FroalaElement.selection.save();
Which marks the position in the original text. And

this.selection.restore();
which restore the selection based on the tags added in the code by the previous one. Thus we need to clear the once formatted text, so we do not loose this tags. Another tag, we want to preserve in the formatted text is

<br>

So that we do not loose the paragraph formatting. Paragraph formatting in Froala can be setup by the config object, we have seen at the begging of this article. It is this line

enter: $.FroalaEditor.ENTER_BR,

Here, we have chosen the

<BR>

Tag to be added with the enter key having been pressed. So we need to dissect text, br and cursor marker nodes from the formated text. And hier is the algorithm to do it.

function textNodesUnder(node){
  var all = [];
  for (node=node.firstChild;node;node=node.nextSibling){
    if (node.nodeType==3 || node.classList.contains('fr-marker') || node.tagName === "BR") all.push(node);
    else all = all.concat(textNodesUnder(node));
  }
  return all;
}
Basically it is a recursion for tree parsing of Breadth first type. We iterate over all siblings and detect whether the node is of the type Text (that is what we primary need), includes fr-mark class (That is the marker of the cursor) or is the new line <br> tag. In the end we get the linear representation of the node’s content, which is what we need.

Now we need to add a listener for the key down event on the Froala text representating object. We also need a reference to the listener, so we can get rid of it, when we inicialize the listener again. Remember this line on the highlight provider?

@Injectable()
export class Highlight {
  public listener: any = null;
…

That is exactly, where we will store the reference. For the iteration of all the content of the Froala content text, Froala exposes the

Froala.el.children

Object, which is an array of all the elements in the input area. Now we need to iterate throught them and get only the PRE nodes, which are the nodes, that are supposed to be highlighted. Thus the following function

Array.from(this.el.children).filter((item: any)=>(item.tagName === 'PRE'))
Then we save the cursor pointer’s position, extract the textual information, append it back to the <PRE> node, highlight it back again and restore the cursor.

Array.from(this.el.children).filter((item: any)=>(item.tagName === 'PRE')).forEach((item: any) => {
  this.selection.save();

  function textNodesUnder(node){
    var all = [];
    for (node=node.firstChild;node;node=node.nextSibling){
      if (node.nodeType==3 || node.classList.contains('fr-marker') || node.tagName === "BR") all.push(node);
      else all = all.concat(textNodesUnder(node));
    }
    return all;
  }

  var text = textNodesUnder(item).map((item)=> item.nodeType==3 ? item.nodeValue : item.outerHTML).join('');
  item.innerHTML = text;
  hljs.highlightBlock(item);
  this.selection.restore();
})

The rest is just a piece of cake, just add the event listener to the content object of the froala editor

this.el.addEventListener("keydown", this.listener);
Here is the whole code of our Highlight provider:

import {Injectable} from "@angular/core";
import hljs from 'highlight.js/lib/highlight';
import javascript from 'highlight.js/lib/languages/javascript';
hljs.registerLanguage('javascript', javascript);

@Injectable()
export class Highlight {
  public listener: any = null;
  public registerHighlightPlugin(){
    $.FroalaEditor.DefineIcon('highlight', {NAME: 'highlight'});
    $.FroalaEditor.RegisterCommand('highlight', {
      title: 'Highlight Code',
      focus: false,
      undo: false,
      refreshAfterCallback: true,

      callback: function() {
        this.html.insert('var variable = "string heer"')
        this.el.removeEventListener('keydown', this.listener);
        this.listener = (event) => {
          Array.from(this.el.children).filter((item: any)=>(item.tagName === 'PRE')).forEach((item: any) => {
            this.selection.save();

            function textNodesUnder(node){
              var all = [];
              for (node=node.firstChild;node;node=node.nextSibling){
                if (node.nodeType==3 || node.classList.contains('fr-marker') || node.tagName === "BR") all.push(node);
                else all = all.concat(textNodesUnder(node));
              }
              return all;
            }

            var text = textNodesUnder(item).map((item)=> item.nodeType==3 ? item.nodeValue : item.outerHTML).join('');
            item.innerHTML = text;
            hljs.highlightBlock(item);
            this.selection.restore();
          })
        };
        this.el.addEventListener("keydown", this.listener);
      }
    });
  }
}

Cheers.




More by Borak

To maximalize your user experience during visit to my page, I use cookies.More info
I understand

#BORAKlive

This page is subjected to the Creative Common Licence. Always cite the Author - Do not use the page's content on commercial basis. Comply with the licence 3.0 Czech Republic.
go to top