PlanZed – welcome to my new project

Hi all, it’s a pity I have no time to share my discovering here more often, and I have so much of them!
I finished developing first phase of PlanZed project – cloud mind map app and it’s now available for free!
Welcome to try the app, I need feedback so much.
Here is in brief what do we have under the hood:

  • React – Next.js
  • Material UI as basement + I had to implement a lot of specific widgets
  • Hosted on Vercel
  • Mongo as a DB
  • No I don’t use jsPlumb – svg arrows are hand-written.
  • Pan/Zoom also hand-written (will have a separate post about this, don’t use css translate for pan, let browser scroll it for you, it’s much better!)
  • Built-in custom icons search from materialdesignicons.com
  • I extented already existing in materialdesignicons keywords by synonyms using wordnet (thanks for ‘node-wordnet’)
  • The project is currently trying to support three languages, same icons search do – icons keywords, extend by synonyms, are translated using Google API
  • No images upload – it will cost for me to store / manage them, so later as a paid option
  • But there is built-in search of free photos from pexels.com – in most cases that’s enough for mind map(they already support localized search)

Here is a GitHub repo serving as a community for PlanZed

Several screenshots from the project:

How to make “Fire-and-forget” HTTP request from Node.js without waiting for response.

Lambda functions at zeit/now v2 platform have default execution timeout 10s.
Another limitation important to consider:
– the process gets killed right after the response was sent.
When you have a job to be done in the lambda, not required immediately to prepare HTTP response (e.g. writing log to the database)
– it’s a good idea to send a response to the client ASAP and do rest of the work “in the background”.
But there are no built-in ways to do such things in zeit/now v2 lambdas.

The only way everybody talks but nobody tried is to extract such task into separate lambda and call it via another HTTP request.
It sounds easy, but let’s take a closer look:

  • λ1 was asked for a response. If it says anything – the game is over, no job can be done anymore.
  • so let λ1 ask λ2 to do rest of the job
  • and then respond to the original request

Ok?
How to ask λ2 to do something?

  1. make a request → get 200OK → respond
    get 200OK? λ2 is lambda too. It can’t say anything until the job is finished or it will be killed.
    So λ1 still have to wait for all the job is done before responding.

  2. make a request → “let it go” → respond
    How to let it go? Don’t pass any callback, don’t wait for any promise.
    But this way response to the original request will be sent too soon and λ1 will be killed.
    λ2 will have no chance to receive a request, parse args and perform any action.

  3. Make sure λ2 got the request, but don’t wait for a response:

//delegate.js
const https = require('https');
module.exports = async (host, message) => {
  message = JSON.stringify(message);
  var options = {
    hostname: host,
    method: 'POST',
    path: '/lambda2',
    headers: {
      'Content-Type': 'application/json',
      'Content-Length': Buffer.byteLength(message),
    },
  };
  await new Promise((resolve, reject) => {
    let req = https.request(options);
    req.on('error', (e) => {
      console.error(`problem with request: ${e.message}`);
      reject(e);
    });
    req.write(message);
    req.end(() => {
      console.log("NOW it's not λ1's problem anymore.");
      resolve();
    });
  });
};
//lambda1.js
const delegate = require('./delegate');
module.exports = async (req, res) => {
  let host = req.headers['x-now-deployment-url'];
  await delegate(host,'Hey, you, λ2! Did you hear an order?');
  res.end('Yes, my Master! Will be done!')
};
//lambda2.js
module.exports = async (req, res) => {
  console.log("Oh, so much to be done! Let's start immediately!");
  await new Promise((resolve) => {
    setTimeout(() => {
      console.log('Nothing was done successfully!');
      send(res, 200);
      resolve();
    }, 2000);
  });
};

Hope this piece of code will save someone’s work day!

Show hidden part of truncated text on hover/touch

Here are three ways to show hidden part of truncated text on touch/hover without Javascript

1) overflow:visible

Hover over or touch me to see the full version of this string. It looks like replaced by tooltip
some other stuff

Pros:

  • No Javascript
  • Good readability
  • Small and simple cross-browser CSS code
  • Easy to render.

Contras:

  • Need extra markup (inner span wrapper)
  • Requires all the parents of the text container to be wide enough or have ‘overflow’ porperty to be ‘visible’.

Here is SCSS with comments:

.overflow-tip {
  /* make it single-line */
  white-space:nowrap;
  /* truncate by container's size */
  overflow-x: hidden;
  /* add three dots */
  text-overflow: ellipsis;
  /* on touch or hover */
  &:active, &:hover {
    /* show hidden part outside of parent */
    overflow-x: visible;
    /* and with inner span */
    span {
      /* allow to overlap siblings */
      position: relative;
      /* make readable design */
      background-color: PaleGoldenRod;
      border: 1px solid gray;
      padding: 3px;
      /* compensate padding and border size to avoid jerking */
      margin-left: -4px;
    }    
  }
}

2) direction:rtl

Hover over or touch me to see the end of this long string.
some other stuff

Pros:

  • No Javascript
  • No extra HTML
  • Super simple CSS

Contras:

  • Not so readable for very long string or very short container
    Commented scss:
.rtl-switch {
  /* make it single-line */
  white-space:nowrap;
  /* truncate by container's size */
  overflow-x: hidden;
  /* add three dots */
  text-overflow: ellipsis;
  /* on touch or hover */
  &:active, &:hover {
    /* make text have right to left direction */
    direction: rtl;
    /* fix three dots overlapping issue*/
    padding-left:10px;
    &:after {
      /* fix brackets in rtl mode */
      content: "\200E‎";
    }
  }
}

3) marquee (transition left)

Hover over or touch me to see animated scrolling of this string. Fancy but buggy. May be still can be improved.
some other stuff

Pros:

1) Fancy
2) Displays whole string in a limited space

Contras:

1) Bloated HTML (needs two extra spans)
2) Bad readability for long strings
3) Animation bug on mouse out or not truncated string(may be still can be fixed, I’ll see later)
4) Animation can load CPU on heavy page
5) Needs hardcoded offset, or will be scrolled until container is not empty
Let’s see some code:

.marquee {
  /* Single line */
  white-space:nowrap;
  /* Truncate by container size */
  overflow: hidden;
  /* Both spans */
  span {
    /* Allow to set size */
    display: inline-block;
    /* Size same as container when not hovered to allow ellipsis */
    width: 100%;
    /* Second span */
    span {
      /* Turn animation on */
      @include transition(left 4s linear);
      /* Allow position manipulation */
      position: relative;
      /* truncate by container's size */
      overflow-x: hidden;
      /* add three dots */
      text-overflow: ellipsis;
      /* Explicitly declare initial position to animate well */
      left: 0px;
    }   
  }
  /* on touch or hover */
  &:active, &:hover {
    /* Both spans */
    span {
      /* resize to contain whole string without truncation. */
      width: auto;
      /* Second span */
      span {
        /* Animated scroll by length of first span (100% of parent),
  which equals to length of string, not a container.
  Also shift final point back by width of container (500px)
  to not finish with empty box
  and pitch by 15px - I don't know why, may be to compensate paddings */
        left:calc(500px - 15px - 100%);//
      }
    }    
  }

Here is a codepen to play:

See the Pen Expand cropped string on hover/touch, pure CSS by Yuri Gor (@yurigor) on CodePen.0

Pan and Zoom in jsPlumb Community Edition with Dagre and jQueryUI Draggable

See the Pen Pan and Zoom in jsPlumb Community Edition with Dagre and jQueryUI Draggable by Yuri Gor (@yurigor) on CodePen.0

Chart with draggable HTML elements as nodes, connected by jsPlumb library.
“Pan&Zoom” feature missing in Comunity Edition implemented by using “jQuery Panzoom” plugin.
Nodes dragging implemented by jQueryUI Draggable, to compensate scale distortion.
Dagre layout library used for demonstration.

I use static predefined html in this example:

<!-- .container - just part of your page,
where you want to render diagram -->
<div class="container">
  <!-- .panzoom - wrapper div, panzoom plugin will transform it.
        Use it for worksheet element styling -->
  <div class="panzoom">
    <!-- .diagram - wrapper div to be used by jsPlumb.
         It will have zero height, so no visual CSS works here. -->
    <div class="diagram">
      <!-- .item - diagram nodes, must have unique id's
           to be able connect them by jsPlumb. -->
      <div id="i0"  class="item">Root</div>
      <div id="i1" class="item">Child 1</div>
      <div  id="i11" class="item">Child 1.1</div>
      <div  id="i12" class="item">Child 1.2</div>
      <div id="i2" class="item">Child 2</div>
      <div id="i21" class="item">Child 2.1</div>
      <div id="i3" class="item">Child 3</div>
    </div>
  </div>
</div>

Links between nodes declared in js array:

var links = [
  { from: "i0", to: "i1" },
  { from: "i1", to: "i11" },
  { from: "i1", to: "i12" },
  { from: "i0", to: "i2" },
  { from: "i2", to: "i21" },
  { from: "i0", to: "i3" },
];

Initializing of panzoom:

$panzoom = $container.find('.panzoom').panzoom({
      minScale: minScale,//0.4
      maxScale: maxScale,//2
      increment: incScale,//0.1
      cursor: "",/*empty string prevents panzoom
          from changing cursor styles defined in your css.*/
    }).on("panzoomstart",function(e,pz,ev){
      $panzoom.css("cursor","move");//set "move" cursor on start only
    })
    .on("panzoomend",function(e,pz){
      $panzoom.css("cursor","");//restore cursor
    });

Mouse wheel support and pan while drag begins outside diagram:

$panzoom.parent()
    .on('mousewheel.focal', function( e ) {
      //if Control pressed then zoom
      if(e.ctrlKey||e.originalEvent.ctrlKey)
      {
        e.preventDefault();
        var delta = e.delta || e.originalEvent.wheelDelta;
        var zoomOut = delta ? delta < 0 : e.originalEvent.deltaY > 0;
        $panzoom.panzoom('zoom', zoomOut, {
           animate: true,
           exponential: false,
        });
      }else{//else pan (touchpad and Shift key works)
        e.preventDefault();
        var deltaY = e.deltaY || e.originalEvent.wheelDeltaY || (-e.originalEvent.deltaY);
        var deltaX = e.deltaX || e.originalEvent.wheelDeltaX || (-e.originalEvent.deltaX);
        $panzoom.panzoom("pan",deltaX/2,deltaY/2,{
          animate: true,
          relative: true,
        });
      }
    })
    //on start store initial offsets and mouse coord
    .on("mousedown touchstart",function(ev){
      var matrix = $container.find(".panzoom").panzoom("getMatrix");
      var offsetX = matrix[4];
      var offsetY = matrix[5];
      var dragstart = {x:ev.pageX,y:ev.pageY,dx:offsetX,dy:offsetY};
      $(ev.target).css("cursor","move");
      $(this).data('dragstart', dragstart);
    })
    //calculate mouse offset from starting pos and apply it to panzoom matrix
    .on("mousemove touchmove", function(ev){
      var dragstart = $(this).data('dragstart');
      if(dragstart)
      {
        var deltaX = dragstart.x-ev.pageX;
        var deltaY = dragstart.y-ev.pageY;
        var matrix = $container.find(".panzoom").panzoom("getMatrix");
        matrix[4] = parseInt(dragstart.dx)-deltaX;
        matrix[5] = parseInt(dragstart.dy)-deltaY;
        $container.find(".panzoom").panzoom("setMatrix",matrix);
      }
    })
    .on("mouseup touchend touchcancel", function(ev){
      $(this).data('dragstart',null);
      $(ev.target).css("cursor","");
    });
  });

Make nodes draggable by jQueryUI/draggable:

var currentScale = 1;
  $container.find(".diagram .item").draggable({
    start: function(e){
      var pz = $container.find(".panzoom");
      //save current scale factor to consider it later
      currentScale = pz.panzoom("getMatrix")[0];
      $(this).css("cursor","move");
      //disable panzoom, to avoid panning while dragging node
      pz.panzoom("disable");
    },
    drag:function(e,ui){
      /*compensate current scale while dragging,
           else pointer and node will have different speeds*/
      ui.position.left = ui.position.left/currentScale;
      ui.position.top = ui.position.top/currentScale;
      //it's possible to have not connected nodes, so let's check it.
      if($(this).hasClass("jsplumb-connected"))
      {
        plumb.repaint($(this).attr('id'),ui.position);
      }
    },
    stop: function(e,ui){
      var nodeId = $(this).attr('id');
      if($(this).hasClass("jsplumb-connected"))
      {
        plumb.repaint(nodeId,ui.position);
      }
      $(this).css("cursor","");
      $container.find(".panzoom").panzoom("enable");
    }
  });