You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

545 lines
16 KiB

  1. /*!
  2. * jquery.fancytree.table.js
  3. *
  4. * Render tree as table (aka 'tree grid', 'table tree').
  5. * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
  6. *
  7. * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de)
  8. *
  9. * Released under the MIT license
  10. * https://github.com/mar10/fancytree/wiki/LicenseInfo
  11. *
  12. * @version 2.38.3
  13. * @date 2023-02-01T20:52:50Z
  14. */
  15. (function (factory) {
  16. if (typeof define === "function" && define.amd) {
  17. // AMD. Register as an anonymous module.
  18. define(["jquery", "./jquery.fancytree"], factory);
  19. } else if (typeof module === "object" && module.exports) {
  20. // Node/CommonJS
  21. require("./jquery.fancytree");
  22. module.exports = factory(require("jquery"));
  23. } else {
  24. // Browser globals
  25. factory(jQuery);
  26. }
  27. })(function ($) {
  28. "use strict";
  29. /******************************************************************************
  30. * Private functions and variables
  31. */
  32. var _assert = $.ui.fancytree.assert;
  33. function insertFirstChild(referenceNode, newNode) {
  34. referenceNode.insertBefore(newNode, referenceNode.firstChild);
  35. }
  36. function insertSiblingAfter(referenceNode, newNode) {
  37. referenceNode.parentNode.insertBefore(
  38. newNode,
  39. referenceNode.nextSibling
  40. );
  41. }
  42. /* Show/hide all rows that are structural descendants of `parent`. */
  43. function setChildRowVisibility(parent, flag) {
  44. parent.visit(function (node) {
  45. var tr = node.tr;
  46. // currentFlag = node.hide ? false : flag; // fix for ext-filter
  47. if (tr) {
  48. tr.style.display = node.hide || !flag ? "none" : "";
  49. }
  50. if (!node.expanded) {
  51. return "skip";
  52. }
  53. });
  54. }
  55. /* Find node that is rendered in previous row. */
  56. function findPrevRowNode(node) {
  57. var i,
  58. last,
  59. prev,
  60. parent = node.parent,
  61. siblings = parent ? parent.children : null;
  62. if (siblings && siblings.length > 1 && siblings[0] !== node) {
  63. // use the lowest descendant of the preceeding sibling
  64. i = $.inArray(node, siblings);
  65. prev = siblings[i - 1];
  66. _assert(prev.tr);
  67. // descend to lowest child (with a <tr> tag)
  68. while (prev.children && prev.children.length) {
  69. last = prev.children[prev.children.length - 1];
  70. if (!last.tr) {
  71. break;
  72. }
  73. prev = last;
  74. }
  75. } else {
  76. // if there is no preceding sibling, use the direct parent
  77. prev = parent;
  78. }
  79. return prev;
  80. }
  81. $.ui.fancytree.registerExtension({
  82. name: "table",
  83. version: "2.38.3",
  84. // Default options for this extension.
  85. options: {
  86. checkboxColumnIdx: null, // render the checkboxes into the this column index (default: nodeColumnIdx)
  87. indentation: 16, // indent every node level by 16px
  88. mergeStatusColumns: true, // display 'nodata', 'loading', 'error' centered in a single, merged TR
  89. nodeColumnIdx: 0, // render node expander, icon, and title to this column (default: #0)
  90. },
  91. // Overide virtual methods for this extension.
  92. // `this` : is this extension object
  93. // `this._super`: the virtual function that was overriden (member of prev. extension or Fancytree)
  94. treeInit: function (ctx) {
  95. var i,
  96. n,
  97. $row,
  98. $tbody,
  99. tree = ctx.tree,
  100. opts = ctx.options,
  101. tableOpts = opts.table,
  102. $table = tree.widget.element;
  103. if (tableOpts.customStatus != null) {
  104. if (opts.renderStatusColumns == null) {
  105. tree.warn(
  106. "The 'customStatus' option is deprecated since v2.15.0. Use 'renderStatusColumns' instead."
  107. );
  108. opts.renderStatusColumns = tableOpts.customStatus;
  109. } else {
  110. $.error(
  111. "The 'customStatus' option is deprecated since v2.15.0. Use 'renderStatusColumns' only instead."
  112. );
  113. }
  114. }
  115. if (opts.renderStatusColumns) {
  116. if (opts.renderStatusColumns === true) {
  117. opts.renderStatusColumns = opts.renderColumns;
  118. // } else if( opts.renderStatusColumns === "wide" ) {
  119. // opts.renderStatusColumns = _renderStatusNodeWide;
  120. }
  121. }
  122. $table.addClass("fancytree-container fancytree-ext-table");
  123. $tbody = $table.find(">tbody");
  124. if (!$tbody.length) {
  125. // TODO: not sure if we can rely on browsers to insert missing <tbody> before <tr>s:
  126. if ($table.find(">tr").length) {
  127. $.error(
  128. "Expected table > tbody > tr. If you see this please open an issue."
  129. );
  130. }
  131. $tbody = $("<tbody>").appendTo($table);
  132. }
  133. tree.tbody = $tbody[0];
  134. // Prepare row templates:
  135. // Determine column count from table header if any
  136. tree.columnCount = $("thead >tr", $table)
  137. .last()
  138. .find(">th", $table).length;
  139. // Read TR templates from tbody if any
  140. $row = $tbody.children("tr").first();
  141. if ($row.length) {
  142. n = $row.children("td").length;
  143. if (tree.columnCount && n !== tree.columnCount) {
  144. tree.warn(
  145. "Column count mismatch between thead (" +
  146. tree.columnCount +
  147. ") and tbody (" +
  148. n +
  149. "): using tbody."
  150. );
  151. tree.columnCount = n;
  152. }
  153. $row = $row.clone();
  154. } else {
  155. // Only thead is defined: create default row markup
  156. _assert(
  157. tree.columnCount >= 1,
  158. "Need either <thead> or <tbody> with <td> elements to determine column count."
  159. );
  160. $row = $("<tr />");
  161. for (i = 0; i < tree.columnCount; i++) {
  162. $row.append("<td />");
  163. }
  164. }
  165. $row.find(">td")
  166. .eq(tableOpts.nodeColumnIdx)
  167. .html("<span class='fancytree-node' />");
  168. if (opts.aria) {
  169. $row.attr("role", "row");
  170. $row.find("td").attr("role", "gridcell");
  171. }
  172. tree.rowFragment = document.createDocumentFragment();
  173. tree.rowFragment.appendChild($row.get(0));
  174. // // If tbody contains a second row, use this as status node template
  175. // $row = $tbody.children("tr").eq(1);
  176. // if( $row.length === 0 ) {
  177. // tree.statusRowFragment = tree.rowFragment;
  178. // } else {
  179. // $row = $row.clone();
  180. // tree.statusRowFragment = document.createDocumentFragment();
  181. // tree.statusRowFragment.appendChild($row.get(0));
  182. // }
  183. //
  184. $tbody.empty();
  185. // Make sure that status classes are set on the node's <tr> elements
  186. tree.statusClassPropName = "tr";
  187. tree.ariaPropName = "tr";
  188. this.nodeContainerAttrName = "tr";
  189. // #489: make sure $container is set to <table>, even if ext-dnd is listed before ext-table
  190. tree.$container = $table;
  191. this._superApply(arguments);
  192. // standard Fancytree created a root UL
  193. $(tree.rootNode.ul).remove();
  194. tree.rootNode.ul = null;
  195. // Add container to the TAB chain
  196. // #577: Allow to set tabindex to "0", "-1" and ""
  197. this.$container.attr("tabindex", opts.tabindex);
  198. // this.$container.attr("tabindex", opts.tabbable ? "0" : "-1");
  199. if (opts.aria) {
  200. tree.$container
  201. .attr("role", "treegrid")
  202. .attr("aria-readonly", true);
  203. }
  204. },
  205. nodeRemoveChildMarkup: function (ctx) {
  206. var node = ctx.node;
  207. // node.debug("nodeRemoveChildMarkup()");
  208. node.visit(function (n) {
  209. if (n.tr) {
  210. $(n.tr).remove();
  211. n.tr = null;
  212. }
  213. });
  214. },
  215. nodeRemoveMarkup: function (ctx) {
  216. var node = ctx.node;
  217. // node.debug("nodeRemoveMarkup()");
  218. if (node.tr) {
  219. $(node.tr).remove();
  220. node.tr = null;
  221. }
  222. this.nodeRemoveChildMarkup(ctx);
  223. },
  224. /* Override standard render. */
  225. nodeRender: function (ctx, force, deep, collapsed, _recursive) {
  226. var children,
  227. firstTr,
  228. i,
  229. l,
  230. newRow,
  231. prevNode,
  232. prevTr,
  233. subCtx,
  234. tree = ctx.tree,
  235. node = ctx.node,
  236. opts = ctx.options,
  237. isRootNode = !node.parent;
  238. if (tree._enableUpdate === false) {
  239. // $.ui.fancytree.debug("*** nodeRender _enableUpdate: false");
  240. return;
  241. }
  242. if (!_recursive) {
  243. ctx.hasCollapsedParents = node.parent && !node.parent.expanded;
  244. }
  245. // $.ui.fancytree.debug("*** nodeRender " + node + ", isRoot=" + isRootNode, "tr=" + node.tr, "hcp=" + ctx.hasCollapsedParents, "parent.tr=" + (node.parent && node.parent.tr));
  246. if (!isRootNode) {
  247. if (node.tr && force) {
  248. this.nodeRemoveMarkup(ctx);
  249. }
  250. if (node.tr) {
  251. if (force) {
  252. // Set icon, link, and title (normally this is only required on initial render)
  253. this.nodeRenderTitle(ctx); // triggers renderColumns()
  254. } else {
  255. // Update element classes according to node state
  256. this.nodeRenderStatus(ctx);
  257. }
  258. } else {
  259. if (ctx.hasCollapsedParents && !deep) {
  260. // #166: we assume that the parent will be (recursively) rendered
  261. // later anyway.
  262. // node.debug("nodeRender ignored due to unrendered parent");
  263. return;
  264. }
  265. // Create new <tr> after previous row
  266. // if( node.isStatusNode() ) {
  267. // newRow = tree.statusRowFragment.firstChild.cloneNode(true);
  268. // } else {
  269. newRow = tree.rowFragment.firstChild.cloneNode(true);
  270. // }
  271. prevNode = findPrevRowNode(node);
  272. // $.ui.fancytree.debug("*** nodeRender " + node + ": prev: " + prevNode.key);
  273. _assert(prevNode);
  274. if (collapsed === true && _recursive) {
  275. // hide all child rows, so we can use an animation to show it later
  276. newRow.style.display = "none";
  277. } else if (deep && ctx.hasCollapsedParents) {
  278. // also hide this row if deep === true but any parent is collapsed
  279. newRow.style.display = "none";
  280. // newRow.style.color = "red";
  281. }
  282. if (prevNode.tr) {
  283. insertSiblingAfter(prevNode.tr, newRow);
  284. } else {
  285. _assert(
  286. !prevNode.parent,
  287. "prev. row must have a tr, or be system root"
  288. );
  289. // tree.tbody.appendChild(newRow);
  290. insertFirstChild(tree.tbody, newRow); // #675
  291. }
  292. node.tr = newRow;
  293. if (node.key && opts.generateIds) {
  294. node.tr.id = opts.idPrefix + node.key;
  295. }
  296. node.tr.ftnode = node;
  297. // if(opts.aria){
  298. // $(node.tr).attr("aria-labelledby", "ftal_" + opts.idPrefix + node.key);
  299. // }
  300. node.span = $("span.fancytree-node", node.tr).get(0);
  301. // Set icon, link, and title (normally this is only required on initial render)
  302. this.nodeRenderTitle(ctx);
  303. // Allow tweaking, binding, after node was created for the first time
  304. // tree._triggerNodeEvent("createNode", ctx);
  305. if (opts.createNode) {
  306. opts.createNode.call(tree, { type: "createNode" }, ctx);
  307. }
  308. }
  309. }
  310. // Allow tweaking after node state was rendered
  311. // tree._triggerNodeEvent("renderNode", ctx);
  312. if (opts.renderNode) {
  313. opts.renderNode.call(tree, { type: "renderNode" }, ctx);
  314. }
  315. // Visit child nodes
  316. // Add child markup
  317. children = node.children;
  318. if (children && (isRootNode || deep || node.expanded)) {
  319. for (i = 0, l = children.length; i < l; i++) {
  320. subCtx = $.extend({}, ctx, { node: children[i] });
  321. subCtx.hasCollapsedParents =
  322. subCtx.hasCollapsedParents || !node.expanded;
  323. this.nodeRender(subCtx, force, deep, collapsed, true);
  324. }
  325. }
  326. // Make sure, that <tr> order matches node.children order.
  327. if (children && !_recursive) {
  328. // we only have to do it once, for the root branch
  329. prevTr = node.tr || null;
  330. firstTr = tree.tbody.firstChild;
  331. // Iterate over all descendants
  332. node.visit(function (n) {
  333. if (n.tr) {
  334. if (
  335. !n.parent.expanded &&
  336. n.tr.style.display !== "none"
  337. ) {
  338. // fix after a node was dropped over a collapsed
  339. n.tr.style.display = "none";
  340. setChildRowVisibility(n, false);
  341. }
  342. if (n.tr.previousSibling !== prevTr) {
  343. node.debug("_fixOrder: mismatch at node: " + n);
  344. var nextTr = prevTr ? prevTr.nextSibling : firstTr;
  345. tree.tbody.insertBefore(n.tr, nextTr);
  346. }
  347. prevTr = n.tr;
  348. }
  349. });
  350. }
  351. // Update element classes according to node state
  352. // if(!isRootNode){
  353. // this.nodeRenderStatus(ctx);
  354. // }
  355. },
  356. nodeRenderTitle: function (ctx, title) {
  357. var $cb,
  358. res,
  359. tree = ctx.tree,
  360. node = ctx.node,
  361. opts = ctx.options,
  362. isStatusNode = node.isStatusNode();
  363. res = this._super(ctx, title);
  364. if (node.isRootNode()) {
  365. return res;
  366. }
  367. // Move checkbox to custom column
  368. if (
  369. opts.checkbox &&
  370. !isStatusNode &&
  371. opts.table.checkboxColumnIdx != null
  372. ) {
  373. $cb = $("span.fancytree-checkbox", node.span); //.detach();
  374. $(node.tr)
  375. .find("td")
  376. .eq(+opts.table.checkboxColumnIdx)
  377. .html($cb);
  378. }
  379. // Update element classes according to node state
  380. this.nodeRenderStatus(ctx);
  381. if (isStatusNode) {
  382. if (opts.renderStatusColumns) {
  383. // Let user code write column content
  384. opts.renderStatusColumns.call(
  385. tree,
  386. { type: "renderStatusColumns" },
  387. ctx
  388. );
  389. } else if (opts.table.mergeStatusColumns && node.isTopLevel()) {
  390. $(node.tr)
  391. .find(">td")
  392. .eq(0)
  393. .prop("colspan", tree.columnCount)
  394. .text(node.title)
  395. .addClass("fancytree-status-merged")
  396. .nextAll()
  397. .remove();
  398. } // else: default rendering for status node: leave other cells empty
  399. } else if (opts.renderColumns) {
  400. opts.renderColumns.call(tree, { type: "renderColumns" }, ctx);
  401. }
  402. return res;
  403. },
  404. nodeRenderStatus: function (ctx) {
  405. var indent,
  406. node = ctx.node,
  407. opts = ctx.options;
  408. this._super(ctx);
  409. $(node.tr).removeClass("fancytree-node");
  410. // indent
  411. indent = (node.getLevel() - 1) * opts.table.indentation;
  412. if (opts.rtl) {
  413. $(node.span).css({ paddingRight: indent + "px" });
  414. } else {
  415. $(node.span).css({ paddingLeft: indent + "px" });
  416. }
  417. },
  418. /* Expand node, return Deferred.promise. */
  419. nodeSetExpanded: function (ctx, flag, callOpts) {
  420. // flag defaults to true
  421. flag = flag !== false;
  422. if ((ctx.node.expanded && flag) || (!ctx.node.expanded && !flag)) {
  423. // Expanded state isn't changed - just call base implementation
  424. return this._superApply(arguments);
  425. }
  426. var dfd = new $.Deferred(),
  427. subOpts = $.extend({}, callOpts, {
  428. noEvents: true,
  429. noAnimation: true,
  430. });
  431. callOpts = callOpts || {};
  432. function _afterExpand(ok, args) {
  433. // ctx.tree.info("ok:" + ok, args);
  434. if (ok) {
  435. // #1108 minExpandLevel: 2 together with table extension does not work
  436. // don't call when 'ok' is false:
  437. setChildRowVisibility(ctx.node, flag);
  438. if (
  439. flag &&
  440. ctx.options.autoScroll &&
  441. !callOpts.noAnimation &&
  442. ctx.node.hasChildren()
  443. ) {
  444. // Scroll down to last child, but keep current node visible
  445. ctx.node
  446. .getLastChild()
  447. .scrollIntoView(true, { topNode: ctx.node })
  448. .always(function () {
  449. if (!callOpts.noEvents) {
  450. ctx.tree._triggerNodeEvent(
  451. flag ? "expand" : "collapse",
  452. ctx
  453. );
  454. }
  455. dfd.resolveWith(ctx.node);
  456. });
  457. } else {
  458. if (!callOpts.noEvents) {
  459. ctx.tree._triggerNodeEvent(
  460. flag ? "expand" : "collapse",
  461. ctx
  462. );
  463. }
  464. dfd.resolveWith(ctx.node);
  465. }
  466. } else {
  467. if (!callOpts.noEvents) {
  468. ctx.tree._triggerNodeEvent(
  469. flag ? "expand" : "collapse",
  470. ctx
  471. );
  472. }
  473. dfd.rejectWith(ctx.node);
  474. }
  475. }
  476. // Call base-expand with disabled events and animation
  477. this._super(ctx, flag, subOpts)
  478. .done(function () {
  479. _afterExpand(true, arguments);
  480. })
  481. .fail(function () {
  482. _afterExpand(false, arguments);
  483. });
  484. return dfd.promise();
  485. },
  486. nodeSetStatus: function (ctx, status, message, details) {
  487. if (status === "ok") {
  488. var node = ctx.node,
  489. firstChild = node.children ? node.children[0] : null;
  490. if (firstChild && firstChild.isStatusNode()) {
  491. $(firstChild.tr).remove();
  492. }
  493. }
  494. return this._superApply(arguments);
  495. },
  496. treeClear: function (ctx) {
  497. this.nodeRemoveChildMarkup(this._makeHookContext(this.rootNode));
  498. return this._superApply(arguments);
  499. },
  500. treeDestroy: function (ctx) {
  501. this.$container.find("tbody").empty();
  502. if (this.$source) {
  503. this.$source.removeClass("fancytree-helper-hidden");
  504. }
  505. return this._superApply(arguments);
  506. },
  507. /*,
  508. treeSetFocus: function(ctx, flag) {
  509. // alert("treeSetFocus" + ctx.tree.$container);
  510. ctx.tree.$container.focus();
  511. $.ui.fancytree.focusTree = ctx.tree;
  512. }*/
  513. });
  514. // Value returned by `require('jquery.fancytree..')`
  515. return $.ui.fancytree;
  516. }); // End of closure