jquery.fancytree
Version:
jQuery tree view / tree grid plugin with support for keyboard, inline editing, filtering, checkboxes, drag'n'drop, and lazy loading
491 lines (442 loc) • 13.7 kB
JavaScript
/*!
* Fancytree Taxonomy Browser
*
* Copyright (c) 2015, Martin Wendt (http://wwWendt.de)
*
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version @VERSION
* @date @DATE
*/
;(function($, window, document, undefined) {
/*globals console, Handlebars */
;
/*******************************************************************************
* Private functions and variables
*/
var taxonTree, searchResultTree, tmplDetails, tmplInfoPane, tmplMedia,
timerMap = {},
USER_AGENT = "Fancytree Taxonomy Browser/1.0",
GBIF_URL = "http://api.gbif.org/v1/",
TAXONOMY_KEY = "d7dddbf4-2cf0-4f39-9b2a-bb099caae36c", // GBIF backbone taxonomy
SEARCH_PAGE_SIZE = 5,
CHILD_NODE_PAGE_SIZE = 200,
glyphOpts = {
map: {
doc: "glyphicon glyphicon-file",
docOpen: "glyphicon glyphicon-file",
checkbox: "glyphicon glyphicon-unchecked",
checkboxSelected: "glyphicon glyphicon-check",
checkboxUnknown: "glyphicon glyphicon-share",
dragHelper: "glyphicon glyphicon-play",
dropMarker: "glyphicon glyphicon-arrow-right",
error: "glyphicon glyphicon-warning-sign",
expanderClosed: "glyphicon glyphicon-menu-right",
expanderLazy: "glyphicon glyphicon-menu-right", // glyphicon-plus-sign
expanderOpen: "glyphicon glyphicon-menu-down", // glyphicon-collapse-down
folder: "glyphicon glyphicon-folder-close",
folderOpen: "glyphicon glyphicon-folder-open",
loading: "glyphicon glyphicon-refresh glyphicon-spin"
}
};
// Load and compile handlebar templates
$.get( "details.tmpl.html", function( data ) {
tmplDetails = Handlebars.compile(data);
Handlebars.registerPartial("tmplDetails", tmplDetails);
});
$.get( "media.tmpl.html", function( data ) {
tmplMedia = Handlebars.compile(data);
Handlebars.registerPartial("tmplMedia", tmplMedia);
});
$.get( "info-pane.tmpl.html", function( data ) {
tmplInfoPane = Handlebars.compile(data);
});
/** Update UI elements according to current status
*/
function updateControls() {
var query = $.trim($("input[name=query]").val());
$("#btnPin")
.attr("disabled", !taxonTree.getActiveNode());
$("#btnUnpin")
.attr("disabled", !taxonTree.isFilterActive())
.toggleClass("btn-success", taxonTree.isFilterActive());
$("#btnResetSearch")
.attr("disabled", query.length === 0);
$("#btnSearch")
.attr("disabled", query.length < 2);
}
/**
* Invoke callback after `ms` miliseconds.
* Any pending action of this type is cancelled before.
*/
function _delay(tag, ms, callback) {
/*jshint -W040:true */
var that = this;
tag = "" + (tag || "default");
if( timerMap[tag] != null ) {
clearTimeout(timerMap[tag]);
delete timerMap[tag];
// console.log("Cancel timer '" + tag + "'");
}
if( ms == null || callback == null ) {
return;
}
// console.log("Start timer '" + tag + "'");
timerMap[tag] = setTimeout(function(){
// console.log("Execute timer '" + tag + "'");
callback.call(that);
}, +ms);
}
/**
*/
function _callWebservice(cmd, data) {
return $.ajax({
url: GBIF_URL + cmd,
data: $.extend({
}, data),
cache: true,
headers: { "Api-User-Agent": USER_AGENT },
dataType: "jsonp"
});
}
/**
*/
function updateItemDetails(key) {
$("#tmplDetails").addClass("busy");
$.bbq.pushState({key: key});
$.when(
_callWebservice("species/" + key),
_callWebservice("species/" + key + "/speciesProfiles"),
_callWebservice("species/" + key + "/synonyms"),
_callWebservice("species/" + key + "/descriptions"),
_callWebservice("species/" + key + "/media")
).done(function(species, profiles, synonyms, descriptions, media){
// Requests are resolved as: [ data, statusText, jqXHR ]
species = species[0];
profiles = profiles[0];
synonyms = synonyms[0];
descriptions = descriptions[0];
media = media[0];
var info = $.extend(species, {
profileList: profiles.results, // marine, extinct
profile: profiles.results.length === 1 ? profiles.results[0] : null, // marine, extinct
synonyms: synonyms.results,
descriptions: descriptions.results,
descriptionsByLang: {},
media: media.results,
now: new Date().toString()
});
$.each(info.descriptions, function(i, o){
if( !info.descriptionsByLang[o.language] ) {
info.descriptionsByLang[o.language] = [];
}
info.descriptionsByLang[o.language].push(o);
});
console.log("updateItemDetails", info);
$("#tmplDetails")
// .html(tmplDetails(info))
.removeClass("busy");
$("#tmplMedia")
// .html(tmplMedia(info))
.removeClass("busy");
$("#tmplInfoPane")
.html(tmplInfoPane(info))
.removeClass("busy");
$("[data-toggle='popover']").popover();
$(".carousel").carousel();
$("#mediaCounter").text("" + (media.results.length || ""));
// $("[data-toggle='collapse']").collapse();
updateControls();
});
}
/**
*/
function updateBreadcrumb(key, loadTreeNodes) {
var $ol = $("ol.breadcrumb").addClass("busy"),
activeNode = taxonTree.getActiveNode();
if( activeNode && activeNode.key !== key ) {
activeNode.setActive(false); // deactivate, in case the new key is not found
}
$.when(
_callWebservice("species/" + key + "/parents"),
_callWebservice("species/" + key)
).done(function(parents, node){
// Both requests resolved (result format: [ data, statusText, jqXHR ])
var nodeList = parents[0],
keyList = [];
nodeList.push(node[0]);
// Display as <OL> list (for Bootstrap breadcrumbs)
$ol.empty().removeClass("busy");
$.each(nodeList, function(i, o){
var name = o.vernacularName || o.canonicalName;
keyList.push(o.key);
if( "" + o.key === "" + key ) {
$ol.append(
$("<li class='active'>").append(
$("<span>", {
text: name,
title: o.rank
})));
} else {
$ol.append(
$("<li>").append(
$("<a>", {
href: "#key=" + o.key,
text: name,
title: o.rank
})));
}
});
if( loadTreeNodes ) {
// console.log("updateBreadcrumb - loadKeyPath", keyList);
taxonTree.loadKeyPath("/" + keyList.join("/"), function(node, status){
// console.log("... updateBreadcrumb - loadKeyPath " + node.title + ": " + status);
switch( status ) {
case "loaded":
node.makeVisible();
break;
case "ok":
node.setActive();
// node.makeVisible();
break;
}
});
}
});
}
/**
*/
function search(query) {
query = $.trim(query);
console.log("searching for '" + query + "'...");
// Store the source options for optional paging
searchResultTree.lastSourceOpts = {
// url: GBIF_URL + "species/match", // Fuzzy matches scientific names against the GBIF Backbone Taxonomy
url: GBIF_URL + "species/search", // Full text search of name usages covering the scientific and vernacular name, the species description, distribution and the entire classification across all name usages of all or some checklists
data: {
q: query,
datasetKey: TAXONOMY_KEY,
// name: query,
// strict: "true",
// hl: true,
limit: SEARCH_PAGE_SIZE,
offset: 0
},
cache: true
// headers: { "Api-User-Agent": USER_AGENT }
// dataType: "jsonp"
};
$("#searchResultTree").addClass("busy");
searchResultTree.reload(searchResultTree.lastSourceOpts).done(function(result){
// console.log("search returned", result);
if( result.length < 1) {
searchResultTree.getRootNode().setStatus("nodata");
}
$("#searchResultTree").removeClass("busy");
// https://github.com/tbasse/jquery-truncate
// SLOW!
// $("div.truncate").truncate({
// multiline: true
// });
updateControls();
});
}
/*******************************************************************************
* Pageload Handler
*/
$(function(){
$("#taxonTree").fancytree({
extensions: ["filter", "glyph", "wide"],
filter: {
mode: "hide"
},
glyph: glyphOpts,
autoCollapse: true,
activeVisible: true,
autoScroll: true,
source: {
url: GBIF_URL + "species/root/" + TAXONOMY_KEY,
data: {},
cache: true
// dataType: "jsonp"
},
init: function(event, data) {
updateControls();
$(window).trigger("hashchange"); // trigger on initial page load
},
lazyLoad: function(event, data) {
data.result = {
url: GBIF_URL + "species/" + data.node.key + "/children",
data: {
limit: CHILD_NODE_PAGE_SIZE
},
cache: true
// dataType: "jsonp"
};
// store this request options for later paging
data.node.lastSourceOpts = data.result;
},
postProcess: function(event, data) {
var response = data.response;
data.node.info("taxonTree postProcess", response);
data.result = $.map(response.results, function(o){
return o && {title: o.vernacularName || o.canonicalName, key: o.key, nubKey: o.nubKey, folder: true, lazy: true};
});
if( response.endOfRecords === false ) {
// Allow paging
data.result.push({
title: "(more)",
statusNodeType: "paging"
});
} else {
// No need to store the extra data
delete data.node.lastSourceOpts;
}
},
activate: function(event, data) {
$("#tmplDetails").addClass("busy");
$("ol.breadcrumb").addClass("busy");
updateControls();
_delay("showDetails", 500, function(){
updateItemDetails(data.node.key);
updateBreadcrumb(data.node.key);
});
},
clickPaging: function(event, data) {
// Load the next page of results
var source = $.extend(true, {}, data.node.parent.lastSourceOpts);
source.data.offset = data.node.parent.countChildren() - 1;
data.node.replaceWith(source);
}
});
$("#searchResultTree").fancytree({
extensions: ["table", "wide"],
source: [{title: "No Results."}],
minExpandLevel: 2,
icon: false,
table: {
nodeColumnIdx: 2
},
postProcess: function(event, data) {
var response = data.response;
data.node.info("search postProcess", response);
data.result = $.map(response.results, function(o){
var res = $.extend({
title: o.scientificName,
key: o.key
}, o);
return res;
});
// Append paging link
if( response.count != null && response.offset + response.limit < response.count ) {
data.result.push({
title: "(" + (response.count - response.offset - response.limit) + " more)",
statusNodeType: "paging"
});
}
data.node.info("search postProcess 2", data.result);
},
// loadChildren: function(event, data) {
// $("#searchResultTree td div.cell").truncate({
// multiline: true
// });
// },
renderColumns: function(event, data) {
var i,
node = data.node,
$tdList = $(node.tr).find(">td"),
cnList = node.data.vernacularNames ? $.map(node.data.vernacularNames, function(o){
return o.vernacularName;
}) : [];
i = 0;
function _setCell($cell, text){
$("<div class='truncate'>").attr("title", text).text(text).appendTo($cell);
}
$tdList.eq(i++).text(node.key);
$tdList.eq(i++).text(node.data.rank);
i++; // #1: node.title = scientificName
// $tdList.eq(i++).text(cnList.join(", "));
_setCell($tdList.eq(i++), cnList.join(", "));
$tdList.eq(i++).text(node.data.canonicalName);
// $tdList.eq(i++).text(node.data.accordingTo);
_setCell($tdList.eq(i++), node.data.accordingTo);
$tdList.eq(i++).text(node.data.taxonomicStatus);
$tdList.eq(i++).text(node.data.nameType);
$tdList.eq(i++).text(node.data.numOccurrences);
$tdList.eq(i++).text(node.data.numDescendants);
// $tdList.eq(i++).text(node.data.authorship);
_setCell($tdList.eq(i++), node.data.authorship);
// $tdList.eq(i++).text(node.data.publishedIn);
_setCell($tdList.eq(i++), node.data.publishedIn);
},
activate: function(event, data) {
if( data.node.isStatusNode() ) { return; }
_delay("activateNode", 500, function(){
updateItemDetails(data.node.key);
updateBreadcrumb(data.node.key);
});
},
clickPaging: function(event, data) {
// Load the next page of results
var source = $.extend(true, {}, searchResultTree.lastSourceOpts);
source.data.offset = data.node.parent.countChildren() - 1;
data.node.replaceWith(source);
}
});
taxonTree = $("#taxonTree").fancytree("getTree");
searchResultTree = $.ui.fancytree.getTree("#searchResultTree");
// Bind a callback that executes when document.location.hash changes.
// (This code uses bbq: https://github.com/cowboy/jquery-bbq)
$(window).bind( "hashchange", function(e) {
var key = $.bbq.getState( "key" );
console.log("bbq key", key);
if( key ) {
updateBreadcrumb(key, true);
}
}); // don't trigger now, since we need the the taxonTree root nodes to be loaded first
$("input[name=query]").keyup(function(e){
var query = $.trim($(this).val()),
lastQuery = $(this).data("lastQuery");
if(e && e.which === $.ui.keyCode.ESCAPE || query === ""){
$("#btnResetSearch").click();
return;
}
if(e && e.which === $.ui.keyCode.ENTER && query.length >= 2){
$("#btnSearch").click();
return;
}
if( query === lastQuery || query.length < 2) {
console.log("Ignored query '" + query + "'");
return;
}
$(this).data("lastQuery", query);
_delay("search", 1, function(){
$("#btnSearch").click();
});
$("#btnResetSearch").attr("disabled", query.length === 0);
$("#btnSearch").attr("disabled", query.length < 2);
}).focus();
$("#btnResetSearch").click(function(e){
$("#searchResultPane").collapse("hide");
$("input[name=query]").val("");
searchResultTree.clear();
updateControls();
});
$("#btnSearch").click(function(event){
$("#searchResultPane").collapse("show");
search( $("input[name=query]").val() );
}).attr("disabled", true);
$("#btnPin").click(function(event){
taxonTree.filterBranches(function(n){
return n.isActive();
});
updateControls();
});
$("#btnUnpin").click(function(event){
taxonTree.clearFilter();
updateControls();
});
// -----------------------------------------------------------------------------
}); // end of pageload handler
}(jQuery, window, document));