creative-connections/Bodylight.js-Components

View on GitHub
src_aurelia-bodylight-plugin/src/elements/markdownaurelia.js

Summary

Maintainability
A
2 hrs
Test Coverage
import Markdownit from 'markdown-it';
//import Markdownitfootnote from 'markdown-it-footnote'; //footnote in MD
import {bodylightFootnotePlugin} from './markdown-it-bfootnote';
//import MarkdownitAttrs from 'markdown-it-attrs'; //replaced by regex in btoc - will parse ## 2. title 2
//import mk from '@liradb2000/markdown-it-katex'; //math in md, iktakahiro version seems to be most updated - works with latest katex
import mk from 'markdown-it-katexx'; //math in md, iktakahiro version seems to be most updated - works with latest katex
import hljs from 'highlight.js'; //highlights in MD source blocks
//import mka from 'markdown-it-abbr'; //adds markdown with <abbr> support
import mka from './markdown-it-abbr-ext'; //adds markdown with <abbr> support with extension
import mda from 'markdown-it-attrs'; //adds classes/attributes and other in curly brackets as attributes {.class #identifier attr=value attr2="spaced value"}
//import {markdownitbtoc} from './markdown-it-btoc'; //commented out - breaks with markdown-it-attrs headers h1 h2 h3
import {bindable, inject} from 'aurelia-framework';
import {HttpClient} from 'aurelia-fetch-client';
import {EventAggregator} from 'aurelia-event-aggregator';
import {I18N} from 'aurelia-i18n';
import {ContentUpdate} from './messages';

const REGISTEREVENTTIMEOUT = 1000; //timout to sent register event for subsequent fmu to bind to inputs
//returns next dialog - sibling or after the parent <p> ...
const findNextDialog = (target) => {
  let mydialog = target.nextSibling;
  if (mydialog && mydialog.className === 'abbr_dialog') return mydialog;
  /*mydialog = target.parentNode.nextSibling;
  if (mydialog && mydialog.tagName === 'DIALOG') return mydialog;
  //parentNode.nextSibling;//closest('dialog');
  mydialog = target.parentNode.parentNode.nextSibling; //may be nested in a <p> dialog is outside <p> in firefox
  if (mydialog && mydialog.tagName === 'DIALOG') return mydialog;*/
  return null;
}

const registerCustomElementFunction = () => {
  //register e.g. click on abbr element will show title and allows expand to url - wikipedia content
  //click on abbr will show extended modal info and link 
  
  //click on abbreviation shows a sibling dialog, if presented
  let abbrs = document.getElementsByTagName('abbr');
  for (let abbr of abbrs) {
    abbr.onclick = (e) => {
      let content = e.target.attributes["data-content"];
      let url = e.target.attributes["data-wiki"];
      console.log('clicked abbr',e);

      let mydialog = findNextDialog(e.target);
      if (mydialog) mydialog.classList.add('abbr_dialog_show');
    }
  }
  //click on dialog will hide it
  let dialogs = document.getElementsByClassName('abbr_dialog');
  for (let dialog of dialogs) {
    dialog.onclick = (e) => {
      console.log('clicked dialog',e);
      if (e.target.tagName === 'A') {
        window.open(e.target.attributes['src'].value);
        e.target.parentNode.classList.remove('abbr_dialog_show');//close(); //close parent dialog
      } else
      e.target.classList.remove('abbr_dialog_show');
    }
    /*for (let child of dialog.children) {
      if (child.tagName === 'A') child.onclick = (e) => {window.open(e.target.attributes['src'].value);}
      child.onclick = (e) => {e.target.parentNode.classList.remove('abbr_dialog_show');}
    } */   
  }
}

/**
 * This is markdownaurelia component to be used in aurelia apps,
 * requires aurelia-dynamic-html plugin to be enabled,
 * in case of using as web-component, use mardkdown which inherits majority
 */
@inject(I18N, HttpClient, EventAggregator )
export class Markdownaurelia {
  @bindable src;
  @bindable watchhash;
  @bindable base='';
  @bindable fromid;
  //@bindable toc;
  @bindable content;
  @bindable keepanim;
  previoussrc='';
  showmodal = false;
  constructor(i18n, httpclient, ea) {
    //this.i18n = i18n;
    this.client = httpclient;
    this.html = '';
    //this.toc = '';
    this.ea = ea;
    this.i18n = i18n;
    //console.log('bdlmarkdownaurelia eventaggregator:', ea);
    //option function to be called when customevent retrieved
    this.handleContentChange = e => {
      this.updateContent(e.detail.content,e.detail.keepanimation);
    };
  }

  bind() {
    if (typeof this.keepanim === 'string') this.keepanim = this.keepanim === 'true';
    //console.log('markdownaurelia bind() src', this.src);
    if (this.base && this.base.length > 0) window.bdlBaseHref = this.base; // define bdlbasehref only if not empty string
    if (this.src && this.src.length > 0 && this.md) this.readmd();
  }

  srcChanged() {
    //console.log('markdownaurelia srcChanged() src', this.src);
    if (this.src && this.src.length > 0 && this.md) this.readmd();
  }


  attached() {
    //console.log('bdlmarkdownaurelia attached() src:', this.src);
    // eslint-disable-next-line new-cap
    //optionally, register customevent handler for 'contentupdate' when fromid is defined
    // eslint-disable-next-line new-cap
    this.md = Markdownit({
      html: true, //enable html tags - this enables also custom elements of components/webcomponents
      linkify: true,
      typographer: false,
      highlight: function(str, lang) { //highlight plugin
        if (lang && hljs.getLanguage(lang)) {
          try {
            return '<pre class="hljs"><code>' +
              hljs.highlight(lang, str, true).value +
              '</code></pre>';
            // eslint-disable-next-line no-empty
          } catch (__) {}
        }

        return ''; // use external default escaping
      }
    }).use(bodylightFootnotePlugin) //footnote - extension to MD - otherwise no link between [^1] and [^1]:
      //.use(MarkdownitAttrs)
      .use(mk, {'throwOnError': true, 'errorColor': ' #cc0000'}) //math-> katex - should be faster than mathjax and crossbrowser compatible when chrom do not support mathml
      .use(mka) //abbreviation      
      //.use(markdownItAnchor, { permalink: true, permalinkBefore: true, permalinkSymbol: 'ยง' } )
      //.use(markdownItTocDoneRight);
      //.use(markdownitbtoc) //generate small table of content
      .use(mda); //attributes
    if (this.i18n.getLocale() === 'cs') {
      console.log('czech version');
    } else {
      console.log('english version');
    }
    if (this.src && this.src.length > 0 && this.md) this.readmd();
    //console.log('bdlmarkdownaurelia eventaggregator2:', this.ea);
    //there seems some bug in ea dependency injection - checking subscribe as function or inner ea object
    //if (typeof this.ea.subscribe === 'function')
    this.ea.subscribe(ContentUpdate, msg => this.updateContent(msg.content));
    //else if (typeof this.ea.ea === 'object')
    //      this.ea.ea.subscribe(ContentUpdate, msg => this.updateContent(msg.content));
    if (this.fromid) {document.getElementById(this.fromid).addEventListener('contentupdate', this.handleContentChange);}

  }

  changesrc(...args) { //first is src, second is base
    console.log('mardownaurelia.changesrc called(), args:', args);
    if (args[1]) this.base = args[1];
    if (args[0] && args[0].length > 0) this.src = args[0];
    this.readmd();
  }

  readmd() {
    //fetch md source from src attribute
    //relative url - set with base
    console.warn('bdlmarkdownaurelia readmemd(), src:', this.src);
    if (! this.src || (this.previoussrc === this.src)) return;
    this.previoussrc = this.src;
    let url = (this.src.startsWith('http')) ? this.src : this.base + this.src;
    this.client.fetch(url)
      .then(response => response.text())
      .then(data => {
        console.log('markdownaurelia fetched md')
        //if (!keepanimation && window.ani) {
        //FIX bug - changing page cause some animation not render correctly or binding is incorrect  
        if (!this.keepanim) {
          if (window.ani) window.ani.detached();
          //window.ani.destroyAdobe();
          console.warn('markdownaurelia animatebindings emptied from readmemd() src:',this.src);
          window.animatebindings = [];
        }
        //}    
        this.text = data;
        //convert from md to html
        this.html = this.md.render(this.text);
        /* comment out TOC
        let tocregex = /<div[^<>]*id="toc"[^<>]*>(.*?)<\/div>/g;
        this.toc = this.md.render('@[toc] \n' + this.text).match(tocregex)[0];
        */
        //console.log('readmd toc:', this.toc);
        this.update();
      });
  }

  detached() {
    window.removeEventListener('hashchange', this.handleContentChange);
  }

  update() {
    //console.log('markdownaurelia update');
    //if (this.mj)this.mj.typesetPromise();
    //if (window.MathJax) window.MathJax.typeset();
    //register some custom implementation for elements    
    //scroll to top of the page
    window.scrollTo(0, 0);
    console.log('i18n',this.i18n);
    console.log('updating translation',this.mydiv);
    this.i18n.updateTranslations(this.mydiv);
    //send fmiregister event for possible shared fmi component to bind to input components
    setTimeout(() => {
      let event = new CustomEvent('fmiregister');
      document.dispatchEvent(event);
      registerCustomElementFunction()    
    },REGISTEREVENTTIMEOUT);

  }

  updateContent(content, keepanimation = false) {
    console.log('markdownaurelia updateContent');
    //fix remove older animation objects
    if (!this.keepanim && !keepanimation && window.ani)  {
      window.ani.detached();
      //window.ani.destroyAdobe();
      console.warn('markdownaurelia animatebindings emptied from updateContent()');
      window.animatebindings = [];
    }
    //fix remove global binding
    //continue with rendering
    this.text = content;
    this.html = this.md.render(this.text);
    //this.registerCustomElementFunction()    
    this.update();
  }

  contentChanged(newvalue, oldvalue) {
    //console.log('markdown2 update called by OOP polymorphism mydiv, html', this.mydiv, this.html);
    this.updateContent(newvalue);
  }
}