IconTree.hx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  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 applyStyleInternal(e: T, element: Element) {
  54. try {
  55. applyStyle(e, element);
  56. }
  57. catch (e) {
  58. }
  59. }
  60. function getValue( c : IconTreeItem<T> ) {
  61. if( c.value != null )
  62. return c.value;
  63. return values.get(c.id);
  64. }
  65. function getVal( id : String ) : T {
  66. var c = map.get(id);
  67. if( c == null ) return null; // id is loading ?
  68. return getValue(c);
  69. }
  70. function makeContent(parent:IconTreeItem<T>) {
  71. var content : Array<IconTreeItem<T>> = get(parent == null ? null : getValue(parent));
  72. var i = 0;
  73. for( c in content ) {
  74. var key = (parent == null ? "" : parent.absKey + "/") + c.text + ":" + i;
  75. if( c.absKey == null ) c.absKey = key;
  76. c.id = "titem__" + (UID++);
  77. map.set(c.id, c);
  78. var prevItem = revMap.get(c.value);
  79. if( Std.isOfType(c.value, String) )
  80. revMapString.set(cast c.value, c);
  81. else {
  82. revMap.set(c.value, c);
  83. values.set(c.id, c.value);
  84. c.value = null;
  85. }
  86. if( c.state == null ) {
  87. var s : Null<Bool>;
  88. if( prevItem != null && prevItem.state.opened != null ) {
  89. s = prevItem.state.opened;
  90. saveDisplayState(key, s);
  91. }
  92. else {
  93. s = getDisplayState(key);
  94. }
  95. if( s != null ) c.state = { opened : s } else c.state = {};
  96. }
  97. if( !async && c.children ) {
  98. c.state.loaded = true;
  99. c.children = cast makeContent(c);
  100. }
  101. i++;
  102. }
  103. return content;
  104. }
  105. public function init(?onReady : Void -> Void) {
  106. var inInit = true;
  107. (element:Dynamic).jstree({
  108. core : {
  109. dblclick_toggle: false,
  110. animation: 50,
  111. themes: {
  112. name: "default-dark",
  113. dots: true,
  114. icons: true
  115. },
  116. check_callback : function(operation, node, node_parent, value, extra) {
  117. if( operation == "edit" && allowRename )
  118. return true;
  119. if(!map.exists(node.id)) // Can happen on drag from foreign tree
  120. return false;
  121. if( operation == "rename_node" ) {
  122. if( node.text == value ) return true; // no change
  123. return onRename(getVal(node.id), value);
  124. }
  125. if( operation == "move_node" ) {
  126. if( extra.ref == null ) return true;
  127. return onAllowMove(getVal(node.id), getVal(node_parent.id));
  128. }
  129. return false;
  130. },
  131. data : function(obj, callb) {
  132. if( !inInit && checkRemoved() )
  133. return;
  134. callb.call(this, makeContent(obj.parent == null ? null : map.get(obj.id)));
  135. }
  136. },
  137. plugins : [ "dnd", "changed" ],
  138. });
  139. element.on("click.jstree", function (event) {
  140. var node = new Element(event.target).closest("li");
  141. if(node == null || node.length == 0) return;
  142. var v = getVal(node[0].id);
  143. if( v == null ) return;
  144. onClick(v, event);
  145. });
  146. element.on("dblclick.jstree", function (event) {
  147. // ignore dblclick on open/close arrow
  148. if( event.target.className.indexOf("jstree-ocl") >= 0 )
  149. return;
  150. var node = new Element(event.target).closest("li");
  151. if( node == null || node.length == 0 ) return;
  152. var v = getVal(node[0].id);
  153. if(onDblClick(v))
  154. return;
  155. if( allowRename ) {
  156. // ignore rename on icon
  157. if( event.target.className.indexOf("jstree-icon") >= 0 )
  158. return;
  159. editNode(v);
  160. return;
  161. }
  162. });
  163. element.on("open_node.jstree", function(event, e) {
  164. var i = map.get(e.node.id);
  165. i.state.opened = true;
  166. if( filter == null ) saveDisplayState(i.absKey, true);
  167. onToggle(getValue(i), true);
  168. });
  169. element.on("close_node.jstree", function(event,e) {
  170. var i = map.get(e.node.id);
  171. i.state.opened = false;
  172. if( filter == null ) saveDisplayState(i.absKey, false);
  173. onToggle(getValue(i), false);
  174. });
  175. element.on("refresh.jstree", function(_) {
  176. var old = waitRefresh;
  177. waitRefresh = [];
  178. if( searchBox != null ) {
  179. searchBox = null;
  180. openFilter(false);
  181. searchFilter(this.filter);
  182. }
  183. for( f in old ) f();
  184. });
  185. element.on("move_node.jstree", function(event, e) {
  186. onMove(getVal(e.node.id), e.parent == "#" ? null : getVal(e.parent), e.position);
  187. });
  188. element.on('ready.jstree', function () {
  189. var lis = element.find("li");
  190. for(li in lis) {
  191. var item = map.get(li.id);
  192. if(item != null)
  193. applyStyleInternal(getValue(item), new Element(li));
  194. }
  195. if (onReady != null)
  196. onReady();
  197. });
  198. element.on('changed.jstree', function (e, data) {
  199. var nodes: Array<Dynamic> = data.changed.deselected;
  200. // desselect all is called when the tree is refreshed,
  201. // so we highjack it to refresh the whole tree
  202. if (data.action == "deselect_all") {
  203. nodes = [for (i in element.find("li").toArray()) i.id];
  204. }
  205. for(id in nodes) {
  206. var item = getVal(id);
  207. var el = getElement(item);
  208. if( item != null && el != null ) applyStyleInternal(item, el);
  209. }
  210. });
  211. element.on("rename_node.jstree", function(e, data) {
  212. var item = getVal(data.node.id);
  213. var el = getElement(item);
  214. if( item != null && el != null ) applyStyleInternal(item, el);
  215. });
  216. element.on("after_open.jstree", function(event, data) {
  217. var lis = new Element(event.target).find("li");
  218. for(li in lis) {
  219. var item = map.get(li.id);
  220. if(item != null)
  221. applyStyleInternal(getValue(item), new Element(li));
  222. }
  223. });
  224. element.keydown(function(e:js.jquery.Event) {
  225. if( e.keyCode == 27 ) closeFilter();
  226. });
  227. inInit = false;
  228. }
  229. function checkRemoved() {
  230. if( element == null || element[0].parentNode == null )
  231. return true;
  232. if( !js.Browser.document.contains(element[0]) ) {
  233. dispose();
  234. return true;
  235. }
  236. return false;
  237. }
  238. public function dispose() {
  239. (element:Dynamic).jstree("detroy");
  240. element.remove();
  241. for( f in Reflect.fields(this) )
  242. try Reflect.deleteField(this,f) catch(e:Dynamic) {}
  243. }
  244. function getRev( o : T ) {
  245. if( Std.isOfType(o, String) )
  246. return revMapString.get(cast o);
  247. return revMap.get(o);
  248. }
  249. public function getElement(e : T) : Element {
  250. if( e == null )
  251. return null;
  252. var v = getRev(e);
  253. if(v == null)
  254. return null;
  255. var el = (element:Dynamic).jstree('get_node', v.id, true);
  256. return el;
  257. }
  258. public function editNode( e : T ) {
  259. var n = getRev(e).id;
  260. (element:Dynamic).jstree('edit',n);
  261. }
  262. public function getCurrentOver() : Null<T> {
  263. var id = element.find(":focus").attr("id");
  264. if( id == null )
  265. return null;
  266. var i = map.get(id.substr(0, -7)); // remove _anchor
  267. return i == null ? null : getValue(i);
  268. }
  269. public function getCurrentHover() : Null<T> {
  270. var elem = element.get(0).querySelectorAll(".jstree-anchor");
  271. var id = null;
  272. for (e in elem) {
  273. if (untyped e.matches(":hover")) {
  274. id = untyped e.id;
  275. break;
  276. }
  277. }
  278. if( id == null )
  279. return null;
  280. var trueId = StringTools.replace(id, "_anchor", "");
  281. var i = map.get(trueId);
  282. return i == null ? null : getValue(i);
  283. }
  284. public function getItemByElement(element: js.html.Element) : Null<T> {
  285. var anchor = element.closest(".jstree-anchor");
  286. if (anchor == null || anchor.id == null)
  287. return null;
  288. var trueId = StringTools.replace(anchor.id, "_anchor", "");
  289. var i = map.get(trueId);
  290. return i == null ? null : getValue(i);
  291. }
  292. public function setSelection( objects : Array<T> ) {
  293. (element:Dynamic).jstree('deselect_all');
  294. var ids = [for( o in objects ) { var v = getRev(o); if( v != null ) v.id; }];
  295. (element:Dynamic).jstree('select_node', ids, false, !autoOpenNodes); // Don't auto-open parent
  296. if(autoOpenNodes)
  297. for(obj in objects)
  298. revealNode(obj);
  299. }
  300. public function refresh( ?onReady : Void -> Void ) {
  301. map = new Map();
  302. revMapString = new haxe.ds.StringMap();
  303. values = new Map();
  304. if( onReady != null ) waitRefresh.push(onReady);
  305. (element:Dynamic).jstree('refresh',true);
  306. }
  307. public function getSelection() : Array<T> {
  308. var ids : Array<String> = (element:Dynamic).jstree('get_selected');
  309. return [for( id in ids ) getVal(id)];
  310. }
  311. public function collapseAll() {
  312. (element:Dynamic).jstree('close_all');
  313. }
  314. public function openNode(e: T) {
  315. var v = getRev(e);
  316. if(v == null) return;
  317. (element:Dynamic).jstree('_open_to', v.id);
  318. }
  319. public function openNodeAsync(e: T, ?onReady : Void -> Void ) {
  320. var v = getRev(e);
  321. if(v == null) return;
  322. (element:Dynamic).jstree('open_node', v.id, onReady, false);
  323. }
  324. public function revealNode(e : T) {
  325. openNode(e);
  326. var el = getElement(e);
  327. if(el != null && el.length > 0)
  328. (el[0] : Dynamic).scrollIntoViewIfNeeded();
  329. }
  330. public function searchFilter( flt : String ) {
  331. this.filter = flt;
  332. if( filter == "" ) filter = null;
  333. if( filter != null ) {
  334. filter = filter.toLowerCase();
  335. // open all nodes that might contain data
  336. for( id => v in map )
  337. if( v.text.toLowerCase().indexOf(filter) >= 0 )
  338. (element:Dynamic).jstree('_open_to', id);
  339. }
  340. var lines = element.find(".jstree-node");
  341. lines.removeClass("filtered");
  342. if( filter != null ) {
  343. for( t in lines ) {
  344. if( t.textContent.toLowerCase().indexOf(filter) < 0 )
  345. t.classList.add("filtered");
  346. }
  347. while( lines.length > 0 ) {
  348. lines = lines.filter(".list").not(".filtered").prev();
  349. lines.removeClass("filtered");
  350. }
  351. }
  352. }
  353. var searchBox : Element;
  354. public function closeFilter() {
  355. if( searchBox != null ) {
  356. searchBox.remove();
  357. searchBox = null;
  358. }
  359. if( filter != null ) {
  360. searchFilter(null);
  361. var sel = getSelection();
  362. refresh(() -> setSelection(sel));
  363. }
  364. }
  365. public function openFilter(focus: Bool = true) {
  366. if( async ) {
  367. async = false;
  368. refresh(openFilter.bind(focus));
  369. return;
  370. }
  371. if( searchBox == null ) {
  372. searchBox = new Element("<div>").addClass("search-box").prependTo(element);
  373. new Element("<input type='text'>").appendTo(searchBox).keydown(function(e) {
  374. if( e.keyCode == 27 ) {
  375. searchBox.find("i").click();
  376. return;
  377. }
  378. }).keyup(function(e) {
  379. var elt = e.getThis();
  380. function filter() {
  381. if( searchBox == null ) return;
  382. var val = StringTools.trim(elt.val());
  383. if( val == "" ) val = null;
  384. if( val != this.filter ) searchFilter(val);
  385. }
  386. var val = elt.val();
  387. haxe.Timer.delay(filter, Ide.inst.ideConfig.typingDebounceThreshold);
  388. });
  389. new Element("<i>").addClass("ico ico-times-circle").appendTo(searchBox).click(function(_) {
  390. closeFilter();
  391. });
  392. }
  393. searchBox.find("input").attr("placeholder", "Find");
  394. searchBox.show();
  395. if (focus) {
  396. searchBox.find("input").focus().select();
  397. }
  398. }
  399. }