29 августа 2013 в 12:58

Делаем «mindmap» на Javascript с локальным хранением в базе данных браузера из песочницы


Это небольшой учебный пример редактора карты памяти. За счёт очень подробных комментариев и простого кода, понять его не составит проблем. Статья предназначена для знающих и изучающих Javascript.

Я опишу особенности создания редактора карты памяти, который использует базу данных браузера. Причём, это будет не LocalStorage, который не может превышать 5 мегабайт. Объём данных сможет превысить 100-200 мегабайт, так как используется IndexedDB или webSQL, смотря что доступно в конкретном браузере.

Исходники выложены в открытый доступ на Github.

Мы уложимся в 520 строк кода, при этом в нашей карте можно будет перетаскивать узлы между собой, удалять, переименовывать и создавать новые. А также можно будет назначать одну из 120 иконок через контекстное меню.

Секрет минимализма в том, что мы будем использовать проверенные в бою плагины:
  1. Ydn.db — хранение информации в базе данных браузера с автоматическим выбором лучшего метода и единым API
  2. jQuery context menu — контекстное меню, которое можно наполнять динамически при помощи Javascript
  3. jsPlumb — расширение позволяющее рисовать линии между HTML элементами
  4. jQuery UI — Drag&drop — перетаскивание элементов между собой


PS: Также мы научимся создавать «синглтон», облегчать себе асинхронное программирование при помощи jQuery и встроенного объекта $.Deferred(), а также при помощи плагина LiveReload, сохраним краску на клавише F5 при изменении свойств CSS и кода в HTML и Javascript.


Код программы с комментариями, для тех, кто торопится

Многие могут дальше не читать, а просто ознакомиться с кодом.
Главный Javascript код с очень подробными комментариями
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(); //перерисовываем линии
	}); //загружаем весь массив из базы данных браузера
	
}




Инструменты которыми я пользуюсь при программировании

Пока отложим изучение кода. Сначала я кратко опишу инструменты, которыми я пользуюсь. Многие новички о них не знают и теряют на этом время.

Программирую я в Coda, а иногда в Sublime Text. Coda привычнее, но чуть подтормаживает раскрашивая код (она только под Мак), а Sublime Text — очень быстрый и работает на любой платформе, но, во первых, я ещё к нему не привык, а во вторых, люблю заходить прямо в Coda на сервер, чтобы быстро исправить несколько файлов. И терминалом для связи с Debian пользуюсь через Coda.

Настоящим прорывом в скорости работы для меня стало открытие GIT. Это система контроля версий. Использую её через официальную программу GitHub:


Github использую как облачное хранение своего кода, а также публикую релизы с его помощью. Чтобы «зарелизить» версию своего сайта, я делаю так:
  1. Проверяю работу сайта на локальной машине и делаю «Коммит», т.е. подтверждаю изменения (см.картинку выше, кнопка Commit)
  2. «Пушу» изменения на сервер одной кнопкой (в том же окне внизу есть кнопка Sync)
  3. Захожу в терминал на свой сервер в Германии по SSH и запускаю комманду:
    git pull httрs://myuser_name:mysslpassword@github.com/Imater/tree.git master
  4. Запускаю скрипт, который сжимает все JS и CSS файлы, выкидывая из них комментарии и ZIP-уя их в один файл js и один файл css. Чем меньше файлов вы вставляете в свою HTML страничку, тем быстрее загружается сайт


Во время работы с кодом у меня всегда открыт Chrome и его консоль. В консоли можно попробовать создаваемые функции, поиграться с данными и отладить код при помощи встроенного отладчика:


Научитесь работать с консолью и Developer Tools и будете экономить много времени.

Теперь поговорим о краске на клавише F5 (в случае MacOS — cmd+r). Совсем недавно открыл для себя LiveReload. Есть версия и под Win и под Mac.

Устанавливаете программу, потом ставите плагин под Хром или другой браузер или вставляете код сразу за тегом body в основном HTML файле:
<script>document.write('<script src=\"http://' + (location.host || 'localhost').split(':')[0] + ':35729/livereload.js?snipver=1\"></' + 'script>')</script>


И после этого получаете массу удовольствия. Сценарий такой: открываете свой сайт, который программируете, потом пишете код в CSS или Javascript или заменяете картинки в папке, которую «мониторит» livereload, и как только вы сохранили, сайт сразу обновляется. Если вы заменили CSS, то изменения применяются мгновенно без перезагрузки страницы.

Ну и в конце, описания инструментов, хочу рекомендовать уменьшитель картинок PNG, который экономит до 70% объёма и, тем самым, ускоряет загрузку сайта.

Описание кода программы для создания карты памяти

Весь код выложен на Github. Вы можете скачать его и развернуть в любую папку. Всё что нужно, уже в репозитории. Все плагины в папке. Комментарии в коде очень подробные.

Начнём с основ. Я люблю оборачивать всё множество функций в "синглтон". Делаю это следующим образом:
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") );


Преимущество такого подхода в возможности сохранять большие массивы данных внутри такого «синглтона» и не бояться, что пользователь или какой либо плагин изменит данные. Мы изолируем функции и переменные. А из практики, мне нравится набирать в консоли: «api4mindmap.js...» и ждать всплывающий список всех функций, которые я создал. Быстро и удобно.

В своё время для меня было открытием, что в вышеуказанном примере, данные в массиве my_all_data сохраняются между вызовами функций.

Теперь поговорим о плагинах, которые мы используем:

Ydn.db — база данных браузера


Это плагин для jQuery, который позволяет при помощи единого api сохранять и считывать данные из локальных баз данных браузеров. Работает и на любых платформах, в том числе на мобильных. Даже в Phonegap.

Я не пользуюсь большинством его функций, а использую, в основном в следующих случаях:
 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;
 }

Это пример из кода для редактора карт памяти. Тут очень простые команды: db.values(«mindmap_db»,null,99999999) — считывает все элементы из таблицы IndexedDb (если в Хроме) и возвращает через некоторое время в функцию которая в параметрах .done(). Так вы можете считывать элементы из базы данных. Можно считать один элемент при помощи команды: db.get(«mindmap_db»,5) — так вы считаете элемент с id = 5.

Для записи в базы данных используется команда: db.put(«mindmap_db», answer ). Так как эта команда асинхронна и браузер не ждёт её выполнения, она не замедляет работу. Именно поэтому эта программа будет работать в Chrome быстрее.

Для работы с асинхронными командами, применяю встроенный в jQuery объект $.Deferred(). Пример приведён выше. Вы просто обещаете в конце функции, что вернёте результат как только, так сразу, при помощи команды: return d.promise(x); А потом когда у вас всё в функции выполнилось, например данные отправлены на сервер, вы выполняете d.resolve(200); Тогда выполняется .done() функция и ей передаётся параметр x.

Это очень удобно, так как позволяет не писать функции внутри друг друга. А ещё, рекомендую изучить команду $.when. Я её использую тогда, когда у нас есть много асинхронных функций, которые мы запустили одновременно и хотим выполнить кое-что сразу после завершения всех асинхронных. Вот пример:
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();
}

По сути, тут все «обещания» набираются в массив и передаются команде $.when, которая выполняет функцию ".done" ровно в тот момент, когда последнее обещание выполнено.

Ydn.db позволяет работать с данными, не задумываясь о методах их хранения в браузере, так как оборачивает их многообразие в единый api. Он, в том числе, позволяет работать с индексами, делать отборы, накладывать фильтры, вычислять суммы в таблицах и так далее. Но в данном примере, мы используем его только для хранения и считывания данных, а роль индекса у нас выполняет массив my_all_data, это обеспечивает очень большую скорость. При этом сохранность данных обеспечивается тем, что функция api4mindmap.jsFind(id, {title: «new_title»}), обновляет этот массив сразу, а в базу отправляет данные асинхронно. Но, тем не менее, вы можете изменить заголовок узла в карте памяти и тут же обновить браузер, а все данные при этом сохранятся. База данных в браузере работает быстро, надёжно и имеет возможность хранить больше 100 мегабайт информации.

Изучив Ydn.db, можно забыть про 5 мегабайт ограничений LocalStorage. Единственное, что пользователя будут спрашивать разрешение на хранение данных каждые 5 мегабайт (Chrome не спрашивает).

Этот плагин отлично работает на Android и в iOS. Но помните, чтобы избежать ошибок, возможно вам придётся отключить выбор IndexedDB в Android. Что примечательно, в последних версиях Internet Explorer используется IndexedDB, что ускоряет работу подобных приложений за счёт асинхронности. Если вы разом сохраняете 1000 элементов, они будут делать это параллельно, но с разной скоростью, что быстрее, чем последовательная запись.

jsPlumb — рисуем линии SVG в любом браузере


Для рисования линий между разными элементами html страницы можно было бы использовать Canvas, но он имеет множество недостатков. Вам пришлось бы создать Canvas значительного размера, например 4000 x 4000 px, что привело бы к нестабильной работе браузера. Линии были бы растровыми и на современных экранах Retina, смотрелись бы хуже, чем векторные. А самое страшное, что все эти 16 миллионов пикселей, требовали бы перерисовки при каждом вводе буквы в карту памяти.

jsPlumb рисует каждую линию в SVG и размещает его в нужное место, ставя ему CSS свойство absolute и рассчитанные координаты. Есть возможность мгновенно перерисовать все линии одной командой. По сути, вам достаточно один раз указать начальную и конечную точку, затем соединить их линией и вы можете забыть про плагин. Практически 3 команды и вы умеете обращаться с линиями.

На их сайте есть пример, где каждый элемент можно перемещать мышкой, при этом линии перемещаются вслед за элементом без замедления.


Изучите, пригодится для рисования: иерархических структур, простых графиков, обучалок (которые показывают линией со стрелкой на элемент про который говорят), организационных структур предприятий, диаграмм и так далее. Работает практически во всех браузерах, в том числе на iOS и Android.

jQuery context menu — контекстное меню

Все плагины меню которые я видел, используют для создания своей иерархии html. А в этом плагине можно создавать структуру в простом, практически JSON формате, при помощи массива объектов.


Задать набор команд в меню можно при помощи вот такого кода:
	$.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 //сгенерированные пункты меню с иконками
            }
		}
		});	



Обратите внимание на пункт items, всё очень понятно и красиво. Тем более, что хранить все функции вызываемые контекстным меню вместе в функции callback, позволяет меньше путаться.

Единственное, что мне пришлось сделать, это подправить скрипты плагина так, чтобы он рисовал не растровые иконки, а иконки из шрифта Fontello.

Основным преимуществом этого контекстного меню является то, что вы можете управлять клавишами для выбора пунктов и, даже, назначать горячие клавиши на определённые команды. Все преимущества становятся понятны после изучения демонстрации на оф.сайте.

Fontello — шрифт с векторными иконками


Тут как в супермаркете, заходите на сайт и набираете те иконки, которые вам смогут пригодиться:


После этого скачиваете архив и вставляете в свой основной html файл ссылку на CSS файл. С тех пор, независимо от браузера, вы пользуетесь этими иконками вот так: — такой html код превратится в иконку.

Сами иконки можете оценить в демонстрации карты памяти, зайдя в контекстное меню. Там 120 иконок в естественной среде обитания.

jQuery UI — библиотека плагинов для взаимодействия с пользователем


Это очень знаменитая библиотека в которой есть такие элементы как: выбор даты из календарика, изменение размеров любых элементов, перетаскивание, верхние закладки, сортировка элементов в списке мышкой и так далее. Изучите, не пожалеете.


В данном примере карты памяти, мы используем Drag&drop для перетаскивания узлов карты между другими узлами. Тут всё просто, единственное что нам пришлось «замудрить», это проверку того, что мы не перетаскиваем родителя к своим потомкам, так как это привело бы к зацикливанию дерева.

CSS — рисование карты памяти

Каждый узел у нас выглядит вот так:
<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>


Если правильно назначить CSS свойства элементам ul, li, .big_n_title и .n_title, то получается именно такая карта памяти, которую вы видите. Все CSS свойства вы можете посмотреть в исходниках.

По сути, весь секрет в том, что мы делаем так:
#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;
}


Т.е. запрещаем спискам переносить элементы на новую строку, а элементы списков делаем display:inline-block, так что они становятся подобием символов в строке текста. Также мы добавляем центрирование по вертикали, а чтобы оно работало для каждого узла в отдельности, мы оборачиваем узел в div.big_n_title.

Ничего революционного и очень сложного. И всё работает.

Проверено, что если комбинировать этот приём с direction: rtl, то можно рисовать карту и в другую сторону — справа налево. Можно сделать так, чтобы левая часть карты уходила в одну сторону, а правая в другую. Но я больше люблю односторонние карты — их проще читать.

Начнём закругляться. Все кто хочет, почитают подробные комментарии в листинге кода на github, хотя я предпочитаю называть функции длинными именами, которые сами говорят о том, что функция делает. Мой код, вполне можно улучшить, но тут передо мной стояла задача продемонстрировать работу нескольких плагинов, которые используются в значительно более сложном моём проекте.

Изучайте готовый пример, все исходники открыты.

PS: Если вы «чуть» доработаете пример, вы получите mindmaster и сможете брать с пользователей от 5 до 15 долларов в месяц (шутка).

Спасибо вам за внимание, а jQuery и создателям плагинов за экономию времени.
Евгений @imater
карма
88,5
рейтинг 0,0
Самое читаемое Разработка

Комментарии (45)

  • +10
    «Карта ума» — как-то жестковато, встречал перевод «диаграмма связей»
    • +3
      Устоявшийся перевод для «mind map» — «карта памяти».
      • 0
        А «карта мыслей»?
        • 0
          Калька с польского? по длине термина предпочтительнее, чем «ментальная карта».
      • +2
        Карта разума?
        Диагра́мма свя́зей, известная также как интелле́кт-ка́рта, ка́рта мыслей (англ. Mind map) или ассоциати́вная ка́рта,
      • +1
        Есть еще «ментальная карта». Этот вариант мне больше других нравится.
        • 0
          Длинновато… хотя и калька с испанского и португальского.
  • +4
    Скажите, пожалуйста, а почему Вы пишите

    $("#mindmap").on(...);
    
    ...
    
    $("#mindmap").on(...);
    


    вместо того, чтобы один раз «запомнить» и использовать:

    var $mindmap = $("#mindmap");
    
    $mindmap.on(...);
    
    ...
    
    $mindmap.on(...);
    


    ?
    • +5
      Вы правы.
      Я так делаю только для того, чтобы новичкам было понятно и не путать их сложным кодом. А так да, чем меньше обращений к DOM, тем быстрее приложение и меньше тратится ресурсов. Спасибо за замечание. Кэширование вообще ускоряет работу во всех областях программирования.
      • +3
        Ясно. Спасибо за Ваш труд.
      • +7
        Мне кажется, новичкам лучше не показывать плохой код. Вообще никому лучше не показывать плохой код.
        Лучше закешировать в переменную, как указали выше, а для новичков оставить внятный комментарий, что это и зачем.
  • +1
    Спасибо, очень интересно! как раз делаю нечто подобное.
  • 0
    вот бы ещё она умела в формат .mm понимать, чтобы с FreeMind «интеграция» была…
    • 0
      и xmind
      • +4
        И чтоб не больше 520 строк и код, понятный для новичков… :)
    • +5
      Сделайте, кто мешает. Этот топик — потрясающий тутор, написанный грамотным языком, просто, понятно и с крутыми примерами / практиками. остальное — ваших рук дело.
      • 0
        Пгостите, но в каком слове моего комментария ви увидели критику к автору?
        • +4
          Простите, что вмешиваюсь.
          Мне кажется, комментарии вроде «вот бы еще эта штука умела делать то или это» неадекватны формату топика. Этот топик — попытка научить что-то делать, а не предоставить продукт.

          То есть этот топик так позиционируется. А на самом деле это реклама 4tree. Ну и да, согласиться, что это «потрясающий тутор, написанный грамотным языком, просто, понятно и с крутыми примерами / практиками» я не могу, конечно.
          • +1
            Когда говорил «потрясающий» имел в виду контекст, найдите на Хабре хотя бы 3 подобных топика по js и с хорошей практикой :)
  • +2
    Пара комментариев по собственно проекту на гитхабе:
    1. А почему вы не думаете о людях из других стран, которые тоже возможно будут заинтересованы? Я только что хотел показать это коллеге — но он ничего не понял, потому что все по-русски! Интернационализация — великая вещь…
    2. БЫло бы неплохо указать лицензию на проекте в гитхабе. Я бы например склонил бы, да только нельзя — по умолчанию, лицензия — копирайт.
  • –2
    Теперь заголовок «Делаем «карту памяти» на Javascript...» создаёт впечатление, что мы делаем флешку на JS. И сходу непонятно, зачем там 3-4 посторонних плагина для рисования графов. Вообще, это стало ясно из комментариев и обнаружения слов «mind map». Наверное, лучше использовать устоявшийся английский термин: «Делаем «mind map» на Javascript...»
  • 0
    К сожалению в Safari 6.1 не работает.
    скриншот с ошибкой
    image
  • +3
    Про git прокрутил.

    На jsPlumb завис на 20 минут. Клёвая библиотека!
  • +2
    Спасибо за пример. Нашел для себя новое.

    А какие ограничения по браузерам?
  • 0
    Интересно, меня друг попросил помочь с похожим мини-проектом. Идея — создавать рабочий стол, основанный на canvas куда можно перетягивать детали из списка. Почти все те же плагины (jquery, grag&drop, context menu, raphaeljs). Так вот, у него похожий код, один главный API-«класс», который может и делает все. Разросся он правда уже на 3500 строк — за счет более сложной математики и логики (пересечение объектов, snapping, умный расклад элементов etc...). Я даже не знаю с какой стороны к нему подойти. Думаю попробовать отделить логику/математику расклада в отдельную библиотеку/класс.

    А у кого какие будут предложения по рефакторингу примера из статьи?
    • +2
      1. ctrl+A
      2. ctrl+del
      3. И писать заново…
        (function() {
            var application = {
      	   config: {
      		
      	   },
      		
      	   init: function() {
      			
      	   },
      	   ...
            };
      
            application.init();
        })();
      
      • –1
        2. del без сtrl
        • 0
          может быть)
  • +9
    Такс… давно не писал комменты на хабре. Но тут просто не удержался. Я конечно все понимаю, но как так можно… это просто нельзя так оставлять. Уверен, вы писали это с чистыми намерениями, но тут есть список, почему вышло не совсем гуд:

    • Это худшая реализация синглтона, что я видел. Даже в той ссылке на вику, что вы там поставили есть пример на js, который уже хоть куда не шел. Синглтон пишется через статическое свойство класса, всего то.
    • «arguments.callee.» — «ES5 Замечание: В strict-режиме любое использование arguments.callee вызовет TypeError, поскольку свойство принято устаревшим.» — пруф с jsGarden.
    • «var this_api = this; //кэшируем самого себя, чтобы использовать внутри функций» — сохраняем контекст, ибо зависит от объекта, который вызывает функцию. О this в js можно много говорить
    • «my_all_data_template, jsSaveAllToDB» определитесь, или камелКейс или андер_скор. Для js де-факто камелКейс.
    • Все эти больше данные с стрингами нужно выносить подальше от логики..
    • Есть такое свойство prototype в функции, говорят помогает при создании методов класса… Нельзя копировать каждому объекту класса свой метод.
    • Блок переменных находится вверху функции, а не сл. слово var по всему телу.
    • Создавайте приложение, а не кучу глобальных функций, что засоряют глобальный объект. Или хотябы уже заверните все это в (function(){})(),
      Вывод: Архитектуры никакой..
    • Загрузку скриптов надо оптимизировать, грузить параллельно, или в одном файле, но не 7-ю запросами. Не говоря уже о том, что файл, который является точкой входа явно не может грузится в центре, он самый последний, ибо юзает все другие подключаемые либы. И раз уже так то и скрипты должны подключатся не в хеде, а в конце бади, ибо блочат загрузку страницы.
    • <body onload="jsDoFirst();" onresize="onResize()"> - в 18-ый век вернулись что ли?.. Ненавязчивый js — «Не, не слышал».


    Лан… пусть будет 10. Думаю сообщество хабра ща сольет мне карму, и больше никого не смогу исправить, и все будут спокойно реагировать на г*нокод.
    PS: ничего личного, замечания полезны, никогда не поздно научится.
    • 0
      Все замечания 100% верны, но хочется как-раз про архитектуру услышать.
      • +2
        Есть иерархия, есть много данных, есть по сути несколько вьюшек, возможна даже одна — как минимум MVC уже напрашивается. Бекбон по-моем идеально ложится, стандартная тудушка (http://backbonejs.ru/#examples-todos) явно покрывает все это, не так сложно к этому применить. Не нравится бекбон — addyosmani.github.io/todomvc/ выбирайте что вам по душе.

        Не хотите MVC, вынесите хотя бы работу с данными отдельно, напишите класс «Приложение» которое работает с объектами класса «Узел карты» и все будет красиво и просто.

        Все эти 100500 плагинов нафиг не надо. Там максимум если драген_дроп лень самому писать, уже можно заюзать.
        • 0
          Мда, код еще тот. Вдогонку:

          Определитесь с тем, насколько универсальным ваш код должен быть. Синглтон по надобности — одна из последних вещей. Если предполагается встраивание вашего кода в какой-то внешний неизвестный код, то первое, что надо — оборачивание всего этого добра в локальный контекст. Смотрите require.js. Второе — destroy-метод. Наподключали обработчиков, а убирать за собой кто будет?

          Половина кода — jquery-навешивание обработчиков на глобальный dom. Если код претендует на универсальность, это нужно убрать.

          Бэкбон здесь на мой взгляд излишен — это одностраничное приложение без роутинга и особого контроллера. Берите knockout.js.

          Для удаленного редактирования файлов посмотрите в сторону sshfs.
  • +1
    Объясните пожалуйста эти строчки:

    if(records.length && false) {

    И еще
    $.each(records, function(i, el){
    my_all_data["n"+el.id] = {};
    my_all_data["n"+el.id] = el;
    • 0
      Прошу прощения и посыпаю голову пеплом.
      Осталось от отладки.
    • 0
      Индусы довольно потирают руки.
  • 0
    Видно, что писать код вы можете.
    Рекомендую прочитать книгу Стояна Стефанова — JavaScript. Шаблоны — 2011.

    В книге рассказывается не только про часто используемые шаблоны, но и про работу самого интерпретатора.
    После прочтения становиться понятно почему имеет хороший тон объявлять все переменные в одном месте (Шаблон
    единственной инструкции var), хорошо объясняется объектная модель языка, как работает наследование через prototype, да и вообще ещё много чего интересного.

    Читал книгу переведенную на русский язык (А.Кисилев) от издательства Символ. Перевод очень понравился.
    • 0
      Спасибо. Буду выгонять из себя «Индуса».
  • 0
    До этого я только код смотрел, решил вот даже статью почитать…

    • «Начнём с основ. Я люблю оборачивать всё множество функций в „синглтон“Преимущество такого подхода в возможности сохранять большие массивы данных внутри такого «синглтона» и не бояться, что пользователь или какой либо плагин изменит данные.»
      — Аааа… что? Причем здесь синглтон?..


    • «облегчать себе асинхронное программирование при помощи jQuery»…
      — как?.. JQuery — это набор функций для работы с DOM, AJAX… А плагин к jQ — $.Deferred() там уже что то делает

    • Для рисования линий между разными элементами html страницы можно было бы использовать Canvas, но он имеет множество недостатков. Вам пришлось бы создать Canvas значительного размера, например 4000 x 4000 px, что привело бы к нестабильной работе браузера. Линии были бы растровыми и на современных экранах Retina, смотрелись бы хуже, чем векторные. А самое страшное, что все эти 16 миллионов пикселей, требовали бы перерисовки при каждом вводе буквы в карту памяти
      — ну… ну зачем?.. Какие 4000? Какая перерисовка на ввод букв? Откуда вы это взяли? Канвас, если уже так говорить, немного, но быстрее свг. Конечно каждый из них более уместен для разных задач. Но, не надо говорить неправду о канвасе


    • 0
      Уточню: Deferred object — это часть jQuery API, а не плагин. api.jquery.com/category/deferred-object/.
      Для данной задачи все же лучше похдодит SVG.
  • 0
    Статью ещё не всю прочитал, но сразу благодарю — давно что-то такое хотел сам написать…
  • 0
    Возможны ли какие-нибудь связи между элементами, кроме «вертикальных» как в иерархическом списке?
  • +2
    Такую бы шутку для создания бизнес процессов, цены бы не было
    • +3
      как говорил один бизнесмен: «Для описания бизнес процессов нужна одна программа — excel». а если вы этого не умеете делать, то никакой «мего софт» за вас это не сделает.
    • –1
      А Gliffy чем не устраивает?
  • 0
    Писал нечто подобное для себя, тоже использовал jsPlumb (trees000.herokuapp.com (связи создаются перетягиванием с меню)), думаю в будущем переписать но используя d3.js

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.