All versions of this documentation
X

Fuzzy search


Open in a new window.
          <!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <script src="../build/ogma.min.js"></script>
  <script src="https://unpkg.com/fuzzy@0.1.3/lib/fuzzy.js"></script>
  <style>
    html, body {
      padding: 0;
      margin: 0;
      font: normal 18px "Helvetica Neue", Helvetica, Arial, sans-serif;
    }

    #graph-container {
      top: 0; bottom: 0; left: 0; right: 0;
      position: absolute; margin: 0; overflow: hidden;
    }
    .toolbar {
      display: block;
      position: absolute;
      top: 20px;
      right: 20px;
      padding: 10px;
      box-shadow:    0 1px 5px rgba(0,0,0,0.65);
      border-radius: 4px;
      background: #ffffff;
      color: #222222;
      font-weight: 300;
      min-width: 35%;
    }

    #search-input {
      width: 100%;
      padding: 1rem;
      font-size: 1rem;
    }

    #search-input:focus {
      outline: 0;
    }

    .match {
      font-weight: bold;
      text-decoration: underline;
    }

    .dropdown {
      position: relative;
    }

    .dropdown-item {
      display: block;
      width: 100%;
      padding: .25rem 1.5rem;
      clear: both;
      font-weight: 400;
      color: #212529;
      text-align: inherit;
      white-space: nowrap;
      background-color: transparent;
      border: 0;

      cursor: pointer;
      box-sizing: border-box;
    }

    .dropdown-item:hover, .dropdown-item.hover {
      background: #dddddd;
    }

    .dropdown-menu {
      position: absolute;
      top: 100%;
      right: 0;
      z-index: 1000;
      display: block;
      float: left;
      min-width: 10rem;
      padding: .5rem 0;
      margin: .125rem 0 0;
      font-size: 1rem;
      color: #212529;
      text-align: left;
      list-style: none;
      background-color: #fff;
      background-clip: padding-box;
      border: 1px solid rgba(0,0,0,.15);
      border-radius: .25rem;

      max-height: 500px;
      overflow-y: auto;
    }


  </style>
</head>
<body>
  <div id="graph-container"></div>
  <div class="toolbar">
    <input type="search" placeholder="Search station" id="search-input" />
    <div id="results" class="dropdown">
    </div>
  </div>
<script>

var ogma = new Ogma({
  container: 'graph-container'
});

// add highlight style rule
ogma.createClass('highlighted', {
  nodeAttributes: {
    radius: function (node) { return node.getAttribute('radius') * 1.5; },
    halo: {
      color: '#cccccc',
      size: 20
    }
  }
});

function highlightNode(id) {
  ogma.getNodes().removeClass('highlighted');
  if (id) {
    ogma.getNode(id).addClass('highlighted');
  }
}

function selectNode(id) {
  ogma.getSelectedNodes().setSelected(false);
  if (id) {
    ogma.getNode(id).setSelected(true);
  }
}

var search = {};
function init(graph) {
  var resultsContainer = document.querySelector('#results');
  var currentItem = 0; // current item selected in the list
  var input = document.querySelector('#search-input');

  // store raw data
  search.data  = graph;
  // expose search procedure
  search.query = function(value) {
    // perform query using fuzzy.js
    var results = fuzzy.filter(value, this.data.nodes, {
      // highlight the matching substring
      pre: '<span class="match">',
      post: '</span>',
      extract: function (item) {
        // extract the text name to display in the list
        return item.data.latin_name;
      }
    });

    // render the list HTML
    resultsContainer.innerHTML =
    '<div class="dropdown-menu">'
      + (results.map(function (result, i) {
        console.log(result);
        return '<div class="dropdown-item '
            + ((i === currentItem) ? 'hover' : '') // current item
            + ' data-position="' + i + '"'
            + ' data-index="'    + result.index + '"'
            + ' data-value="'    + result.original.data.latin_name + '"'
            + ' data-id="'       + result.original.id + '">'
            + result.string + '</div>';
        }).join(''))
    + "</div>";
    highlightNode(results[currentItem].original.id);
  };


  // listen for input
  input.addEventListener('keyup', function (evt) {
    if (evt.keyCode === 40) {         // down arrow
      currentItem++;
    } else if (evt.keyCode === 38) {  // up arrow
      currentItem--;
    } else if (evt.keyCode === 13) {  // enter
      var item = resultsContainer.querySelectorAll('.dropdown-item')[currentItem];
      input.value = item.getAttribute('data-value');
      selectNode(item.getAttribute('data-id'));
    }

    // wait for the next frame to throttle the search
    Ogma.utils.requestAnimFrame(function () {
      search.query(input.value);
    });
  });


  // highlight on hover
  resultsContainer.addEventListener('mousemove', function (evt) {
    Ogma.utils.requestAnimFrame(function () {
      var id = evt.target.getAttribute('data-id');
      currentItem = evt.target.getAttribute('data-position') || 0;
      highlightNode(id);
    });
  });

  // select on click
  resultsContainer.addEventListener('click', function (evt) {
    var id = evt.target.getAttribute('data-id');
    currentItem = evt.target.getAttribute('data-position') || 0;
    input.value = evt.target.getAttribute('data-value');
    selectNode(id);
  });

  return graph;
}


ogma.parse.jsonFromUrl('files/paris-metro.json') // load data
  .then(init) // init search
  .then(function (graph) { return ogma.setGraph(graph); }) // add graph to ogma
  .then(function () { return ogma.view.locateGraph(); }) // position
  .then(function () { // trigger up the search
    var input = document.querySelector('#search-input');
    input.value = 'Lou';
    search.query('Lou');
    input.focus();
  });

</script>
</body>
</html>