Custom Tree v2 jQuery plugin

    На днях решил вернуться к перепиливанию одного своего старенького Open Source проекта.
    В процессе обдумывания решил, что предыдущий компонент с деревом в его нынешнем виде меня больше не устраивает.

    Хотелось чего-нибудь более Event Driven, с понятным и простым API.

    Сейчас решил, что оно уже готово для Public.

    Берите, пользуйтесь.
    Или посмотрите на example в рамках GH-pages.

    Под катом краткий перевод краткой документации по API.

    UPD: в комментариях мой код для организации перетаскивания.


    Исполнено оно в виде jQuery плагина.

    Пример конфига, т.е. того, что передаётся когда вы «создаёте» дерево:

    $('#tree_content').customTree({
    
    	root : 'top',
    	init : {
    		callback : function (controller, tree) {
    			info('Init callback.');
    		}
    	},
    
    	// for leaf callbacks
    	handlers : {
    		added : function (leaf, controller, tree) {
    		},
    		loaded : function (leaf, controller, tree) {
    		},
    		parsed : function (leaf, controller, tree) {
    		},
    		open : function (leaf, controller, tree) {
    		},
    		close : function (leaf, controller, tree) {
    		},
    		hover : function (leaf, controller, tree) {
    		},
    		unhover : function (leaf, controller, tree) {
    		},
    		focus : function (leaf, controller, tree) {
    		},
    		beforeblur : function (callback, leaf, controller, tree) {
    		},
    		blur : function (leaf, controller, tree) {
    		},
    		deleted : function (leaf, controller, tree) {
    		},
    		dblclick : function (leaf, controller, tree) {
    		}
    	},
    
    	listeners : {
    		// click, dblclick, contextmenu up the element Label
    		contextmenu : function (leaf, controller, tree, event) {
    		},
    		dblclick : function (leaf, controller, tree, event) {
    		}
    	},
    
    	storeLoaded : false,
    	focusParentOnClose : true,
    	// focusByDblClick: true,
    	// blurFromContainerClick : false,
    	// blurFromContainerDblClick : false,
    
    	labelsBreak : {
    		by : 50,
    		expandOnHover : false,
    		expandOnSelect : true
    	},
    
    	loader : function (path, callback) {
    		// ... your code for nodes loading
    	}
    
    });
    
    


    В рамках кода используется строгое соглашение по параметрам ноды, приходящей на Parsing\Rendering, поэтому привожу пример единичной ноды (leaf):

    {
    	// – должно быть уникальным в рамках текущего узла,
    	'unique_naming_string' : {
    
    		// – опционально, используется для представления,
    		text : 'string',
    
    		// – опционально, указывает на то, является ли данный leaf папкой,
    		folder : [true || false],
    
    		// – опционально, указывает на то, нужно ли "открыть" папку
    		open : [true || false]
    		
    		// любые другие свойства могут быть дополнительно переданы, вы сможете их использвать
    		
    	}
    }
    
    
    


    Объяснения {

    • root – имя для root пути узла
    • init – опции начальной загрузки


    // то, что будет использовано для загрузки root
    init : {
    	
    	// отсрочка в миллисекундах или null
    	delay : null,
    	
    	// имя класса установленное на root во время первоначальной загрузки
    	// если не будет установлено, то preloader не будет индицирован
    	preloader : 'preloader',
    	
    	// function (controller, tree) будет вызван после загрузки
    	callback : null,
    	
    	// jQuery метод, который будет использован для "показа" root
    	method : 'fadeIn',
    	
    	// если true (default), то загрузка произойдёт сразу после delay
    	auto : true,
    	
    	// путь узла, который нужно focus после загрузки
    	focus : null
    
    }
    
    
    


    handlers ( leaf, controller, tree ) – предустановленные обработчики событий.

    Все принимают текущий узел, контроллер и объект дерева.

    Но beforeblur принимает ещё и callback как первый параметр.

    Если Вам, допустим, нужно проверять, можно ли blur текущий узел.
    Если «да», тогда этот callback нужно вызвать.
    Иначе просто не вызывайте его, тогда blur не произойдёт.

    Если blur должен был быть произведён, потому, что нужно было сделать focus для другого узла, то если не вызвать этот callback, ни blur текущего ни focus нужного произведён не будет.

    Естественно, эти манипуляции с callback для beforeblur будут возможны только если вы вообще передадите в конфиг handler.beforeblur.

    loader ( path, callback ) – то, к чему контроллер будет обращаться для загрузки leaf.

    Принимает ['tree.root', 'leaf.name'] в качестве path.

    Должен вернуть JS Object!

    listeners – стандартыне jQuery .on( callbacks для текстового элемента узла.
    Т.е., если, допустим, нужно contextmenu или кастомный click, то нужно использовать listeners.

    theme – CSS PREFIX_ для классов при рендере

    cls – набор CSS классов при рендере:

    cls : {
    
    	// для root
    	root : 'tree_root',
    
    	// для места, где плюс и минус
    	control : 'tree_control',
    	
    	// для места, где текущий статус, например "загружается"
    	status : 'tree_leaf_status',
    	
    	// для текстового поля
    	text : 'tree_leaf_text',
    
    	// для "папок"
    	folder : 'folder',
    	
    	// для выделенного по focus()
    	selected : 'selected',
    	
    	// когда мышка над текстом
    	hover : 'hover',
    
    	// когда загружается текущий узел
    	loader : 'loader',
    	
    	// когда папка открыта
    	open : 'open',
    
    	// для всего контейнера
    	container : 'container',
    
    	// когда не нужно позволять выделение текста
    	supressLabelTextSelection : 'unselectable',
    	supressTreeTextSelection : 'unselectable'
    
    }
    
    
    


    html – HTML тэги для рендера:

    html : {
    
    	// root container
    	tree : '<UL>',
    	
    	// leaf container
    	leaf : '<LI>',
    	
    	// то, где будут children узла
    	children : '<UL>',
    
    	// где хранится текст и контролы +\- status
    	heading : '<DIV>',
    	
    	// то где +\-
    	control : '<SPAN>',
    	
    	// то, где статус
    	status : '<SPAN>',
    	
    	// то, где текст
    	text : '<SPAN>',
    
    	// сам контейнер
    	container : '<DIV>'
    
    }
    
    


    control – строки, использованные для +\-:

    control : {
    	close : '+',
    	open : '–'
    },
    
    


    storeLoaded: true || false – сохранять «свёрнутые» узлы, или перезагружать каждый раз

    focusParentOnClose: true || false – выделить родительский элемент, если его потомок имел focus

    focusByDblClick: true || false – выделять по двойному щелчку

    blurFromContainerClick: true || false – blur когда щелкают root

    blurFromContainerDblClick: true || false – blur когда дважды щелкают root

    labelsBreak: – текстовые caption узла могут быть длинными, опции для «обрезки» если нужны:

    labelsBreak : {
    
    	// на какое количество символов делать перенос строк
    	by : 0,
    	
    	// чем переносить 
    	str : '\n',
    	
    	// всегда полностью отображать все строки
    	expandAlways : false,
    	
    	// отображать все, когда hover
    	expandOnHover : false,
    	
    	// отображать все, когда focused
    	expandOnSelect : true
    }
    
    


    Controller API


    • getPath ( leaf ) – возвращает URL ARRAY для Leaf Object: [ 'top', 'leaf_name', 'child_name'… ]
    • refresh ( leaf, callback, andOpen ) – перезагружает переданный 'leaf',
      'callback' используется по завершении,
      pass 'andOpen' если нужно развернуть свёрнутый узел
    • blur ( leaf ) – делаем blur узлу
    • focus ( leaf ) – делаем focus узлу
    • x – текущий конфиг
    • x.current – текущий выделенный узел
    • init – если config.init.auto == false, здесь будет функция вызова init дерева


    Надеюсь, что кому-нибудь это всё пригодится.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 17

      +2
      Жаль что перетаскивания нету.
      Сам пользуюсь — github.com/mjsarfatti/nestedSortable но не хватает аякс подгрузки вложенных уровней.
        0
        Перетаскивание организовать просто, я пользуюсь этим, только загрузите JQ-UI:

        added : function (leaf, controller, tree) {
        	if ($.fn.draggable && $.fn.droppable && $.fn.sortable) {
        
        		if (leaf.parent.items.length > 1) {
        			if (leaf.parent.els.children.hasClass('ui-sortable')) {
        				leaf.parent.els.children.sortable('refresh');
        			} else {
        				leaf.parent.els.children.sortable({
        					axis : 'y',
        					distance : 5,
        					containment : '.custom_tree_root',
        					update : function (event, ui) {
        						var parent = event.target;
        						var order = [];
        						$.each(parent.children, function (index, item) {
        							// your code is here...
        							debugger;
        						});
        						// your code is here...
        						debugger;
        					}
        				});
        			}
        
        		}
        
        		leaf.container.prop('leaf', leaf);
        		leaf.els.text
        		.prop('leaf', leaf)
        		.prop('dropped', null)
        		.draggable({
        			addClasses : false,
        			// revert : true,
        			distance : 11,
        			zIndex : 100,
        			start : function () {
        				leaf.els.text.prop('dropped', tree);
        			},
        			stop : function () {
        				// your code is here...
        				debugger;
        			}
        		})
        		.droppable({
        			addClasses : false,
        			accept : 'span.tree_leaf_text',
        			drop : function (event, ui) {
        				ui.draggable.prop('dropped', leaf);
        			}
        		});
        
        	}
        }
        
        


          0
          Код выше, это пример draggable и sortable для этого конкретного дерева, для примера на гитхабе, а не вообще.
          Таскается за узлы, сортируется за +\-.

          Не стал добавлять сразу, т.к. откуда я знаю как и за что вы можете захотеть таскать узлы, и куда.
          Может быть вам будет нужно таскать не за «текст», а за +\-, или вы добавите в status какую-нибудь иконку, и будете таскать за неё, откуда мне знать то?

          К тому же может будет не нужен sortable, например, и вам хватит только draggable.
          0
          www.jstree.com/
          Попробуйте — не разочарует.
        +3
        Прошу сильно не гнобить меня, ибо я занимаюсь проблемой отрисовки деревьев с 2008 года и имею довольно обширный опыт и даже некоторые успехи.

        Все что я изложу ниже — из чистого желания сделать вашу жизнь проще, поскольку на абсолютное большинство грабель я уже наступил. Для меня вопрос отрисовки деревьев уже почти закрыт.

        1) Как опять? Два проекта по деревьям за одну неделю это жестко. habrahabr.ru/post/188118/

        2) Я смотрел код. Вы сделали очень большую работу. Но судя по всему она малопригодна для кого-либо, кроме вас. Надо быть одержимым, что бы найти в себе силы в этом разобраться.

        3) Вы напрасно решили рисовать дерево JS-ом. Вы смешали верстку и код. Логику и отображение. Вы сделали это очень жестко.

        	html : {
        
        tree : '<UL>',
        leaf : '<LI>',
        children : '<UL>',
        
        heading : '<DIV>',
        control : '<SPAN>',
        status : '<SPAN>',
        text : '<SPAN>',
        
        container : '<DIV>'
        
        }
        


        По сути, вы «подумали и все решили за меня». А что если у меня более сложное устройство верстки узла? Я должен перебрать весь JS? Намека на отделение HTML от JS нет. Это откровенный Fail.

        4) Зачем раскрывать дерево при наведении? Мне трудно это понять. Какое сверх-новшество в UI вы пытались реализовать? Деревья всегда раскрывались нажатием иконки плюса. Если я хочу раскрыть дерево я нажму иконку. Я не хочу что бы дерево убегало от меня, разворачиваясь вниз.

        5) Прочитайте реализацию отрисовки дерева и комментарии к посту habrahabr.ru/post/188118/. Если у вас принципиально такая же схема — исправьте свой подход. Должно стать существенно лучше.

        6) Передавать дерево в JSON клиенту весьма спорный и проблемный момент. Со [script]alert(1)[/script] намучаетесь. Особенно если придется рисовать что-то хоть более менее серьезное с реальным пользовательским вводом. Дерево комментариев — отличный пример.

        Какую проблему вы хотите решить передавая дерево JSON-ом? Трафик жалко? Поздравляю! Смесь JS и верстки я уже видел.

        7) Деревья прекрасно и очень быстро рисуются на сервере. На любом языке. Да, даже на руби, а все знают. что руби скоростью не славится.

        8) Прежде чем плюнуть в мою сторону после весьма обширного списка замечаний с моей стороны, прошу хоть краем глаза глянуть в мою репу github.com/the-teacher/the_sortable_tree

        а конкретнее на Ruby реализацию отрисовки:

        Функиция отрисовки

        Один из шаблонов отрисовки

        Кроме того есть практически идентичный код для отрисовки дерева на JS (не поддерживается за ненадобностью):

        Отрисовка дерева на JS(Coffee)

        9) Для перетаскивания элементов дерева рекомендую эту штуку. Работает хорошо уже несколько лет. Замечаний не имею.

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

        И удачи конечно.

          +1
          по пункту 4) кажется, я ошибся. мне показалось.
            +1
            Привет, да, спасибо за комментарий :)

            Каюсь, изложение API нужно было поставить после концепции, которую я не изложил.
            Надо было объяснить сначала структуру узла, и уже потом объяснять всё.

            Узел состоит из контейнера, элемента управления где +\-, элемента со статусом и элемента с текстом.
            Внутри контейнера последним элементом идёт контейнер для Child'ов.
            Мне показалось, что все, кому станет интересно, поставят debugger в примере на github и увидят это.

            Моя концепция в том, что эти элементы «по любому» нужны.

            Соответственно, раз есть событие added, то всю эту «верстку» можно изменить под свой личный вкус.

            К тому же код не закрыт, там MIT лицензия. Мне показалось, что эти несчастные 500 строк не будут представлять существенной проблемы для человека, которому захочется что-нибудь «в корне» изменить.
            Посмотрите на github.com/wentout/CustomTree2/blob/master/tree.js, строка 288 метод makeLeaf.
            Там же всё «прозрачно».

            			var els = leaf.els = {
            				control : makeEl('control'),
            				status : makeEl('status'),
            				text : makeEl('text', leaf.text),
            				children : makeEl('children')
            			};
            


            Стараясь именно отделить логику от представления я в конфиге сделал html, где перечислил все тэги, которые использую для отрисовки.
            Безусловно — это лишь значения по умолчанию, и Вы можете использовать любые другие тэги, передав их в конфиг.

            Draggable, droppable и sortable я не стал добавлять, т.к. любой человек, знакомый с JQ UI, добавит их в обработчик события added самостоятельно за час на свой вкус, ведь мне не известно что будет у него в верстке и как он захочет этим распоряжаться.

            По п. 4, да действительно, я обрабатываю hover, но раскрытие\свертывание идёт по +\-, как обычно, в примере есть.

            Про «шаблоны» понятно, но мне показалось, что тянуть зависимость от шаблонизатора «не совсем».
            Можно было, конечно, добавить в API контроллера кроме load ещё и событие make, но тогда нужно было бы ещё одно соглашение по поводу структур, которые сохраняет контроллер внутри себя. По сути это ещё больше усложнило бы процесс, разработчику потребовалось бы ещё больше bootstrap кода написать.

            И, да, конечно, дерево я писал для себя, мне нужно именно такое т.к. оно отображает структуру ЧПУ в рамках Fine Cut (лежит рядом в моих репах).
              0
              Многое о чем вы говорите имеет логику и имеет право на существование. Мне пришлось перебрать очень много вариантов реализации прежде я остановился на чем-то конкретном. Если не оставите эту задачу, наверняка еще многое поменяете. Сейчас я бы на вашем месте сконцентрировался на API, доке и конкретных примерах использования. По себе могу судить, что именно при написании доки многое становится на свои места и код сильно меняется.

              Если отпишитесь в скайп — не исключаю, что смогу чем-то помочь по коду.
          0
          Очень гибкий jsTree (cайт). Правда уже почти год не обновлялся.
            0
            jsTree — это безусловно, он покрывает почти весь, если вообще не весь возможный функционал для деревьев.
            Но мне его «много».
            Это всё равно что профессиональным стенобитным перфоратором делать дырочки для чеснока в буженине.
              0
              Год не обновлялся в данном случае — положительный признак. Багов — тупо нет.
              0
              Надо было построить сильно кастомизированное дерево, с драг н дропом и lazy loading'ом и прочими штуками. Лучше не нашел: code.google.com/p/dynatree/
                0
                Да, клёвое, спасибо :)
                Хочется свой велосипед, как говорит Бендер :)
                0
                Почему интересно до сих пор нет тега treeview? Давно уж задаюсь этим вопросом, казалось бы стандартный элемент многих приложений, а тега до сих пор нет и видимо не планируется.
                  0
                  Думаю, что в силу достаточно большого количества возможных вариантов исполнения единой концепции по тому, как оно должно работать – нет. В XUL, например, есть treeview тэг, но его использование очень часто перемежается с различными хуками, которые необходимы для покрытия функционала. Т.е., иногда проще наляпать своё. Мне вот тоже нужен был специфический способ, где контейнеры не Array, а Object + функция динамической подгрузки не является частью либы, а отдана «на откуп» разработчику, и пока он не напишет этот bootstrap код, ничего работать не будет.
                    0
                    И, да, кстати, например позиция в списке у меня основана на том, что у свойств JS Object всё же есть порядковый номер, а именно – то, как они были перечислены при создании объекта и является их позицией, а добавление происходит в конец.
                    Т.е., это такое свойство JS Object, которое далеко не всем известно, но является определяющим, например, для меня.

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

                  Самое читаемое