IconTree.hx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. package hide.comp;
  2. typedef IconTreeItem<T> = {
  3. var value : T;
  4. var text : String;
  5. @:optional var children : Bool;
  6. @:optional var icon : String;
  7. @:optional var state : {
  8. @:optional var opened : Bool;
  9. @:optional var selected : Bool;
  10. @:optional var disabled : Bool;
  11. @:optional var loaded : Bool;
  12. };
  13. @:optional var li_attr : Dynamic;
  14. @:optional var a_attr : Dynamic;
  15. @:optional @:noCompletion var id : String; // internal usage
  16. @:optional @:noCompletion var absKey : String; // internal usage
  17. }
  18. class IconTree<T:{}> extends Component {
  19. static var UID = 0;
  20. var waitRefresh = new Array<Void->Void>();
  21. var map : Map<String, IconTreeItem<T>> = new Map();
  22. var values : Map<String, T> = new Map();
  23. var revMapString : haxe.ds.StringMap<IconTreeItem<T>> = new haxe.ds.StringMap();
  24. var revMap : haxe.ds.ObjectMap<T, IconTreeItem<T>> = new haxe.ds.ObjectMap();
  25. public var allowRename : Bool;
  26. public var async : Bool = false;
  27. public var autoOpenNodes = true;
  28. public var filter(default,null) : String;
  29. public function new(?parent,?el) {
  30. super(parent,el);
  31. element.addClass("tree");
  32. }
  33. public dynamic function get( parent : Null<T> ) : Array<IconTreeItem<T>> {
  34. return [{ value : null, text : "get()", children : true }];
  35. }
  36. public dynamic function onClick( e : T, evt: Dynamic) : Void {
  37. }
  38. public dynamic function onDblClick( e : T ) : Bool {
  39. return false;
  40. }
  41. public dynamic function onToggle( e : T, isOpen : Bool ) : Void {
  42. }
  43. public dynamic function onRename( e : T, value : String ) : Bool {
  44. return false;
  45. }
  46. public dynamic function onAllowMove( e : T, to : T ) : Bool {
  47. return false;
  48. }
  49. public dynamic function onMove( e : T, to : T, index : Int ) {
  50. }
  51. public dynamic function applyStyle( e : T, element : Element ) {
  52. }
  53. function getValue( c : IconTreeItem<T> ) {
  54. if( c.value != null )
  55. return c.value;
  56. return values.get(c.id);
  57. }
  58. function getVal( id : String ) : T {
  59. var c = map.get(id);
  60. if( c == null ) return null; // id is loading ?
  61. return getValue(c);
  62. }
  63. function makeContent(parent:IconTreeItem<T>) {
  64. var content : Array<IconTreeItem<T>> = get(parent == null ? null : getValue(parent));
  65. var i = 0;
  66. for( c in content ) {
  67. var key = (parent == null ? "" : parent.absKey + "/") + c.text + ":" + i;
  68. if( c.absKey == null ) c.absKey = key;
  69. c.id = "titem__" + (UID++);
  70. map.set(c.id, c);
  71. var prevItem = revMap.get(c.value);
  72. if( Std.is(c.value, String) )
  73. revMapString.set(cast c.value, c);
  74. else {
  75. revMap.set(c.value, c);
  76. values.set(c.id, c.value);
  77. c.value = null;
  78. }
  79. if( c.state == null ) {
  80. var s : Null<Bool>;
  81. if( prevItem != null && prevItem.state.opened != null ) {
  82. s = prevItem.state.opened;
  83. saveDisplayState(key, s);
  84. }
  85. else {
  86. s = getDisplayState(key);
  87. }
  88. if( s != null ) c.state = { opened : s } else c.state = {};
  89. }
  90. if( !async && c.children ) {
  91. c.state.loaded = true;
  92. c.children = cast makeContent(c);
  93. }
  94. i++;
  95. }
  96. return content;
  97. }
  98. public function init() {
  99. var inInit = true;
  100. (element:Dynamic).jstree({
  101. core : {
  102. dblclick_toggle: false,
  103. animation: 50,
  104. themes: {
  105. name: "default-dark",
  106. dots: true,
  107. icons: true
  108. },
  109. check_callback : function(operation, node, node_parent, value, extra) {
  110. if( operation == "edit" && allowRename )
  111. return true;
  112. if(!map.exists(node.id)) // Can happen on drag from foreign tree
  113. return false;
  114. if( operation == "rename_node" ) {
  115. if( node.text == value ) return true; // no change
  116. return onRename(getVal(node.id), value);
  117. }
  118. if( operation == "move_node" ) {
  119. if( extra.ref == null ) return true;
  120. return onAllowMove(getVal(node.id), getVal(node_parent.id));
  121. }
  122. return false;
  123. },
  124. data : function(obj, callb) {
  125. if( !inInit && checkRemoved() )
  126. return;
  127. callb.call(this, makeContent(obj.parent == null ? null : map.get(obj.id)));
  128. }
  129. },
  130. plugins : [ "dnd", "changed" ],
  131. });
  132. element.on("click.jstree", function (event) {
  133. var node = new Element(event.target).closest("li");
  134. if(node == null || node.length == 0) return;
  135. var v = getVal(node[0].id);
  136. if( v == null ) return;
  137. onClick(v, event);
  138. });
  139. element.on("dblclick.jstree", function (event) {
  140. // ignore dblclick on open/close arrow
  141. if( event.target.className.indexOf("jstree-ocl") >= 0 )
  142. return;
  143. var node = new Element(event.target).closest("li");
  144. if( node == null || node.length == 0 ) return;
  145. var v = getVal(node[0].id);
  146. if(onDblClick(v))
  147. return;
  148. if( allowRename ) {
  149. // ignore rename on icon
  150. if( event.target.className.indexOf("jstree-icon") >= 0 )
  151. return;
  152. editNode(v);
  153. return;
  154. }
  155. });
  156. element.on("open_node.jstree", function(event, e) {
  157. var i = map.get(e.node.id);
  158. i.state.opened = true;
  159. if( filter == null ) saveDisplayState(i.absKey, true);
  160. onToggle(getValue(i), true);
  161. });
  162. element.on("close_node.jstree", function(event,e) {
  163. var i = map.get(e.node.id);
  164. i.state.opened = false;
  165. if( filter == null ) saveDisplayState(i.absKey, false);
  166. onToggle(getValue(i), false);
  167. });
  168. element.on("refresh.jstree", function(_) {
  169. var old = waitRefresh;
  170. waitRefresh = [];
  171. if( searchBox != null ) {
  172. element.append(searchBox);
  173. searchFilter(this.filter);
  174. }
  175. for( f in old ) f();
  176. });
  177. element.on("move_node.jstree", function(event, e) {
  178. onMove(getVal(e.node.id), e.parent == "#" ? null : getVal(e.parent), e.position);
  179. });
  180. element.on('ready.jstree', function () {
  181. /* var lis = element.find("li");
  182. for(li in lis) {
  183. var item = map.get(li.id);
  184. if(item != null)
  185. applyStyle(getValue(item), new Element(li));
  186. } */
  187. });
  188. element.on('changed.jstree', function (e, data) {
  189. var nodes: Array<Dynamic> = data.changed.deselected;
  190. for(id in nodes) {
  191. var item = getVal(id);
  192. var el = getElement(item);
  193. applyStyle(item, el);
  194. }
  195. });
  196. element.on("rename_node.jstree", function(e, data) {
  197. var item = getVal(data.node.id);
  198. var el = getElement(item);
  199. applyStyle(item, el);
  200. });
  201. element.on("after_open.jstree", function(event, data) {
  202. var lis = new Element(event.target).find("li");
  203. for(li in lis) {
  204. var item = map.get(li.id);
  205. if(item != null)
  206. applyStyle(getValue(item), new Element(li));
  207. }
  208. });
  209. element.keydown(function(e:js.jquery.Event) {
  210. if( e.keyCode == 27 ) closeFilter();
  211. });
  212. inInit = false;
  213. }
  214. function checkRemoved() {
  215. if( element == null || element[0].parentNode == null )
  216. return true;
  217. if( !js.Browser.document.contains(element[0]) ) {
  218. dispose();
  219. return true;
  220. }
  221. return false;
  222. }
  223. public function dispose() {
  224. (element:Dynamic).jstree("detroy");
  225. element.remove();
  226. for( f in Reflect.fields(this) )
  227. try Reflect.deleteField(this,f) catch(e:Dynamic) {}
  228. }
  229. function getRev( o : T ) {
  230. if( Std.is(o, String) )
  231. return revMapString.get(cast o);
  232. return revMap.get(o);
  233. }
  234. public function getElement(e : T) : Element {
  235. var v = getRev(e);
  236. if(v == null)
  237. return null;
  238. var el = (element:Dynamic).jstree('get_node', v.id, true);
  239. return el;
  240. }
  241. public function editNode( e : T ) {
  242. var n = getRev(e).id;
  243. (element:Dynamic).jstree('edit',n);
  244. }
  245. public function getCurrentOver() : Null<T> {
  246. var id = element.find(":focus").attr("id");
  247. if( id == null )
  248. return null;
  249. var i = map.get(id.substr(0, -7)); // remove _anchor
  250. return i == null ? null : getValue(i);
  251. }
  252. public function setSelection( objects : Array<T> ) {
  253. (element:Dynamic).jstree('deselect_all');
  254. var ids = [for( o in objects ) { var v = getRev(o); if( v != null ) v.id; }];
  255. (element:Dynamic).jstree('select_node', ids, false, !autoOpenNodes); // Don't auto-open parent
  256. if(autoOpenNodes)
  257. for(obj in objects)
  258. revealNode(obj);
  259. }
  260. public function refresh( ?onReady : Void -> Void ) {
  261. map = new Map();
  262. revMapString = new haxe.ds.StringMap();
  263. values = new Map();
  264. if( onReady != null ) waitRefresh.push(onReady);
  265. (element:Dynamic).jstree('refresh',true);
  266. }
  267. public function getSelection() : Array<T> {
  268. var ids : Array<String> = (element:Dynamic).jstree('get_selected');
  269. return [for( id in ids ) getVal(id)];
  270. }
  271. public function collapseAll() {
  272. (element:Dynamic).jstree('close_all');
  273. }
  274. public function openNode(e: T) {
  275. var v = getRev(e);
  276. if(v == null) return;
  277. (element:Dynamic).jstree('_open_to', v.id);
  278. }
  279. public function revealNode(e : T) {
  280. openNode(e);
  281. var el = getElement(e);
  282. if(el != null)
  283. (el[0] : Dynamic).scrollIntoViewIfNeeded();
  284. }
  285. public function searchFilter( flt : String ) {
  286. this.filter = flt;
  287. if( filter == "" ) filter = null;
  288. if( filter != null ) {
  289. filter = filter.toLowerCase();
  290. // open all nodes that might contain data
  291. for( id => v in map )
  292. if( v.text.toLowerCase().indexOf(filter) >= 0 )
  293. (element:Dynamic).jstree('_open_to', id);
  294. }
  295. var lines = element.find(".jstree-node");
  296. lines.removeClass("filtered");
  297. if( filter != null ) {
  298. for( t in lines ) {
  299. if( t.textContent.toLowerCase().indexOf(filter) < 0 )
  300. t.classList.add("filtered");
  301. }
  302. while( lines.length > 0 ) {
  303. lines = lines.filter(".list").not(".filtered").prev();
  304. lines.removeClass("filtered");
  305. }
  306. }
  307. }
  308. var searchBox : Element;
  309. public function closeFilter() {
  310. if( searchBox != null ) {
  311. searchBox.remove();
  312. searchBox = null;
  313. }
  314. if( filter != null ) {
  315. searchFilter(null);
  316. var sel = getSelection();
  317. refresh(() -> setSelection(sel));
  318. }
  319. }
  320. public function openFilter() {
  321. if( async ) {
  322. async = false;
  323. refresh(openFilter);
  324. return;
  325. }
  326. if( searchBox == null ) {
  327. searchBox = new Element("<div>").addClass("searchBox").prependTo(element);
  328. new Element("<input type='text'>").appendTo(searchBox).keydown(function(e) {
  329. if( e.keyCode == 27 ) {
  330. searchBox.find("i").click();
  331. return;
  332. }
  333. }).keyup(function(e) {
  334. var elt = e.getThis();
  335. function filter() {
  336. if( searchBox == null ) return;
  337. var val = StringTools.trim(elt.val());
  338. if( val == "" ) val = null;
  339. if( val != this.filter ) searchFilter(val);
  340. }
  341. var val = elt.val();
  342. haxe.Timer.delay(filter, val.length == 1 ? 500 : 100);
  343. });
  344. new Element("<i>").addClass("ico ico-times-circle").appendTo(searchBox).click(function(_) {
  345. closeFilter();
  346. });
  347. }
  348. searchBox.show();
  349. searchBox.find("input").focus().select();
  350. }
  351. }