drilldownrow.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. // Copyright 2007 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview Tree-like drilldown components for HTML tables.
  16. *
  17. * This component supports expanding and collapsing groups of rows in
  18. * HTML tables. The behavior is like typical Tree widgets, but tables
  19. * need special support to enable the tree behaviors.
  20. *
  21. * Any row or rows in an HTML table can be DrilldownRows. The root
  22. * DrilldownRow nodes are always visible in the table, but the rest show
  23. * or hide as input events expand and collapse their ancestors.
  24. *
  25. * Programming them: Top-level DrilldownRows are made by decorating
  26. * a TR element. Children are made with addChild or addChildAt, and
  27. * are entered into the document by the render() method.
  28. *
  29. * A DrilldownRow can have any number of children. If it has no children
  30. * it can be loaded, not loaded, or with a load in progress.
  31. * Top-level DrilldownRows are always displayed (though setting
  32. * style.display on a containing DOM node could make one be not
  33. * visible to the user). A DrilldownRow can be expanded, or not. A
  34. * DrilldownRow displays if all of its ancestors are expanded.
  35. *
  36. * Set up event handlers and style each row for the application in an
  37. * enterDocument method.
  38. *
  39. * Children normally render into the document lazily, at the first
  40. * moment when all ancestors are expanded.
  41. *
  42. * @see ../demos/drilldownrow.html
  43. */
  44. // TODO(user): Build support for dynamically loading DrilldownRows,
  45. // probably using automplete as an example to follow.
  46. // TODO(user): Make DrilldownRows accessible through the keyboard.
  47. // The render method is redefined in this class because when addChildAt renders
  48. // the new child it assumes that the child's DOM node will be a child
  49. // of the parent component's DOM node, but all DOM nodes of DrilldownRows
  50. // in the same tree of DrilldownRows are siblings to each other.
  51. //
  52. // Arguments (or lack of arguments) to the render methods in Component
  53. // all determine the place of the new DOM node in the DOM tree, but
  54. // the place of a new DrilldownRow in the DOM needs to be determined by
  55. // its position in the tree of DrilldownRows.
  56. goog.provide('goog.ui.DrilldownRow');
  57. goog.require('goog.asserts');
  58. goog.require('goog.dom');
  59. goog.require('goog.dom.TagName');
  60. goog.require('goog.dom.classlist');
  61. goog.require('goog.dom.safe');
  62. goog.require('goog.html.SafeHtml');
  63. goog.require('goog.string.Unicode');
  64. goog.require('goog.ui.Component');
  65. /**
  66. * Builds a DrilldownRow component, which can overlay a tree
  67. * structure onto sections of an HTML table.
  68. *
  69. * @param {!goog.ui.DrilldownRow.DrilldownRowProperties=} opt_properties
  70. * Optional properties.
  71. * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
  72. * @constructor
  73. * @extends {goog.ui.Component}
  74. * @final
  75. */
  76. goog.ui.DrilldownRow = function(opt_properties, opt_domHelper) {
  77. goog.ui.Component.call(this, opt_domHelper);
  78. var properties = opt_properties || {};
  79. // Initialize instance variables.
  80. var html;
  81. if (!goog.isDefAndNotNull(properties.html)) {
  82. html = goog.html.SafeHtml.EMPTY;
  83. } else {
  84. goog.asserts.assert(properties.html instanceof goog.html.SafeHtml);
  85. html = properties.html;
  86. }
  87. /**
  88. * String of HTML to initialize the DOM structure for the table row.
  89. * Should have the form '<tr attr="etc">Row contents here</tr>'.
  90. * @type {!goog.html.SafeHtml}
  91. * @private
  92. */
  93. this.html_ = html;
  94. /**
  95. * Controls whether this component's children will show when it shows.
  96. * @type {boolean}
  97. * @private
  98. */
  99. this.expanded_ =
  100. typeof properties.expanded != 'undefined' ? properties.expanded : true;
  101. /**
  102. * If this component's DOM element is created from a string of
  103. * HTML, this is the function to call when it is entered into the DOM tree.
  104. * @type {Function} args are DrilldownRow and goog.events.EventHandler
  105. * of the DrilldownRow.
  106. * @private
  107. */
  108. this.decoratorFn_ = properties.decorator || goog.ui.DrilldownRow.decorate;
  109. /**
  110. * Is the DrilldownRow to be displayed? If it is rendered, this mirrors
  111. * the style.display of the DrilldownRow's row.
  112. * @type {boolean}
  113. * @private
  114. */
  115. this.displayed_ = true;
  116. };
  117. goog.inherits(goog.ui.DrilldownRow, goog.ui.Component);
  118. /**
  119. * Used to define properties for a new DrilldownRow. Properties can contain:
  120. * loaded: initializes the isLoaded property, defaults to true.
  121. * expanded: DrilldownRow expanded or not, default is true.
  122. * html: Relevant and required for DrilldownRows to be added as
  123. * children. Ignored when decorating an existing table row.
  124. * decorator: Function that accepts one DrilldownRow argument, and
  125. * should customize and style the row. The default is to call
  126. * goog.ui.DrilldownRow.decorator.
  127. * @typedef {{
  128. * loaded: (boolean|undefined),
  129. * expanded: (boolean|undefined),
  130. * html: (!goog.html.SafeHtml|undefined),
  131. * decorator: (Function|undefined)
  132. * }}
  133. */
  134. goog.ui.DrilldownRow.DrilldownRowProperties;
  135. /**
  136. * Example object with properties of the form accepted by the class
  137. * constructor. These are educational and show the compiler that
  138. * these properties can be set so it doesn't emit warnings.
  139. */
  140. goog.ui.DrilldownRow.sampleProperties = {
  141. html: goog.html.SafeHtml.create(
  142. goog.dom.TagName.TR, {},
  143. goog.html.SafeHtml.concat(
  144. goog.html.SafeHtml.create(goog.dom.TagName.TD, {}, 'Sample'),
  145. goog.html.SafeHtml.create(goog.dom.TagName.TD, {}, 'Sample'))),
  146. loaded: true,
  147. decorator: function(selfObj, handler) {
  148. // When the mouse is hovering, add CSS class goog-drilldown-hover.
  149. goog.ui.DrilldownRow.decorate(selfObj);
  150. var row = selfObj.getElement();
  151. handler.listen(row, 'mouseover', function() {
  152. goog.dom.classlist.add(row, goog.getCssName('goog-drilldown-hover'));
  153. });
  154. handler.listen(row, 'mouseout', function() {
  155. goog.dom.classlist.remove(row, goog.getCssName('goog-drilldown-hover'));
  156. });
  157. }
  158. };
  159. //
  160. // Implementations of Component methods.
  161. //
  162. /**
  163. * The base class method calls its superclass method and this
  164. * drilldown's 'decorator' method as defined in the constructor.
  165. * @override
  166. */
  167. goog.ui.DrilldownRow.prototype.enterDocument = function() {
  168. goog.ui.DrilldownRow.superClass_.enterDocument.call(this);
  169. this.decoratorFn_(this, this.getHandler());
  170. };
  171. /** @override */
  172. goog.ui.DrilldownRow.prototype.createDom = function() {
  173. this.setElementInternal(
  174. goog.ui.DrilldownRow.createRowNode_(this.html_, this.getDomHelper()));
  175. };
  176. /**
  177. * A top-level DrilldownRow decorates a TR element.
  178. *
  179. * @param {Element} node The element to test for decorability.
  180. * @return {boolean} true iff the node is a TR.
  181. * @override
  182. */
  183. goog.ui.DrilldownRow.prototype.canDecorate = function(node) {
  184. return node.tagName == goog.dom.TagName.TR;
  185. };
  186. /**
  187. * Child drilldowns are rendered when needed.
  188. *
  189. * @param {goog.ui.Component} child New DrilldownRow child to be added.
  190. * @param {number} index position to be occupied by the child.
  191. * @param {boolean=} opt_render true to force immediate rendering.
  192. * @override
  193. */
  194. goog.ui.DrilldownRow.prototype.addChildAt = function(child, index, opt_render) {
  195. goog.asserts.assertInstanceof(child, goog.ui.DrilldownRow);
  196. goog.ui.DrilldownRow.superClass_.addChildAt.call(this, child, index, false);
  197. child.setDisplayable_(this.isVisible_() && this.isExpanded());
  198. if (opt_render && !child.isInDocument()) {
  199. child.render();
  200. }
  201. };
  202. /** @override */
  203. goog.ui.DrilldownRow.prototype.removeChild = function(child) {
  204. goog.dom.removeNode(child.getElement());
  205. return goog.ui.DrilldownRow.superClass_.removeChild.call(this, child);
  206. };
  207. /**
  208. * Rendering of DrilldownRow's is on need, do not call this directly
  209. * from application code.
  210. *
  211. * Rendering a DrilldownRow places it according to its position in its
  212. * tree of DrilldownRows. DrilldownRows cannot be placed any other
  213. * way so this method does not use any arguments. This does not call
  214. * the base class method and does not modify any of this
  215. * DrilldownRow's children.
  216. * @override
  217. */
  218. goog.ui.DrilldownRow.prototype.render = function() {
  219. if (arguments.length) {
  220. throw Error('A DrilldownRow cannot be placed under a specific parent.');
  221. } else {
  222. var parent = this.getParent();
  223. if (!parent.isInDocument()) {
  224. throw Error('Cannot render child of un-rendered parent');
  225. }
  226. // The new child's TR node needs to go just after the last TR
  227. // of the part of the parent's subtree that is to the left
  228. // of this. The subtree includes the parent.
  229. goog.asserts.assertInstanceof(parent, goog.ui.DrilldownRow);
  230. var previous = parent.previousRenderedChild_(this);
  231. var row;
  232. if (previous) {
  233. goog.asserts.assertInstanceof(previous, goog.ui.DrilldownRow);
  234. row = previous.lastRenderedLeaf_().getElement();
  235. } else {
  236. row = parent.getElement();
  237. }
  238. row = /** @type {Element} */ (row.nextSibling);
  239. // Render the child row component into the document.
  240. if (row) {
  241. this.renderBefore(row);
  242. } else {
  243. // Render at the end of the parent of this DrilldownRow's
  244. // DOM element.
  245. var tbody = /** @type {Element} */ (parent.getElement().parentNode);
  246. goog.ui.DrilldownRow.superClass_.render.call(this, tbody);
  247. }
  248. }
  249. };
  250. /**
  251. * Finds the numeric index of this child within its parent Component.
  252. * Throws an exception if it has no parent.
  253. *
  254. * @return {number} index of this within the children of the parent Component.
  255. */
  256. goog.ui.DrilldownRow.prototype.findIndex = function() {
  257. var parent = this.getParent();
  258. if (!parent) {
  259. throw Error('Component has no parent');
  260. }
  261. return parent.indexOfChild(this);
  262. };
  263. //
  264. // Type-specific operations
  265. //
  266. /**
  267. * Returns the expanded state of the DrilldownRow.
  268. *
  269. * @return {boolean} true iff this is expanded.
  270. */
  271. goog.ui.DrilldownRow.prototype.isExpanded = function() {
  272. return this.expanded_;
  273. };
  274. /**
  275. * Sets the expanded state of this DrilldownRow: makes all children
  276. * displayable or not displayable corresponding to the expanded state.
  277. *
  278. * @param {boolean} expanded whether this should be expanded or not.
  279. */
  280. goog.ui.DrilldownRow.prototype.setExpanded = function(expanded) {
  281. if (expanded != this.expanded_) {
  282. this.expanded_ = expanded;
  283. var elem = this.getElement();
  284. goog.asserts.assert(elem);
  285. goog.dom.classlist.toggle(elem, goog.getCssName('goog-drilldown-expanded'));
  286. goog.dom.classlist.toggle(
  287. elem, goog.getCssName('goog-drilldown-collapsed'));
  288. if (this.isVisible_()) {
  289. this.forEachChild(function(child) { child.setDisplayable_(expanded); });
  290. }
  291. }
  292. };
  293. /**
  294. * Returns this DrilldownRow's level in the tree. Top level is 1.
  295. *
  296. * @return {number} depth of this DrilldownRow in its tree of drilldowns.
  297. */
  298. goog.ui.DrilldownRow.prototype.getDepth = function() {
  299. for (var component = this, depth = 0;
  300. component instanceof goog.ui.DrilldownRow;
  301. component = component.getParent(), depth++) {
  302. }
  303. return depth;
  304. };
  305. /**
  306. * This static function is a default decorator that adds HTML at the
  307. * beginning of the first cell to display indentation and an expander
  308. * image; sets up a click handler on the toggler; initializes a class
  309. * for the row: either goog-drilldown-expanded or
  310. * goog-drilldown-collapsed, depending on the initial state of the
  311. * DrilldownRow; and sets up a click event handler on the toggler
  312. * element.
  313. *
  314. * This creates a DIV with class=toggle. Your application can set up
  315. * CSS style rules something like this:
  316. *
  317. * tr.goog-drilldown-expanded .toggle {
  318. * background-image: url('minus.png');
  319. * }
  320. *
  321. * tr.goog-drilldown-collapsed .toggle {
  322. * background-image: url('plus.png');
  323. * }
  324. *
  325. * These background images show whether the DrilldownRow is expanded.
  326. *
  327. * @param {goog.ui.DrilldownRow} selfObj DrilldownRow to be decorated.
  328. */
  329. goog.ui.DrilldownRow.decorate = function(selfObj) {
  330. var depth = selfObj.getDepth();
  331. var row = selfObj.getElement();
  332. goog.asserts.assert(row);
  333. if (!row.cells) {
  334. throw Error('No cells');
  335. }
  336. var cell = row.cells[0];
  337. var dom = selfObj.getDomHelper();
  338. var fragment = dom.createDom(
  339. goog.dom.TagName.DIV, {'style': 'float: left; width: ' + depth + 'em;'},
  340. dom.createDom(
  341. goog.dom.TagName.DIV,
  342. {'class': 'toggle', 'style': 'width: 1em; float: right;'},
  343. // NOTE: NBSP is probably only needed by IE6. This div can probably be
  344. // made contentless.
  345. goog.string.Unicode.NBSP));
  346. cell.insertBefore(fragment, cell.firstChild);
  347. goog.dom.classlist.add(
  348. row, selfObj.isExpanded() ? goog.getCssName('goog-drilldown-expanded') :
  349. goog.getCssName('goog-drilldown-collapsed'));
  350. // Default mouse event handling:
  351. var toggler =
  352. goog.dom.getElementsByTagName(goog.dom.TagName.DIV, fragment)[0];
  353. selfObj.getHandler().listen(toggler, 'click', function(event) {
  354. selfObj.setExpanded(!selfObj.isExpanded());
  355. });
  356. };
  357. //
  358. // Private methods
  359. //
  360. /**
  361. * Turn display of a DrilldownRow on or off. If the DrilldownRow has not
  362. * yet been rendered, this renders it. This propagates the effect
  363. * of the change recursively as needed -- children displaying iff the
  364. * parent is displayed and expanded.
  365. *
  366. * @param {boolean} display state, true iff display is desired.
  367. * @private
  368. */
  369. goog.ui.DrilldownRow.prototype.setDisplayable_ = function(display) {
  370. if (display && !this.isInDocument()) {
  371. this.render();
  372. }
  373. if (this.displayed_ == display) {
  374. return;
  375. }
  376. this.displayed_ = display;
  377. if (this.isInDocument()) {
  378. this.getElement().style.display = display ? '' : 'none';
  379. }
  380. var selfObj = this;
  381. this.forEachChild(function(child) {
  382. child.setDisplayable_(display && selfObj.expanded_);
  383. });
  384. };
  385. /**
  386. * True iff this and all its DrilldownRow parents are displayable. The
  387. * value is an approximation to actual visibility, since it does not
  388. * look at whether DOM nodes containing the top-level component have
  389. * display: none, visibility: hidden or are otherwise not displayable.
  390. * So this visibility is relative to the top-level component.
  391. *
  392. * @return {boolean} visibility of this relative to its top-level drilldown.
  393. * @private
  394. */
  395. goog.ui.DrilldownRow.prototype.isVisible_ = function() {
  396. for (var component = this; component instanceof goog.ui.DrilldownRow;
  397. component = component.getParent()) {
  398. if (!component.displayed_) return false;
  399. }
  400. return true;
  401. };
  402. /**
  403. * Create and return a TR element from HTML that looks like
  404. * "<tr> ... </tr>".
  405. *
  406. * @param {!goog.html.SafeHtml} html for one row.
  407. * @param {!goog.dom.DomHelper} dom DOM to hold the Element.
  408. * @return {Element} table row node created from the HTML.
  409. * @private
  410. */
  411. goog.ui.DrilldownRow.createRowNode_ = function(html, dom) {
  412. // Note: this may be slow.
  413. var tableHtml = goog.html.SafeHtml.create(goog.dom.TagName.TABLE, {}, html);
  414. var div = dom.createElement(goog.dom.TagName.DIV);
  415. goog.dom.safe.setInnerHtml(div, tableHtml);
  416. return div.firstChild.rows[0];
  417. };
  418. /**
  419. * Get the recursively rightmost child that is in the document.
  420. *
  421. * @return {goog.ui.DrilldownRow} rightmost child currently entered in
  422. * the document, potentially this DrilldownRow. If this is in the
  423. * document, result is non-null.
  424. * @private
  425. */
  426. goog.ui.DrilldownRow.prototype.lastRenderedLeaf_ = function() {
  427. var leaf = null;
  428. for (var node = this; node && node.isInDocument();
  429. // Node will become undefined if parent has no children.
  430. node = node.getChildAt(node.getChildCount() - 1)) {
  431. leaf = node;
  432. }
  433. return /** @type {goog.ui.DrilldownRow} */ (leaf);
  434. };
  435. /**
  436. * Search this node's direct children for the last one that is in the
  437. * document and is before the given child.
  438. * @param {goog.ui.DrilldownRow} child The child to stop the search at.
  439. * @return {goog.ui.Component?} The last child component before the given child
  440. * that is in the document.
  441. * @private
  442. */
  443. goog.ui.DrilldownRow.prototype.previousRenderedChild_ = function(child) {
  444. for (var i = this.getChildCount() - 1; i >= 0; i--) {
  445. if (this.getChildAt(i) == child) {
  446. for (var j = i - 1; j >= 0; j--) {
  447. var prev = this.getChildAt(j);
  448. if (prev.isInDocument()) {
  449. return prev;
  450. }
  451. }
  452. }
  453. }
  454. return null;
  455. };