📜 ⬆️ ⬇️

Make a "mindmap" on Javascript with local storage in the browser database


This is a small learning example of a memory card editor. Due to very detailed comments and simple code, it will not be a problem to understand it. This article is intended for those who know and learn Javascript.

I will describe the features of creating a memory card editor that uses a browser database. Moreover, it will not be LocalStorage, which can not exceed 5 megabytes. The amount of data can exceed 100-200 megabytes, as used by IndexedDB or webSQL, depending on what is available in a particular browser.

Sources are available on Github .
')
We will be laid in 520 lines of code, while in our map you can drag the nodes between them, delete, rename and create new ones. And also it will be possible to assign one of 120 icons through the context menu.

The secret of minimalism is that we will use plug-ins tested in battle :
  1. Ydn.db - storing information in a browser database with automatic selection of the best method and a single API
  2. jQuery context menu - context menu that can be dynamically populated with Javascript
  3. jsPlumb - an extension that allows you to draw lines between HTML elements
  4. jQuery UI - Drag & drop - drag and drop items between themselves


PS: We will also learn how to create a “singleton”, facilitate asynchronous programming with jQuery and the built-in object $ .Deferred (), as well as with the LiveReload plugin, save the paint on the F5 key when changing CSS properties and code in HTML and JavaScript.


Program code with comments for those in a hurry

Many may not continue to read, but simply get acquainted with the code.
Main Javascript code with very detailed comments.
var API_4_MINDMAP = function(){ //singleton -      if ( (typeof arguments.callee.instance=='undefined') ) { //     arguments.callee.instance = new function() { var this_api = this; //  ,     var my_all_data = {}; //    var my_all_data_template = { //  ,     "n1":{ id:1, parent_id:0, title:" <br> <br>"+ "  <br> .<br>Javascript" }, "n2":{ id:2, parent_id:1, title:"", icon:"icon-gift" }, "n3":{ id:3, parent_id:2, title:"   ", icon:"icon-flow-line" }, "n5":{ id:5, parent_id:3, title:"  jsPlumb", icon: "icon-link" }, "n4":{ id:4, parent_id:3, title:"  -  SVG" }, "n7":{ id:7, parent_id:6, title:"  jQuery ContextMenu", icon: "icon-link" }, "n8":{ id:8, parent_id:1, title:" ", icon: "icon-lamp" }, "n9":{ id:9, parent_id:8, title:"Javascript + jQuery — 520 " }, "n10":{ id:10, parent_id:2, title:"   ", icon: "icon-floppy-1" }, "n11":{ id:11, parent_id:17, title:"IndexedDB" }, "n12":{ id:12, parent_id:17, title:"webSQL" }, "n13":{ id:13, parent_id:17, title:"LocalStorage" }, "n14":{ id:14, parent_id:10, title:"  Ydn.db", icon: "icon-link" }, "n15":{ id:15, parent_id:10, title:"   " }, "n16":{ id:16, parent_id:2, title:"   Javascript", icon: "icon-cd" }, "n17":{ id:17, parent_id:10, title:"" }, "n18":{ id:18, parent_id:6, title:"  " }, "n20":{ id:20, parent_id:8, title:"CSS — 220 " }, "n19":{ id:19, parent_id:8, title:"HTML — 50 " }, "n22":{ id:22, parent_id:16, title:"    " }, "n23":{ id:23, parent_id:16, title:"   " }, "n24":{ id:24, parent_id:2, title:"  ", icon: "icon-emo-wink" }, "n6":{ id:6, parent_id:2, title:" ", icon: "icon-list" }, "n25":{ id:25, parent_id:24, title:"   Fontello", icon: "icon-link" }, "n27":{ id:27, parent_id:2, title:"Drag&Drop jQuery UI", icon: "icon-link" }, "n26":{ id:26, parent_id:24, title:"    " } }; this.jsSaveAllToDB = function() { //      $.each(my_all_data, function(i, el){ db.put("mindmap_db", el ).done(function(){ }); }); } this.jsLoadAllFromDB = function() { //          var d=new $.Deferred(); //    my_all_data = {}; //  db.values("mindmap_db",null,99999999).done(function(records) { if(records.length && false) { $.each(records, function(i, el){ my_all_data["n"+el.id] = {}; my_all_data["n"+el.id] = el; }); } else { //   ,          my_all_data = my_all_data_template; this_api.jsSaveAllToDB(); } d.resolve(); // ,     done }); return d.promise(); //,    ,    } this.jsFind = function(id, changes) { //   id     //    ,  n     id var answer = my_all_data["n"+id]; if(!answer) return false; //     if(changes) { //   ,     $.each(changes, function(name_field, new_field_value){ answer[name_field] = new_field_value; }); db.put("mindmap_db", answer ).done(function(){ //      console.info("     "); //    }); } return answer; } this.jsFindByParent = function(parent_id) { //    parent_id var answer = []; $.each(my_all_data, function(i,el){ //      = parent_id if((el.parent_id == parent_id) && (!el.del)) answer.push(el); }); return answer; } this.jsAddNew = function(parent_id, title) { //    parent_id var max_id = 0; $.each(my_all_data, function(i,el){ //  id if(el.id>max_id) max_id = el.id; }); var new_id = (parseInt(max_id)+1); //  id my_all_data["n"+new_id] = {}; //   my_all_data["n"+new_id] = {id:new_id, parent_id: parent_id, title: title}; //  return new_id; } //   ,     this.jsRecursiveByParent = function(id, recursive_array) { if(!recursive_array) recursive_array = []; var answer = this_api.jsFindByParent(id); $.each(answer,function(i,el) { //      ,    recursive_array.push(el); recursive_array = this_api.jsRecursiveByParent(el.id, recursive_array); }); return recursive_array; } this.jsDeleteById = function(id) { //       if(confirm("  №"+id+"   ?")) { var childs = this_api.jsRecursiveByParent(id); $.each(childs, function(i, el){ api4mindmap.jsFind(el.id, {del:1}); //"" -       }); if(id!=1) api4mindmap.jsFind(id, {del:1}); // ,    №1 } } this.jsRenderAllMap = function(focus_id) { //      if(!focus_id) focus_id = 1; var html = "<ul myid='"+focus_id+"'>"; html = this_api.jsRenderOneParent(focus_id, html); //  html += "</ul>"; $("#mindmap").html(html); jsMakeDroppable(); //    } this.jsRenderOneParent = function(parent_id, html) { //     html += "<li id='node_"+parent_id+"' myid='"+parent_id+"'>"; html += "<div class='big_n_title'>"; html += this_api.jsRenderOneElement(parent_id); //   html += "</div>"; var childs = this_api.jsFindByParent(parent_id); //   if(childs.length) { html += "<ul class='childs' myid='"+parent_id+"'>"; } $.each(childs, function(i,el){ html = this_api.jsRenderOneParent(el.id,html); //   ,    }); if(childs.length) { html += "</ul>"; } html += "</li>"; return html; } this.jsRenderOneElement = function(id) { //   var element = this_api.jsFind(id); //  var childs_count = this_api.jsFindByParent(id).length; //-    var icon_type = ''; if(element.icon) icon_type = element.icon; //  ,   if(childs_count>0) { //   var collapser_html = "<div class='collapse'></div>"; //   ,   var icon = "<div class='type_icon'><i class='icon-folder-1 folder'><div class='count'>"+ childs_count+"</div></i><i class='"+icon_type+"'></i>"+"</div>"; } else { var collapser_html = ""; var icon = "<div class='type_icon'><i class='"+icon_type+"'></i></div>"; } var answer = icon+"<div class='n_title' contenteditable='true'>"+element.title+ "</div><div class='contextmenu'></div>"+collapser_html; return answer; } this.jsDrawMindmap = function(focus_id) { //     var line_cache = []; $("#mindmap ul:visible").each(function(){ //   ul var ul_id = $(this).attr("myid"); var childs = this_api.jsFindByParent(ul_id); $.each(childs, function(i,el){ // ,      var target = el.id; if(!$("li[myid='"+target+"']"+" .big_n_title:first").hasClass("_jsPlumb_endpoint_anchor_")) { var parent_id = el.parent_id; line_cache.push( {source: parent_id, target: target} ); } }); }); if(line_cache.length) { //   ,     if(!myjsPlumb.isSuspendDrawing()) { myjsPlumb.setSuspendDrawing(true, true); console.info("set_suspend"); } } $.each(line_cache, function(i, el){ if(el.source == 1) { //        anchor1 = [ 1, 0.5, 1, 0, -1, -1 ]; } else { anchor1 = [ 1, 1, 1, 0, -1, -1 ]; //    } //   : var p1 = myjsPlumb.addEndpoint("node_"+el.source+" .big_n_title:first", { anchor: anchor1 }); //   : var p2 = myjsPlumb.addEndpoint("node_"+el.target+" .big_n_title:first", { anchor: [ 0, 1, -1, 0, 1, -1 ]}); //   : var count = this_api.jsFindByParent(el.source).length; if(count>10) { //  ,     var LineType = "Straight"; } else { var LineType = "Bezier"; //   } //  ,    myjsPlumb.connect({source: p1, target: p2, scope:"someScope", deleteEndpointsOnDetach:true, connector:[ LineType, { curviness: 30, cornerRadius: 20 } ]}); }); } //jsDrawMindmap this.jsRefreshMindmap = function() { //         myjsPlumb.reset(); //   var save_scroll_top = $("#mindmap").scrollTop(); //  ,   var save_scroll_left = $("#mindmap").scrollLeft();//     var hidden_elements = []; //    $(".hide").each(function(){ hidden_elements.push($(this).attr("myid")); }); api4mindmap.jsRenderAllMap(1); //    $.each(hidden_elements, function(i, el){ // ,    . $("#node_"+el).addClass("hide"); }); api4mindmap.jsDrawMindmap(1); // ,      onResize(); //    $("#mindmap").scrollTop(save_scroll_top); //  ,   $("#mindmap").scrollLeft(save_scroll_left);//     } this.jsRegAllKeys = function() { //    $("#mindmap").on("keydown", ".n_title", function(e){ //  Enter if(e.keyCode==13) { e.preventDefault(); $(this).blur(); // ,      } }); $("#mindmap").on("keyup", ".n_title", function(e){ e.preventDefault(); if(e.keyCode==13) $(this).blur(); onResize(); // ,   ,  ,  }); $("#mindmap").on("blur", ".n_title", function(){ //  ,   var n_title_text = $(this).html(); var id = $(this).parents("li:first").attr("myid"); if(n_title_text.length==0) n_title_text = " "; //  ,   . $(this).html( strip_tags(n_title_text) ); //     this_api.jsFind(id, {title:n_title_text}); //        onResize(); //  }); $("#mindmap").on("click", ".n_title", function(){ //   ,  $(this).focus(); }); $("#mindmap").on("focus", ".n_title", function(){ // ,    var ntitle = $(this); setTimeout(function(){ if(ntitle.is(":focus")) document.execCommand('selectAll',false,null); },3); //        Firefox }); $("#mindmap").on("click", ".collapse", function(){ //     $(this).parents("li:first").toggleClass("hide"); //  api4mindmap.jsDrawMindmap(1); // ,   onResize(); return false; }); var font_size = 14; //   $("#zoom_in").on("click", function(){ //   font_size += 1; $("#mindmap").css("font-size", font_size+"px"); onResize(); return false; }); $("#zoom_out").on("click", function(){ //   font_size -= 1; $("#mindmap").css("font-size", font_size+"px"); onResize(); return false; }); $("#collapse_all").on("click", function(){ // "  " $("#node_1 ul li").addClass("hide"); onResize(); return false; }); $("#expand_all").on("click", function(){ // "  " $("#node_1 ul li").removeClass("hide"); onResize(); return false; }); } //jsRegAllKeys } } return arguments.callee.instance; //   } function onResize() { myjsPlumb.setSuspendDrawing(false, true); //   } function jsGetIcons(n) { //    var icons = {}; icons[0] = ["progress-0","progress-1","progress-2","progress-3","dot","dot-2","dot-3","star-empty","star","record"]; icons[1] = ["check","heart-empty","heart","bookmark-empty","bookmark","ok-2","help","wallet","mail-2","cloud"]; icons[2] = ["tree","chat-2","article-alt","volume","flash","aperture-alt","layers","steering-wheel","skiing","flight"]; icons[3] = ["lock-open","lock","umbrella","camera","book-open","clock-1","plus","minus","trash","music"]; icons[4] = ["calculator","address","pin","vcard","basket-1","swimming","youtube","leaf","mic","target"]; icons[5] = ["monitor","phone","download","bell","at","pause","play","stop-1","flag","key"]; icons[6] = ["users-1","eye","inbox","brush","moon","college","fast-food","coffee","top-list","bag"]; icons[7] = ["chart-area","info","home-1","hourglass","attention","scissors","tint","guidedog","archive","flow-line"]; icons[8] = ["emo-grin","emo-happy","emo-wink","emo-sunglasses","emo-thumbsup","emo-sleep","emo-unhappy","emo-devil","emo-surprised","emo-tongue"]; icons[9] = ["plus","minus","keyboard","fast-fw","to-end","to-start","cancel-circle","check","flash","feather"]; icons[10] = ["plus-circle","pencil-alt","target-1","chart-pie","adjust","user-add","volume","install","flow-cascade","sitemap"]; icons[11] = ["minus-circle","clock-1","light-down","light-up","lamp","upload","picture-2","dollar","gift","link-1"]; answer = {}; $.each(icons, function(j, icon_group){ sub_icons = {}; $.each(icons[j], function(i, icon){ sub_icons["icon-"+icon] = {}; sub_icons["icon-"+icon] = {name:icon, icon: "icon-"+icon}; }); answer["icon-group"+icon_group] = {}; answer["icon-group"+icon_group] = {name:" №"+(parseInt(j)+1), icon: "icon-"+icons[j][0], items: sub_icons}; }); return answer; //     } function jsMakeDroppable() { //    $(".n_title").not("ui-draggable").draggable({ zIndex: 999, delay:50, revert: false, // will cause the event to go back to its helper:"clone", appendTo: "body", refreshPositions:true }); $( ".n_title" ).not("ui-droppable").droppable({ accept: ".n_title", activeClass: "ui-can-recieve", tolerance: "pointer", hoverClass: "ui-can-hover", over: function (event, ui) { //$(this).click(); }, drop: function( event, ui ) { //console.info("drop-all",usedOverlays,ui,ui.draggable[0] ); var my_draggable = $(ui.draggable[0]); var my_droppable = $(event.target); my_draggable_id = my_draggable.parents("li:first").attr("myid"); my_droppable_id = my_droppable.parents("li:first").attr("myid"); if( jsCanDrop(my_draggable_id, my_droppable_id) ) { //,    api4mindmap.jsFind(my_draggable_id, {parent_id:my_droppable_id}); api4mindmap.jsRefreshMindmap(); $(".ui-draggable-dragging").remove(); //  ,   } else { alert("      "); } } }); } function jsCanDrop(draggable_id, droppable_id) { // ,        var can_drop = true; var all_childs = api4mindmap.jsRecursiveByParent(my_draggable_id); $.each(all_childs, function(i,el){ console.info(el.id, droppable_id); if(el.id == droppable_id) can_drop = false; }); if(draggable_id == droppable_id) var can_drop = false; return can_drop; } //      function strip_tags( str ){ if(!str) return ""; answer = str.replace(/<\/?[^>]+>/gi, ''); answer = answer.replace(/\n/gi, ''); return answer; } var myjsPlumb; //    ///////////////////////   html //////////////////////// function jsDoFirst() { api4mindmap = new API_4_MINDMAP(); //  api  "" jsPlumb.Defaults.Container = $("#mindmap"); // ""  myjsPlumb = jsPlumb.getInstance({ DragOptions: { cursor: 'pointer', zIndex: 2000 }, PaintStyle:{ lineWidth:1, strokeStyle:"#888" }, Connector:[ "Bezier", { curviness: 30 } ], Endpoint:[ "Blank", { radius:5 } ], EndpointStyle : { fillStyle: "#567567" }, Anchors : [[ 1, 1, 1, 0, -1, -1 ],[ 0, 1, -1, 0, 1, -1 ]] }); var icons_html = jsGetIcons(0); //         $.contextMenu({ //          .contextmenu selector: '.contextmenu', trigger: 'left', callback: function(key, options) { var id = $(this).parents("li:first").attr("myid"); if( /icon-/ig.test(key) ) { //  api4mindmap.jsFind(id, {icon:key}); api4mindmap.jsRefreshMindmap(); } else if(key == "delete") { //    api4mindmap.jsDeleteById(id); api4mindmap.jsRefreshMindmap(id); } else if(key == "add_down") { //  var parent_id = api4mindmap.jsFind(id).parent_id; var new_id = api4mindmap.jsAddNew(parent_id, " "); api4mindmap.jsRefreshMindmap(); $("#node_"+new_id+" .n_title").focus(); } else if(key == "add_right") { //  var new_id = api4mindmap.jsAddNew(id, " "); $(this).parents("li").removeClass("hide"); api4mindmap.jsRefreshMindmap(); $("#node_"+new_id+" .n_title").focus(); } }, delay:0, items: { "add_down": {"name":" ", "icon": "icon-down-1"}, "add_right": {"name":" ", "icon": "icon-right-1"}, "sep1": "--------", "delete": {"name":"", "icon": "icon-trash"}, "context_make_did1011": {"name": "", "icon": "icon-emo-wink", "items": icons_html //     } } }); //    var mindmap_store_schema = { //   name: "mindmap_db", //  keyPath: 'id', //       , autoIncrement: false }; var schema = { //   stores: [mindmap_store_schema] }; if( navigator.userAgent.toLowerCase().indexOf("android") !=-1 ) { var options = {mechanisms: ['websql', 'indexeddb']}; // websql   } else { var options = {}; // indexeddb    ),      } db = new ydn.db.Storage('_all_mindmap', schema, options); //    api4mindmap.jsLoadAllFromDB().done(function(){ //        api4mindmap.jsRegAllKeys(); //  api4mindmap.jsRenderAllMap(1); //    №1 api4mindmap.jsDrawMindmap(1); // ,      onResize(); //  }); //       } 



Tools that I use when programming

For now, let's postpone the study of the code. First, I will briefly describe the tools I use. Many newcomers do not know about them and lose time on it.

I program in Coda , and sometimes in Sublime Text . Coda is more familiar, but it slows down a little while coloring the code (it is only for Mac), and Sublime Text is very fast and works on any platform, but, first, I am not used to it, and second, I like to log in directly to Coda to quickly fix multiple files. And I use the terminal to communicate with Debian through Coda.

A real breakthrough in the speed of work for me was the opening of GIT . This is a version control system. I use it through the official GitHub program:


I use Github as a cloud storage of my code, and I also publish releases using it. To “release” the version of my site, I do this:
  1. I check the work of the site on a local machine and make a “commit”, i.e. I confirm the changes (see the picture above, the Commit button)
  2. “Push” changes to the server with one button (there is a Sync button in the same window below)
  3. I go to the terminal on my server in Germany via SSH and run the command:
    git pull https: // myuser_name: mysslpassword@github.com/Imater/tree.git master
  4. I run a script that compresses all JS and CSS files, throwing comments and ZIP files out of them into one js file and one css file. The fewer files you insert into your HTML page, the faster the site loads.


While working with the code, I always have Chrome and its console open. In the console, you can try the created functions, play around with the data and debug the code using the built-in debugger:


Learn how to work with the console and Developer Tools and save a lot of time.

Now let's talk about the paint on the F5 key (in the case of MacOS - cmd + r). Most recently discovered LiveReload . There is a version for both Win and Mac.

Install the program, then install the plugin under Chrome or another browser, or paste the code immediately after the body tag in the main HTML file:
 <script>document.write('<script src=\"http://' + (location.host || 'localhost').split(':')[0] + ':35729/livereload.js?snipver=1\"></' + 'script>')</script> 


And after that you get a lot of pleasure. The scenario is this: open your website, which you program, then write code in CSS or Javascript or replace the images in the folder that “monitors” livereload, and as soon as you save it, the website is immediately updated. If you replace CSS, the changes are applied instantly without reloading the page.

Well, in the end, descriptions of the tools, I want to recommend a PNG image reducer, which saves up to 70% of the volume and, thereby, speeds up the loading of the site.

Description of the program code for creating a memory card

All code is laid out on Github . You can download it and deploy to any folder. All you need is already in the repository. All plugins in the folder. Comments in the code are very detailed.

Let's start with the basics. I love to wrap all the many features in a singleton . I do this as follows:
 var API_4_MINDMAP = function(){ //singleton -      if ( (typeof arguments.callee.instance=='undefined') ) { //     arguments.callee.instance = new function() { var my_all_data = {}; var this_api = this; //  ,      this.jsAlert = function( name ) { if( prompt("Hello "+name+", are you ok?") ) { var is_ok = true; } else { var is_ok = false; } my_all_data[name] = {}; my_all_data[name] = {name: name, hi_is_ok: is_ok}; return my_all_data; } } return arguments.callee.instance; //   } api4mindmap = new API_4_MINDMAP(); //  api  "" console.info( api4mindmap.jsAlert("Habrahabr") ); 

The advantage of this approach is the ability to save large data arrays inside such a "singleton" and not be afraid that the user or any plugin will change the data. We isolate functions and variables. And from practice, I like to type in the console: “api4mindmap.js ...” and wait for a pop-up list of all the functions that I created. Quick and convenient.

At one time it was a discovery for me that in the above example, the data in the array my_all_data is stored between function calls.

Now let's talk about the plugins we use:

Ydn.db - browser database


This is a jQuery plugin that allows you to save and read data from local browser databases using a single api. It also works on any platforms, including mobile ones. Even in Phonegap.

I do not use most of its functions, but I use it mainly in the following cases:
  this.jsLoadAllFromDB = function() { //          var d=new $.Deferred(); //    my_all_data = {}; //  db.values("mindmap_db",null,99999999).done(function(records) { if(records.length) { $.each(records, function(i, el){ my_all_data["n"+el.id] = {}; my_all_data["n"+el.id] = el; }); } else { //   ,          my_all_data = my_all_data_template; this_api.jsSaveAllToDB(); } d.resolve(); // ,     done }); return d.promise(); //,    ,    } this.jsFind = function(id, changes) { //   id     //    ,  n     id var answer = my_all_data["n"+id]; if(!answer) return false; //     if(changes) { //   ,     $.each(changes, function(name_field, new_field_value){ answer[name_field] = new_field_value; }); db.put("mindmap_db", answer ).done(function(){ //      console.info("     "); //    }); } return answer; } 

This is an example from the code for the memory card editor. Here are very simple commands: db.values ​​("mindmap_db", null, 99999999) - reads all the elements from the IndexedDb table (if in Chrome) and returns after some time to a function that is in the .done () parameters. So you can read items from a database. One element can be counted using the command: db.get (“mindmap_db”, 5) - this is how you consider the element with id = 5.

To write to the database, use the command: db.put ("mindmap_db", answer). Since this command is asynchronous and the browser does not wait for its execution, it does not slow down. That is why this program will work faster in Chrome.

To work with asynchronous commands, I use the $ .Deferred () object built into jQuery. An example is given above. You simply promise at the end of the function that you will return the result as soon as, then immediately, with the help of the command: return d.promise (x); And then when you have everything in the function completed, for example, the data is sent to the server, you perform d.resolve (200); Then the .done () function is executed and the x parameter is passed to it.

This is very convenient, since it allows you not to write functions inside each other. And also, I recommend learning the $ .when command. I use it when we have a lot of asynchronous functions that we started at the same time and want to do something right after the completion of all asynchronous functions. Here is an example:
 function jsDo() { var dfdArray = []; for(var i=0; i<1000; i++) { dfdArray.push( jsAsync() ); } $.when.apply(null, dfdArray).then( function(){ alert("  ") } ); } function jsAcync() { var d = new $.Deferred(); setTimeout(function(){ d.resolve(); }, Math.random()*5000 ); return d.promise(); } 

In fact, all the “promises” are recruited into the array and transferred to the $ .when command, which performs the “.done” function exactly at the moment when the last promise is fulfilled.

Ydn.db allows you to work with data without thinking about the methods of storing them in the browser, as it wraps their diversity into a single api. It also allows you to work with indices, do selections, apply filters, calculate sums in tables, and so on. But in this example, we use it only for storing and reading data, and the role of the index is performed by the array my_all_data, this provides a very high speed. At the same time, data security is ensured by the fact that the function api4mindmap.jsFind (id, {title: “new_title”}) updates this array immediately, and sends data asynchronously to the database. But, nevertheless, you can change the host header in the memory card and immediately update the browser, and all the data will be saved. The database in the browser is fast, reliable and has the ability to store more than 100 megabytes of information.

After examining Ydn.db, you can forget about the 5 megabytes of LocalStorage restrictions. The only thing that the user will ask permission to store data every 5 megabytes (Chrome does not ask).

This plugin works fine on Android and iOS. But remember, in order to avoid mistakes, you may have to disable the IndexedDB selection in Android. Remarkably, in the latest versions of Internet Explorer IndexedDB is used, which speeds up the work of such applications due to asynchrony. If you save 1000 items at once, they will do it in parallel, but at different speeds, which is faster than sequential recording.

jsPlumb - draw SVG lines in any browser


Canvas could be used to draw lines between different html page elements, but it has many drawbacks. You would have to create a Canvas of significant size, for example 4000 x 4000 px, which would lead to an unstable browser. Lines would be raster and on modern Retina screens, would look worse than vector ones. And the worst thing is that all these 16 million pixels would require a redraw every time you enter a letter into a memory card.

jsPlumb draws each line in the SVG and places it in the right place, giving it the absolute CSS property and calculated coordinates. It is possible to instantly redraw all the lines in one command. In fact, you only need to specify the start and end point once, then connect them with a line and you can forget about the plugin. Almost 3 teams and you know how to handle lines.

On their website there is an example where each element can be moved with a mouse, while the lines move after the element without slowing down.


Learn, useful for drawing: hierarchical structures, simple graphs, tutorials (which show a line with an arrow on the element about which they are talking about), organizational structures of enterprises, diagrams, and so on. It works in almost all browsers, including iOS and Android.

jQuery context menu - context menu

All the menu plugins I've seen are used to create their own html hierarchy. And in this plugin you can create a structure in a simple, almost JSON format, using an array of objects.


You can set a set of commands in the menu using this code:
  $.contextMenu({ //     .contextmenu selector: '.contextmenu', trigger: 'left', callback: function(key, options) { var id = $(this).parents("li:first").attr("myid"); if( /icon-/ig.test(key) ) { //  api4mindmap.jsFind(id, {icon:key}); api4mindmap.jsRefreshMindmap(); } else if(key == "delete") { //    api4mindmap.jsDeleteById(id); api4mindmap.jsRefreshMindmap(id); } else if(key == "add_down") { //  var parent_id = api4mindmap.jsFind(id).parent_id; var new_id = api4mindmap.jsAddNew(parent_id, " "); api4mindmap.jsRefreshMindmap(); $("#node_"+new_id+" .n_title").focus(); } else if(key == "add_right") { //  var new_id = api4mindmap.jsAddNew(id, " "); $(this).parents("li").removeClass("hide"); api4mindmap.jsRefreshMindmap(); $("#node_"+new_id+" .n_title").focus(); } }, delay:0, items: { "add_down": {"name":" ", "icon": "icon-down-1"}, "add_right": {"name":" ", "icon": "icon-right-1"}, "sep1": "--------", "delete": {"name":"", "icon": "icon-trash"}, "context_make_did1011": {"name": "", "icon": "icon-emo-wink", "items": icons_html //     } } }); 


Pay attention to the item items, everything is very clear and beautiful. Moreover, to store all the functions called by the context menu together in the callback function, allows less confusing.

The only thing I had to do was to tweak the plug-in scripts so that it did not draw raster icons, but icons from the Fontello font.

The main advantage of this context menu is that you can control the keys to select items and even assign hotkeys to certain commands. All the benefits become clear after studying the demonstration at the office .

Fontello - font with vector icons


Here, as in a supermarket, go to the site and type those icons that can be useful to you:


After that, download the archive and insert a link to the CSS file in your main html file. Since then, regardless of the browser, you use these icons like this: - this html code will turn into an icon.

The icons themselves can be evaluated in a demonstration of a memory card by going to the context menu. There are 120 icons in their natural habitat.

jQuery UI - plugin library for user interaction


This is a very famous library in which there are such elements as: selecting a date from the calendar, resizing any items, dragging, top bookmarks, sorting items in the list with the mouse and so on. Study, you will not regret.


In this example of the memory card, we use Drag & drop to drag the map nodes between other nodes. Everything is simple, the only thing that we had to “screw up” is to check that we are not dragging a parent to our descendants, as this would lead to a tree looping.

CSS - drawing a memory card

Each node we have looks like this:
 <div id="mindmap"> <ul class='childs'> <li> <div class='big_n_title'><div class='n_title'></div></div> <ul class='childs'> ...... </ul> </li> </ul> </div> 


If you correctly assign CSS properties to the ul, li, .b_g_n_title and .n_title elements, you’ll get exactly the kind of memory card you see. All CSS properties you can see in the source .

In fact, the whole secret is that we do this:
 #mindmap { background-image: url(cross.png); background-attachment: scroll; white-space: nowrap; } #mindmap ul { display: inline-block; white-space: nowrap; vertical-align: middle; list-style: none; } #mindmap .big_n_title { display: inline-block; vertical-align: baseline; margin-right: 40px; position: relative; } #mindmap .n_title { display: inline-block; white-space: normal; } 

Those. we prohibit lists from moving elements to a new line, and elements of lists make display: inline-block, so that they become similar to characters in a line of text. We also add vertical centering, and in order for it to work for each node separately, we wrap the node in div.big_n_title.

Nothing revolutionary and very complicated. And it works.

It is verified that if you combine this technique with direction: rtl , then you can draw a map and in the other direction - from right to left. You can make the left side of the card go one way, and the right the other. But I prefer one-sided cards more - they are easier to read.

Let's start to round out. Anyone who wants read the detailed comments in the github code listing, although I prefer to call functions with long names that themselves say what the function does. My code, it is possible to improve, but then I had the task to demonstrate the work of several plug-ins, which are used in a much more complex my project .

Study the finished example , all sources are open.

PS: If you “slightly” modify the example, you will get a mindmaster and you will be able to charge users from 5 to 15 dollars a month (just kidding ).

Thank you for your attention, and jQuery and plug-in creators for saving time.

Source: https://habr.com/ru/post/191850/


All Articles