77 * Released under the MIT license
88 * https://github.com/mar10/fancytree/wiki/LicenseInfo
99 *
10- * @version 2.0 .0
11- * @date 2014-05-01T21:48
10+ * @version 2.1 .0
11+ * @date 2014-05-29T16:44
1212 */
1313
1414/** Core Fancytree module.
@@ -61,6 +61,11 @@ function consoleApply(method, args){
6161 }
6262}
6363
64+ /*Return true if x is a FancytreeNode.*/
65+ function _isNode ( x ) {
66+ return ! ! ( x . tree && x . statusNodeType !== undefined ) ;
67+ }
68+
6469/** Return true if dotted version string is equal or higher than requested version.
6570 *
6671 * See http://jsfiddle.net/mar10/FjSAN/
@@ -275,6 +280,8 @@ function FancytreeNode(parent, obj){
275280 } else {
276281 this . key = "_" + ( FT . _nextNodeKey ++ ) ;
277282 }
283+ } else {
284+ this . key = "" + this . key ; // Convert to string (#217)
278285 }
279286
280287 // Fix tree.activeNode
@@ -683,9 +690,8 @@ FancytreeNode.prototype = /** @lends FancytreeNode# */{
683690 // recursively set children and render
684691 this . removeChildren ( ) ;
685692 this . addChildren ( dict . children ) ;
686- } else {
687- this . renderTitle ( ) ;
688693 }
694+ this . renderTitle ( ) ;
689695/*
690696 var children = dict.children;
691697 if(children === undefined){
@@ -1350,79 +1356,93 @@ FancytreeNode.prototype = /** @lends FancytreeNode# */{
13501356 /**
13511357 *
13521358 * @param {boolean | PlainObject } [effects=false] animation options.
1353- * @param {FancytreeNode } [topNode =null] this node will remain visible in
1359+ * @param {object } [options =null] {topNode: null, effects: ..., parent: ...} this node will remain visible in
13541360 * any case, even if `this` is outside the scroll pane.
13551361 * @returns {$.Promise }
13561362 */
1357- scrollIntoView : function ( effects , topNode ) {
1358- effects = ( effects === true ) ? { duration : 200 , queue : false } : effects ;
1359- var topNodeY ,
1363+ scrollIntoView : function ( effects , options ) {
1364+ if ( options !== undefined && _isNode ( options ) ) {
1365+ this . warn ( "scrollIntoView() with 'topNode' option is deprecated since 2014-05-08. Use 'options.topNode' instead." ) ;
1366+ options = { topNode : options } ;
1367+ }
1368+ // this.$scrollParent = (this.options.scrollParent === "auto") ? $ul.scrollParent() : $(this.options.scrollParent);
1369+ // this.$scrollParent = this.$scrollParent.length ? this.$scrollParent || this.$container;
1370+
1371+ var topNodeY , nodeY , horzScrollbarHeight , containerOffsetTop ,
1372+ opts = $ . extend ( {
1373+ effects : ( effects === true ) ? { duration : 200 , queue : false } : effects ,
1374+ scrollOfs : this . tree . options . scrollOfs ,
1375+ scrollParent : this . tree . options . scrollParent || this . tree . $container ,
1376+ topNode : null
1377+ } , options ) ,
13601378 dfd = new $ . Deferred ( ) ,
13611379 that = this ,
1362- nodeY = $ ( this . span ) . position ( ) . top ,
13631380 nodeHeight = $ ( this . span ) . height ( ) ,
1364- $container = this . tree . $container ,
1365- scrollTop = $container [ 0 ] . scrollTop ,
1366- horzScrollHeight = Math . max ( 0 , ( $container . innerHeight ( ) - $container [ 0 ] . clientHeight ) ) ,
1367- // containerHeight = $container.height(),
1368- containerHeight = $container . height ( ) - horzScrollHeight ,
1381+ $container = $ ( opts . scrollParent ) ,
1382+ topOfs = opts . scrollOfs . top || 0 ,
1383+ bottomOfs = opts . scrollOfs . bottom || 0 ,
1384+ containerHeight = $container . height ( ) , // - topOfs - bottomOfs,
1385+ scrollTop = $container . scrollTop ( ) ,
1386+ $animateTarget = $container ,
1387+ isParentWindow = $container [ 0 ] === window ,
1388+ topNode = opts . topNode || null ,
13691389 newScrollTop = null ;
13701390
1371- // console.log("horzScrollHeight: " + horzScrollHeight);
1372- // console.log("$container[0].scrollTop: " + $container[0].scrollTop);
1373- // console.log("$container[0].scrollHeight: " + $container[0].scrollHeight);
1374- // console.log("$container[0].clientHeight: " + $container[0].clientHeight);
1375- // console.log("$container.innerHeight(): " + $container.innerHeight());
1376- // console.log("$container.height(): " + $container.height());
1377-
1378- if ( nodeY < 0 ) {
1379- newScrollTop = scrollTop + nodeY ;
1380- } else if ( ( nodeY + nodeHeight ) > containerHeight ) {
1381- newScrollTop = scrollTop + nodeY - containerHeight + nodeHeight ;
1391+ // this.debug("scrollIntoView(), scrollTop=", scrollTop, opts.scrollOfs);
1392+ _assert ( $ ( this . span ) . is ( ":visible" ) , "scrollIntoView node is invisible" ) ; // otherwise we cannot calc offsets
1393+
1394+ if ( isParentWindow ) {
1395+ nodeY = $ ( this . span ) . offset ( ) . top ;
1396+ topNodeY = topNode ? $ ( topNode . span ) . offset ( ) . top : 0 ;
1397+ $animateTarget = $ ( "html,body" ) ;
1398+
1399+ } else {
1400+ _assert ( $container [ 0 ] !== document && $container [ 0 ] !== document . body , "scrollParent should be an simple element or `window`, not document or body." ) ;
1401+
1402+ containerOffsetTop = $container . offset ( ) . top ,
1403+ nodeY = $ ( this . span ) . offset ( ) . top - containerOffsetTop + scrollTop ; // relative to scroll parent
1404+ topNodeY = topNode ? $ ( topNode . span ) . offset ( ) . top - containerOffsetTop + scrollTop : 0 ;
1405+ horzScrollbarHeight = Math . max ( 0 , ( $container . innerHeight ( ) - $container [ 0 ] . clientHeight ) ) ;
1406+ containerHeight -= horzScrollbarHeight ;
1407+ }
1408+
1409+ // this.debug(" scrollIntoView(), nodeY=", nodeY, "containerHeight=", containerHeight);
1410+ if ( nodeY < ( scrollTop + topOfs ) ) {
1411+ // Node is above visible container area
1412+ newScrollTop = nodeY - topOfs ;
1413+ // this.debug(" scrollIntoView(), UPPER newScrollTop=", newScrollTop);
1414+
1415+ } else if ( ( nodeY + nodeHeight ) > ( scrollTop + containerHeight - bottomOfs ) ) {
1416+ newScrollTop = nodeY + nodeHeight - containerHeight + bottomOfs ;
1417+ // this.debug(" scrollIntoView(), LOWER newScrollTop=", newScrollTop);
13821418 // If a topNode was passed, make sure that it is never scrolled
13831419 // outside the upper border
13841420 if ( topNode ) {
1385- topNodeY = topNode ? $ ( topNode . span ) . position ( ) . top : 0 ;
1386- if ( ( nodeY - topNodeY ) > containerHeight ) {
1387- newScrollTop = scrollTop + topNodeY ;
1421+ _assert ( $ ( topNode . span ) . is ( ":visible" ) ) ;
1422+ if ( topNodeY < newScrollTop ) {
1423+ newScrollTop = topNodeY - topOfs ;
1424+ // this.debug(" scrollIntoView(), TOP newScrollTop=", newScrollTop);
13881425 }
13891426 }
13901427 }
1428+
13911429 if ( newScrollTop !== null ) {
1392- if ( effects ) {
1393- // TODO: resolve dfd after animation
1394- // var that = this;
1395- effects . complete = function ( ) {
1430+ // this.debug(" scrollIntoView(), SET newScrollTop=", newScrollTop);
1431+ if ( opts . effects ) {
1432+ opts . effects . complete = function ( ) {
13961433 dfd . resolveWith ( that ) ;
13971434 } ;
1398- $container . animate ( {
1435+ $animateTarget . stop ( true ) . animate ( {
13991436 scrollTop : newScrollTop
1400- } , effects ) ;
1437+ } , opts . effects ) ;
14011438 } else {
1402- $container [ 0 ] . scrollTop = newScrollTop ;
1439+ $animateTarget [ 0 ] . scrollTop = newScrollTop ;
14031440 dfd . resolveWith ( this ) ;
14041441 }
14051442 } else {
14061443 dfd . resolveWith ( this ) ;
14071444 }
14081445 return dfd . promise ( ) ;
1409- /* from jQuery.menu:
1410- var borderTop, paddingTop, offset, scroll, elementHeight, itemHeight;
1411- if ( this._hasScroll() ) {
1412- borderTop = parseFloat( $.css( this.activeMenu[0], "borderTopWidth" ) ) || 0;
1413- paddingTop = parseFloat( $.css( this.activeMenu[0], "paddingTop" ) ) || 0;
1414- offset = item.offset().top - this.activeMenu.offset().top - borderTop - paddingTop;
1415- scroll = this.activeMenu.scrollTop();
1416- elementHeight = this.activeMenu.height();
1417- itemHeight = item.height();
1418-
1419- if ( offset < 0 ) {
1420- this.activeMenu.scrollTop( scroll + offset );
1421- } else if ( offset + itemHeight > elementHeight ) {
1422- this.activeMenu.scrollTop( scroll + offset - elementHeight + itemHeight );
1423- }
1424- }
1425- */
14261446 } ,
14271447
14281448 /**Activate this node.
@@ -2817,8 +2837,8 @@ $.extend(Fancytree.prototype,
28172837 // folder or doctype icon
28182838 role = aria ? " role='img'" : "" ;
28192839 if ( icon && typeof icon === "string" ) {
2820- imageSrc = ( icon . charAt ( 0 ) === "/" ) ? icon : ( opts . imagePath + icon ) ;
2821- ares . push ( "<img src='" + imageSrc + "' alt='' />" ) ;
2840+ imageSrc = ( icon . charAt ( 0 ) === "/" ) ? icon : ( ( opts . imagePath || "" ) + icon ) ;
2841+ ares . push ( "<img src='" + imageSrc + "' class='fancytree-icon' alt='' />" ) ;
28222842 } else if ( node . data . iconclass ) {
28232843 // TODO: review and test and document
28242844 ares . push ( "<span " + role + " class='fancytree-custom-icon" + " " + node . data . iconclass + "'></span>" ) ;
@@ -2834,7 +2854,6 @@ $.extend(Fancytree.prototype,
28342854 nodeTitle = opts . renderTitle . call ( tree , { type : "renderTitle" } , ctx ) || "" ;
28352855 }
28362856 if ( ! nodeTitle ) {
2837- // TODO: escape tooltip string
28382857 tooltip = node . tooltip ? " title='" + FT . escapeHtml ( node . tooltip ) + "'" : "" ;
28392858 id = aria ? " id='ftal_" + node . key + "'" : "" ;
28402859 role = aria ? " role='treeitem'" : "" ;
@@ -2979,7 +2998,6 @@ $.extend(Fancytree.prototype,
29792998 node = ctx . node ,
29802999 tree = ctx . tree ,
29813000 opts = ctx . options ,
2982- // userEvent = !!ctx.originalEvent,
29833001 noEvents = ( callOpts . noEvents === true ) ,
29843002 isActive = ( node === tree . activeNode ) ;
29853003
@@ -3003,7 +3021,7 @@ $.extend(Fancytree.prototype,
30033021 }
30043022 if ( opts . activeVisible ) {
30053023 // tree.nodeMakeVisible(ctx);
3006- node . makeVisible ( ) ;
3024+ node . makeVisible ( { scrollIntoView : false } ) ; // nodeSetFocus will scroll
30073025 }
30083026 tree . activeNode = node ;
30093027 tree . nodeRenderStatus ( ctx ) ;
@@ -3080,9 +3098,9 @@ $.extend(Fancytree.prototype,
30803098 }
30813099 // Trigger expand/collapse after expanding
30823100 dfd . done ( function ( ) {
3083- if ( opts . autoScroll && ! noAnimation ) {
3101+ if ( flag && opts . autoScroll && ! noAnimation ) {
30843102 // Scroll down to last child, but keep current node visible
3085- node . getLastChild ( ) . scrollIntoView ( true , node ) . always ( function ( ) {
3103+ node . getLastChild ( ) . scrollIntoView ( true , { topNode : node } ) . always ( function ( ) {
30863104 if ( ! noEvents ) {
30873105 ctx . tree . _triggerNodeEvent ( flag ? "expand" : "collapse" , ctx ) ;
30883106 }
@@ -3093,7 +3111,6 @@ $.extend(Fancytree.prototype,
30933111 }
30943112 }
30953113 } ) ;
3096-
30973114 // vvv Code below is executed after loading finished:
30983115 _afterLoad = function ( callback ) {
30993116 var duration , easing , isVisible , isExpanded ;
@@ -3201,7 +3218,7 @@ $.extend(Fancytree.prototype,
32013218 this . _callHook ( "treeSetFocus" , ctx , true , true ) ;
32023219 }
32033220 // this.nodeMakeVisible(ctx);
3204- node . makeVisible ( ) ;
3221+ node . makeVisible ( { scrollIntoView : false } ) ;
32053222 tree . focusNode = node ;
32063223// node.debug("FOCUS...");
32073224// $(node.span).find(".fancytree-title").focus();
@@ -3512,6 +3529,8 @@ $.widget("ui.fancytree",
35123529 keyboard : true ,
35133530 keyPathSeparator : "/" ,
35143531 minExpandLevel : 1 ,
3532+ scrollOfs : { top : 0 , bottom : 0 } ,
3533+ scrollParent : null ,
35153534 selectMode : 2 ,
35163535 strings : {
35173536 loading : "Loading…" ,
@@ -3772,7 +3791,7 @@ $.extend($.ui.fancytree,
37723791 /** @lends Fancytree_Static# */
37733792 {
37743793 /** @type {string } */
3775- version : "2.0 .0" , // Set to semver by 'grunt release'
3794+ version : "2.1 .0" , // Set to semver by 'grunt release'
37763795 /** @type {string } */
37773796 buildType : "production" , // Set to 'production' by 'grunt build'
37783797 /** @type {int } */
@@ -3800,6 +3819,29 @@ $.extend($.ui.fancytree,
38003819 assert : function ( cond , msg ) {
38013820 return _assert ( cond , msg ) ;
38023821 } ,
3822+ /** Return a function that executes *fn* at most every *timeout* ms.
3823+ * @param {integer } timeout
3824+ * @param {function } fn
3825+ * @param {boolean } [invokeAsap=false]
3826+ * @param {any } [ctx]
3827+ */
3828+ debounce : function ( timeout , fn , invokeAsap , ctx ) {
3829+ var timer ;
3830+ if ( arguments . length === 3 && typeof invokeAsap !== "boolean" ) {
3831+ ctx = invokeAsap ;
3832+ invokeAsap = false ;
3833+ }
3834+ return function ( ) {
3835+ var args = arguments ;
3836+ ctx = ctx || this ;
3837+ invokeAsap && ! timer && fn . apply ( ctx , args ) ;
3838+ clearTimeout ( timer ) ;
3839+ timer = setTimeout ( function ( ) {
3840+ invokeAsap || fn . apply ( ctx , args ) ;
3841+ timer = null ;
3842+ } , timeout ) ;
3843+ } ;
3844+ } ,
38033845 /** Write message to console if debugLevel >= 2
38043846 * @param {string } msg
38053847 */
@@ -3850,8 +3892,7 @@ $.extend($.ui.fancytree,
38503892 getEventTarget : function ( event ) {
38513893 var tcn = event && event . target ? event . target . className : "" ,
38523894 res = { node : this . getNode ( event . target ) , type : undefined } ;
3853- // tcn may contains UI themeroller or Font Awesome classes, so we use
3854- // a fast version of $(res.node).hasClass()
3895+ // We use a fast version of $(res.node).hasClass()
38553896 // See http://jsperf.com/test-for-classname/2
38563897 if ( / \b f a n c y t r e e - t i t l e \b / . test ( tcn ) ) {
38573898 res . type = "title" ;
@@ -3862,8 +3903,10 @@ $.extend($.ui.fancytree,
38623903 } else if ( / \b f a n c y t r e e - i c o n \b / . test ( tcn ) ) {
38633904 res . type = "icon" ;
38643905 } else if ( / \b f a n c y t r e e - n o d e \b / . test ( tcn ) ) {
3865- // TODO: (http://code.google.com/p/dynatree/issues/detail?id=93)
3866- // res.type = this._getTypeForOuterNodeEvent(event);
3906+ // Somewhere near the title
3907+ res . type = "title" ;
3908+ } else if ( event && event . target && $ ( event . target ) . closest ( ".fancytree-title" ) . length ) {
3909+ // #228: clicking an embedded element inside a title
38673910 res . type = "title" ;
38683911 }
38693912 return res ;
0 commit comments