2012-07-09

用 jQuery 種一顆簡單樹(Tree)

小時候要用 Javascript 做一個下面這種樹狀圖,應該要報個幾十個人天吧。

如果再加上右鍵功能,嗯,「PM,請給我幾個人月吧」!



最大的問題應該出在瀏覽器相容性,以及少了一些很好用的 function 吧。

這些問題 jQuery 都解決了,所以今天種一顆樹簡單多了。

程式有三隻:easytree.js、easytree.html 與 easytree.css,當然還有幾張 icon。

easytree.js
var bi = {};
bi.Tree = function(domId) {
 
 this.domId = domId;
 this.dom = null;
 this.nodes = [];
 this.nodeSn = 1;
 this.isShow = false;
 this.maxLevel = 5;
 this.logging = true;
 
 // 建立根目錄
 this.rootNode = new bi.Folder(this, 'EasyTree', true);
 this.rootNode.isRoot = true;
 this.nodes[this.rootNode.nodeId] = this.rootNode;
 
 // 好用的變數
 var that = this;
 
 // create the tree dom if not found
 if ($('#' + this.domId).length == 0) {
  $('body').append('<div id="' + this.domId + '"></div>');
 }
 this.dom = $('#' + this.domId);
 // add tree dom class
 this.dom.addClass('biTree');
 
 // attachEvent - 開關目錄
 $('.folderTitle').live('click', function() {
  var nodeId = $(this).attr('nodeId');
  var node = that.get(nodeId);
  that.toggleFolder(node);
 });

 // attachEvent - 右鍵選單
 $('.node, #rcMenu').live("contextmenu", function(e){
  return false;
 }); 
 $('#rcMenu').live("mouseleave", function(e){
  $(this).hide();
 }); 
 $('.node').live('mousedown', function(e) {
  if (e.which != 3) {
   return false;
  }
  var nodeId = $(this).attr('nodeId');
  var node = that.get(nodeId);
  var rcMenu = that.getRcMenu();
  rcMenu.css({'top': e.pageY + 'px', 'left': e.pageX + 'px'});
  rcMenu.empty();
  if (node.children) {
   // folder node
   rcMenu.append('<div class="item" onclick="tree.addFile(\'New File\', ' + nodeId + ');">Add A File</div>');
   rcMenu.append('<div class="item" onclick="tree.addFolder(\'New Folder\', false, ' + nodeId + ');">Add A Folder</div>');
   rcMenu.append('<div class="item" onclick="tree.renameNode(' + nodeId + ');">Rename...</div>');
   rcMenu.append('<div class="item" onclick="tree.deleteFolder(' + nodeId + ');">Delete</div>');
  }
  else {
   // file node
   rcMenu.append('<div class="item" onclick="tree.renameNode(' + nodeId + ');">Rename...</div>');
   rcMenu.append('<div class="item" onclick="tree.deleteFile(' + nodeId + ');">Delete</div>');
  }
 });

 /**
  * 取得右鍵選單
  */
 this.getRcMenu = function() {
  if ($('#rcMenu').length == 0) {
   $('body').append('<div id="rcMenu"></div>');
  }
  var rcMenu = $('#rcMenu');
  rcMenu.show();
  return rcMenu;
 };
 

 /**
  * 計算每個 node 的位置
  */
 this.calculate = function() {
   $('.node').each(function(){
    var p = $(this).offset();
    var w = $(this).outerWidth();
    var h = $(this).outerHeight();
    var node = that.get($(this).attr('nodeId'));
    node.area['top'] = p.top;
    node.area['left'] = p.left;
    node.area['bottom'] = p.top + h;
    node.area['right'] = p.left + w;
   });
 };
 
 /**
  * 開啟或關閉目錄
  */
 this.toggleFolder = function(node) {
  // 取得目錄包
  var fullNodeDom = node.getFullNodeDom();
  // 移除舊css
  fullNodeDom.removeClass('folderopen').removeClass('folderclosed');
  // 修改狀態
  node.open = !node.open;
  // 更新css
  fullNodeDom.addClass(node.open ? 'folderopen' : 'folderclosed');
  node.getInsideDom().slideToggle(function(){
   // 計算每個 node 的位置
   that.calculate();
  });
  this.log();
 };
 
 /**
  * 新增目錄
  */
 this.addFolder = function(title, open, parentId) {
  $('#rcMenu').hide();
  var parentNode = null;
  if (parentId) {
   parentNode = this.get(parentId);
   // 只能加在目錄下
   if (!parentNode.children) {
    alert("Can't add something under a file!");
    return;
   }
   // 層數限制
   var parents = parentNode.getNodeDom().parents('.folderInside');
   if (parents.length >= this.maxLevel) {
    alert("Can't add any folders deeper than this!");
    return;
   }
  }
  var f = new bi.Folder(this, title, open);
  if (this.isShow) {
   // tree 已經 render,必須修改html
   var html = f.html();
   var parentNode = this.get(parentId);
   // 加到 tree 裡
   parentNode.add(f);
   // 加到 html 裡
   parentNode.getInsideDom().append(html);
   // 沒打開的目錄把它打開
   if (!parentNode.open) {
    this.toggleFolder(parentNode);
   }
   // 計算每個 node 的位置
    this.calculate();
  }
  else {
   // tree 尚未 render,先加到 tree 裡
   this.add(f);
  }
  this.log();
  return f;
 };
 
 /**
  * 新增檔案
  */
 this.addFile = function(title, parentId) {
  $('#rcMenu').hide();
  var parentNode = null;
  if (parentId) {
   parentNode = this.get(parentId);
   // 只能加在目錄下
   if (!parentNode.children) {
    alert("Can't add something under a file!");
    return;
   }
   // 層數限制
   var parents = parentNode.getNodeDom().parents('.folderInside');
   if (parents.length >= this.maxLevel) {
    alert("Can't add any files deeper than this!");
    return;
   }
  }
  var f = new bi.File(this, title);
  if (this.isShow) {
   // tree 已經 render,必須修改html
   var html = f.html();
   // 加到 tree 裡
   parentNode.add(f);
   // 加到 html 裡
   parentNode.getInsideDom().append(html);
   // 沒打開的目錄把它打開
   if (!parentNode.open) {
    this.toggleFolder(parentNode);
   }
   // 計算每個 node 的位置
    this.calculate();
  }
  else {
   // tree 尚未 render,先加到 tree 裡
   this.add(f);
  }
  this.log();
  return f;
 };
  
 /**
  * 重新命名
  */
 this.renameNode = function(nodeId) {
  $('#rcMenu').hide();
  var node = this.get(nodeId);
  var newTitle = prompt('Enter new Title', node.title);
  if (!newTitle) {
   return;
  }
  // 更新 tree 裡的 node
  node.title = newTitle;
  // 更新網頁
  node.getNodeDom().html(node.title);
  this.log();
 };
 
 /**
  * 刪除目錄
  */
 this.deleteFolder = function(nodeId) {
  $('#rcMenu').hide();
  var node = this.get(nodeId);
  if (node.isRoot) {
   alert("Can't delete root node!");
   return;
  }
  if (!confirm('Are you sure?')) {
   return;
  }
  this.doDeleteFolder(node);
 };

 /**
  * 執行目錄刪除
  */
 this.doDeleteFolder = function(node) {
  // 從 tree 裡移除
  var parentNode = this.get(node.getParentNodeId());
  parentNode.children = $.grep(parentNode.children, function(obj){
   return obj.nodeId != node.nodeId;
  });
  // 從網頁移除
  node.getFullNodeDom().slideUp(function(){
   node.getFullNodeDom().remove();
   // 計算每個 node 的位置
    that.calculate();
  });
  this.log();
 };
 
 /**
  * 刪除檔案
  */
 this.deleteFile = function(nodeId) {
  $('#rcMenu').hide();
  var node = this.get(nodeId);
  if (!confirm('Are you sure?')) {
   return;
  }
  this.doDeleteFile(node);
 };
 
 /**
  * 執行檔案刪除
  */
 this.doDeleteFile = function(node) {
  // 從 tree 裡移除
  var parentNode = this.get(node.getParentNodeId());
  parentNode.children = $.grep(parentNode.children, function(obj){
   return obj.nodeId != node.nodeId;
  });
  // 從網頁移除
  node.getNodeDom().slideUp(function(){
   node.getNodeDom().remove();
   // 計算每個 node 的位置
    that.calculate();
  });
  this.log();
 };
 
 /**
  * 新增 node 到 tree 裡
  */
 this.add = function(node) {
  // 放到根目錄下
  this.rootNode.children.push(node);
  // 快速查詢用
  this.nodes[node.nodeId] = node;
  this.log();
 };
 
 /**
  * 快速查詢所有 node
  */
 this.get = function(nodeId) {
  return this.nodes[nodeId];
 };
 
 /**
  * 將 tree 顯示到網頁裡
  */
 this.show = function() {
   this.dom.html(this.rootNode.html());
   this.isShow = true;
  // 計算每個 node 的位置
   this.calculate();
  this.log();
 };

 this.log = function(msg, replace) {
  if (this.logging) {
   if (msg) {
    if (replace) {
     $('#msg').html('<br/>' + msg);
    }
    else {
     $('#msg').append('<br/>' + msg);
    }
   }
   else {
    $('#msg').html(this.toString());
   }
  }
 };
 
  this.toString = function() {
   var s = ' - BiTree Array In Memory - ';
   $.each(this.rootNode.children, function(idx, obj){
    s += "<br/>" + obj;
   });
   return s;
  };
};

bi.Folder = function(tree, title, open){
 
 this.nodeId = tree.nodeSn++;;
 this.tree = tree;
  this.title = title;
 this.open = open;
 // 子包
 this.children = [];
 // 是否為根目錄
 this.isRoot = false;
  // 目錄名稱 DOM 物件
  this.nodeDom = null;
  // 目錄包 DOM 物件
  this.fullNodeDom = null;
  // DOM 物件的四邊
 this.area = [];
 this.area['top'] = null;
 this.area['left'] = null;
 this.area['bottom'] = null;
 this.area['right'] = null;
 
 /**
  * 加入檔案或目錄
  */
 this.add = function(node) {
  // 放入子包
  this.children.push(node);
  // 放入 tree 包,方便快速尋找
  this.tree.nodes[node.nodeId] = node;
 };

 /**
  * 取得 node 的 DOM 物件
  */
 this.getNodeDom = function() {
  if (!this.nodeDom) {
   this.nodeDom = $('.node' + this.nodeId);
  }
  return this.nodeDom;
 };
 
 /**
  * 取得目錄包
  */
 this.getFullNodeDom = function() {
  // 因為目錄結構不同於檔案,用來取得整個目錄包
  if (!this.fullNodeDom) {
   // 目錄名稱往上一層
   this.fullNodeDom = this.getNodeDom().parent();
  }
  return this.fullNodeDom;
 };
 
 /**
  * 取得目錄包裡的子包
  */
 this.getInsideDom = function() {
  return $('.folderInside' + this.nodeId);
 };
 
 /**
  * 取得父層 nodeId
  */
 this.getParentNodeId = function() {
  return this.getNodeDom().parent().parent().attr('nodeId');
 };
 
 this.html = function() {
  // 目錄包結構為
  // 目錄包 folder
  //   - 目錄名稱 folderTitle
  //   - 子包 folderInside
  //       - 其他檔案或目錄
  var html = '';
  html += '<div class="folder ' + (this.open ? 'folderopen' : 'folderclosed') + '">';
  html += '<div class="node folderTitle node' + this.nodeId + '" nodeId="' + this.nodeId + '">' + this.title + '</div>';
  html += '<div class="folderInside folderInside' + this.nodeId + '" nodeId="' + this.nodeId + '" style="display: ' + (this.open ? 'block' : 'none') + '">';
   $.each(this.children, function(idx, obj){
    html += obj.html();
   });
  html += '</div>';
  html += '</div>';
   return html;
 };
 
  this.toString = function() {
   var s = '[Folder ' + this.nodeId + '] ' + this.title;
   $.each(this.children, function(idx, obj){
    s += "<br/>" + obj;
   });
   return s;
  };
};

bi.File = function(tree, title) {

 this.nodeId = tree.nodeSn++;;
 this.tree = tree;
  this.title = title;
  // DOM 物件
  this.nodeDom = null;
  // DOM 物件的四邊
 this.area = [];
 this.area['top'] = null;
 this.area['left'] = null;
 this.area['bottom'] = null;
 this.area['right'] = null;
 
 /**
  * 取得 node 的 DOM 物件
  */
 this.getNodeDom = function() {
  if (!this.nodeDom) {
   this.nodeDom = $('.node' + this.nodeId);
  }
  return this.nodeDom;
 };

 /**
  * 取得父層 nodeId
  */
 this.getParentNodeId = function() {
  // 往上一層取 nodeId
  return this.getNodeDom().parent().attr('nodeId');
 };
 
 this.html = function() {
  var html = '';
  html += '<div class="node file node' + this.nodeId + '" nodeId="' + this.nodeId + '">';
  html += this.title;
  html += '</div>';
  return html;
 };
 
 this.toString = function() {
   return '[File ' + this.nodeId + '] ' + this.title;
  };
};

easytree.css
body {
 font-size: 14px;
}

.tree {
 width: 200px;
}

#msg {
 position: absolute;
 left: 500px;
 top: 0;
 margin: 10px;
}

/***** node *****/
.tree .folderclosed {
}

.tree .folderopen {
}

.tree .node {
 background-repeat: no-repeat;
 padding: 0 0 0 34px;
 cursor: pointer;
}

.tree .node:hover {
 color: #f30;
}

.tree .folder {
}

.tree .folderclosed > .folderTitle {
 background-image: url(../images/folderClosed.png);
}

.tree .folderopen > .folderTitle {
 background-image: url(../images/folderOpen.png);
}

.tree .folderInside {
 padding: 0 0 0 17px;
}

.tree .file {
 background-image: url(../images/file.png);
}

/***** rcMenu *****/
#rcMenu {
 background-color: #eee;
 border: 1px solid #ddd;
 position: absolute;
 padding: 5px;
}

#rcMenu .item {
 cursor: pointer;
 padding: 2px;
}

#rcMenu .item:hover {
 color: #fff;
 background-color: #f90;
}
easytree.html
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=BIG5">
<title>Easy Tree</title>
<link type="text/css" rel="stylesheet" href="style/easytree.css"></link>
<script type="text/javascript" src="http://code.jquery.com/jquery-1.7.2.min.js"></script>
<script type="text/javascript" src="js/easytree.js"></script>
<script type="text/javascript">
var tree;
$(function(){
 
 tree = new bi.Tree('easytree');
 
 for (var i=1; i<=5; i++) {
  var f = new bi.Folder(tree, "Folder_" + i, false);
  f.add(new bi.Folder(tree, "Sub Folder_" + i, false));
  f.add(new bi.File(tree, "File_" + i, ""));
  tree.add(f);
 }
 for (var i=1; i<=10; i++) {
  tree.addFile("File_" + (i + 5));
 }
 
 tree.show();
 
 $('#msg').show();
});
</script>
</head>
<body>
<div id="easytree" class="tree"></div>
<div id="msg"></div>
</body>
</html>
當然,今天還是可以跟 PM 報個幾個月人力...

2 則留言: