├── .gitignore ├── LICENSE ├── README.md ├── dist └── felixhayashi │ └── topstoryview │ ├── config.js │ ├── files │ ├── layout.css │ └── tiddlywiki.files │ ├── plugin.info │ ├── tiddlers │ ├── config.tid │ ├── license.tid │ └── readme.tid │ └── top_story_view.js └── src ├── gulpfile.js ├── node_modules └── plugins └── felixhayashi └── topstoryview ├── config.js ├── files ├── layout.scss └── tiddlywiki.files ├── plugin.info ├── tiddlers ├── config.tid ├── license.tid └── readme.tid └── top_story_view.js /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore all files with a leading dot 2 | .* 3 | !.gitignore 4 | !.git 5 | 6 | # ignore file in these paths 7 | src/note_modules/ 8 | src/jsdoc/templates/docstrap/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Felix Küppers 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TopStoryView 2 | 3 | TopStoryView is a TiddlyWiki plugin that adds another story view to tiddlywiki -- the *top* view. 4 | 5 | ## What it does 6 | 7 | It behaves like TiddlyWiki's *classic* story view but with a different scrolling strategy: 8 | 9 | * When opening or navigating to a tiddler, the site is scrolled until the title of the tiddler reaches the very top of the browser window. 10 | * Even the last tiddler is scrolled till it reaches the top. This works because the plugin adds an invisible spacer at the bottom so the tiddler can be scrolled to the top. 11 | 12 | ### What happens if a tiddler at the bottom of the story-river is removed? 13 | 14 | In this case the plugin will put the tiddler above into focus (unless the story river is empty). 15 | 16 | ## How it looks like 17 | 18 | The [demo site](http://tiddlymap.org) of TiddlyMap uses this story view. 19 | 20 | ## How to install and configure it? 21 | 22 | You can use this plugin by importing it (e.g. from the [TiddlyMap demo site](http://tiddlymap.org)) and switching your wiki's story view to *top* story view. 23 | 24 | ![selection_426](https://cloud.githubusercontent.com/assets/4307137/5669923/f6409ca6-977b-11e4-94ba-134248aa7305.png) 25 | 26 | You can configure the plugin by opening TiddlyWiki's configuration and selecting TopStoryView in the plugin section. 27 | 28 | ![selection_584](https://cloud.githubusercontent.com/assets/4307137/6992188/3c8f75e8-dabf-11e4-8d04-04bb64c7dc4e.png) 29 | 30 | ## Motivation 31 | 32 | See this discussion https://github.com/Jermolene/TiddlyWiki5/issues/1290 -------------------------------------------------------------------------------- /dist/felixhayashi/topstoryview/config.js: -------------------------------------------------------------------------------- 1 | /*\ 2 | 3 | title: $:/plugins/felixhayashi/topstoryview/config.js 4 | type: application/javascript 5 | module-type: library 6 | 7 | @preserve 8 | 9 | \*/ 10 | (function(){"use strict";exports.config={classNames:{storyRiver:"tc-story-river",backDrop:"story-backdrop",tiddlerFrame:"tc-tiddler-frame",tiddlerTitle:"tc-title"},references:{userConfig:"$:/config/topStoryView",focussedTiddlerStore:"$:/temp/focussedTiddler",refreshTrigger:"$:/temp/focussedTiddler/refresh"},checkbackTime:$tw.utils.getAnimationDuration()}})(); -------------------------------------------------------------------------------- /dist/felixhayashi/topstoryview/files/layout.css: -------------------------------------------------------------------------------- 1 | html .tc-story-river:after { 2 | content: ""; 3 | display: block; } 4 | -------------------------------------------------------------------------------- /dist/felixhayashi/topstoryview/files/tiddlywiki.files: -------------------------------------------------------------------------------- 1 | { 2 | "tiddlers": [ 3 | { 4 | "file": "layout.css", "fields": { 5 | "title": "$:/plugins/felixhayashi/topstoryview/layout", 6 | "type": "text/vnd.tiddlywiki", 7 | "tags": [ "$:/tags/Stylesheet" ] 8 | }} 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /dist/felixhayashi/topstoryview/plugin.info: -------------------------------------------------------------------------------- 1 | { 2 | "title": "$:/plugins/felixhayashi/topstoryview", 3 | "description": "TopStoryView – Always scroll navigated tiddlers to the top", 4 | "author": "Felix Küppers", 5 | "version": "0.2.0", 6 | "released": "", 7 | "core-version": ">=5.1.5", 8 | "source": "https://github.com/felixhayashi/TW5-TopStoryView", 9 | "type": "application/json", 10 | "plugin-type": "plugin", 11 | "list": "Configuration License Readme", 12 | "dependents": "" 13 | } -------------------------------------------------------------------------------- /dist/felixhayashi/topstoryview/tiddlers/config.tid: -------------------------------------------------------------------------------- 1 | title: $:/plugins/felixhayashi/topstoryview/Configuration 2 | 3 | Please see the [[GitHub page|https://github.com/felixhayashi/TW5-TopStoryView]] for more information on the options. 4 | 5 | Save and reload the wiki to activate changes. 6 | 7 | 8 | 9 | 10 | 11 | 12 |
Scroll offset:<$edit-text tiddler="$:/config/topStoryView" field="scroll-offset" tag="input" default="150px" />
-------------------------------------------------------------------------------- /dist/felixhayashi/topstoryview/tiddlers/license.tid: -------------------------------------------------------------------------------- 1 | title: $:/plugins/felixhayashi/topstoryview/License 2 | 3 | This code is released under the BSD license. For the exact terms visit: 4 | 5 | https://github.com/felixhayashi/TW5-TopStoryView/blob/master/LICENSE -------------------------------------------------------------------------------- /dist/felixhayashi/topstoryview/tiddlers/readme.tid: -------------------------------------------------------------------------------- 1 | title: $:/plugins/felixhayashi/topstoryview/Readme 2 | 3 | Please visit the [[GitHub page|https://github.com/felixhayashi/TW5-TopStoryView]] for more information. -------------------------------------------------------------------------------- /dist/felixhayashi/topstoryview/top_story_view.js: -------------------------------------------------------------------------------- 1 | /*\ 2 | title: $:/plugins/felixhayashi/topstoryview/top.js 3 | type: application/javascript 4 | module-type: storyview 5 | 6 | Views the story as a linear sequence 7 | 8 | @preserve 9 | 10 | \*/ 11 | (function(){"use strict";var t=require("$:/plugins/felixhayashi/topstoryview/config.js").config;var e="cubic-bezier(0.645, 0.045, 0.355, 1)";var i=function(e){this.listWidget=e;this.pageScroller=new $tw.utils.PageScroller;this.pageScroller.scrollIntoView=this.scrollIntoView;this.pageScroller.storyRiverDomNode=document.getElementsByClassName(t.classNames.storyRiver)[0];var i=$tw.wiki.getTiddler(t.references.userConfig);var o=i?i.fields:{};$tw.hooks.addHook("th-opening-default-tiddlers-list",this.hookOpenDefaultTiddlers);var r=parseInt(o["scroll-offset"]);this.pageScroller.scrollOffset=isNaN(r)?71:r;this.recalculateBottomSpace()};i.prototype.refreshStart=function(t,e){};i.prototype.refreshEnd=function(t,e){};i.prototype.hookOpenDefaultTiddlers=function(t){return t};i.prototype.navigateTo=function(t){var e=this.listWidget.findListItem(0,t.title);if(e===undefined)return;var i=this.listWidget.children[e];var o=i.findFirstDomNode();if(!(o instanceof Element))return;this.pageScroller.scrollIntoView(o)};i.prototype.insert=function(t){if(!t)return;var e=t.findFirstDomNode();if(!(e instanceof Element))return;this.startInsertAnimation(e,function(){this.recalculateBottomSpace()}.bind(this))};i.prototype.remove=function(t){if(!t)return;var e=t.findFirstDomNode();if(!(e instanceof Element)){t.removeChildDomNodes();return}var i=this.getLastFrame()===e;this.startRemoveAnimation(t,e,function(){t.removeChildDomNodes();this.recalculateBottomSpace();if(i){this.pageScroller.scrollIntoView(this.getLastFrame())}}.bind(this))};i.prototype.getLastFrame=function(){var t=this.listWidget.children[this.listWidget.children.length-1];return t?t.findFirstDomNode():null};i.prototype.recalculateBottomSpace=function(){var t=this.pageScroller.storyRiverDomNode;if(this.getLastFrame()){var e=this.getLastFrame().getBoundingClientRect();var i=window.innerHeight;if(e.height=1){l.cancelScroll();t=1}t=$tw.utils.slowInSlowOut(t);window.scrollTo(i.x+(s-i.x)*t,i.y+(a-i.y)*t);if(t<1){l.idRequestFrame=l.requestAnimationFrame.call(window,c)}};c()}};i.prototype.startInsertAnimation=function(t,i){var o=$tw.utils.getAnimationDuration();var r=window.getComputedStyle(t),n=parseInt(r.marginBottom,10),s=parseInt(r.marginTop,10),a=t.offsetHeight+s;setTimeout(function(){$tw.utils.setStyle(t,[{transition:"none"},{marginBottom:""}]);i()},o);$tw.utils.setStyle(t,[{transition:"none"},{marginBottom:-a+"px"},{opacity:"0.0"}]);$tw.utils.forceLayout(t);$tw.utils.setStyle(t,[{transition:"opacity "+o+"ms "+e+", "+"margin-bottom "+o+"ms "+e},{marginBottom:n+"px"},{opacity:"1.0"}])};i.prototype.startRemoveAnimation=function(t,i,o){var r=$tw.utils.getAnimationDuration();var n=i.offsetWidth,s=window.getComputedStyle(i),a=parseInt(s.marginBottom,10),l=parseInt(s.marginTop,10),c=i.offsetHeight+l;setTimeout(o,r);$tw.utils.setStyle(i,[{transition:"none"},{transform:"translateX(0px)"},{marginBottom:a+"px"},{opacity:"1.0"}]);$tw.utils.forceLayout(i);$tw.utils.setStyle(i,[{transition:$tw.utils.roundTripPropertyName("transform")+" "+r+"ms "+e+", "+"opacity "+r+"ms "+e+", "+"margin-bottom "+r+"ms "+e},{transform:"translateX(-"+n+"px)"},{marginBottom:-c+"px"},{opacity:"0.0"}])};exports.top=i})(); -------------------------------------------------------------------------------- /src/gulpfile.js: -------------------------------------------------------------------------------- 1 | 2 | /******************************************************************** 3 | * Imports 4 | *******************************************************************/ 5 | 6 | var gulp = require("gulp"); 7 | var gutil = require("gulp-util"); 8 | var sass = require("gulp-sass"); 9 | var addsrc = require("gulp-add-src"); // https://github.com/gulpjs/gulp/issues/396 10 | var uglify = require("gulp-uglify"); 11 | var esprima = require('gulp-esprima'); 12 | var debug = require('gulp-debug'); 13 | var gulpif = require('gulp-if'); 14 | var argv = require('yargs').argv; 15 | var del = require("del"); // rm -rf 16 | //~ var jsdoc = require("gulp-jsdoc"); // not maintained! 17 | 18 | /******************************************************************** 19 | * Tasks 20 | *******************************************************************/ 21 | 22 | // clean will not be run twice, even though it is called as a dependency twice. 23 | // @see https://github.com/gulpjs/gulp/blob/master/docs/recipes/running-tasks-in-series.md 24 | gulp.task("clean", function (cb) { 25 | 26 | var options = { force: true }; 27 | del([ "../dist/"/*, "../docs/"*/ ], options, cb); 28 | 29 | }); 30 | 31 | gulp.task("compile", ["clean"], function () { 32 | 33 | // copy everything that doesn't need further processing 34 | gulp.src(["plugins/**", "!plugins/**/*.scss", "!plugins/**/*.js"]) 35 | .pipe(gulp.dest("../dist/")); 36 | 37 | // compile styles 38 | gulp.src("plugins/**/*.scss") 39 | .pipe(sass({ style: "expanded" })) 40 | .pipe(gulp.dest("../dist/")); 41 | 42 | // uglify js 43 | gulp.src("plugins/**/*.js") 44 | .pipe( 45 | gulpif(argv.production, 46 | uglify({ compress: false, preserveComments: "some" }))) 47 | .pipe(gulp.dest("../dist/")); 48 | 49 | }); 50 | 51 | gulp.task("create docs", ["clean"], function () { 52 | 53 | // ... 54 | 55 | }); 56 | 57 | gulp.task("default", ["compile", "create docs"]); -------------------------------------------------------------------------------- /src/node_modules: -------------------------------------------------------------------------------- 1 | /usr/local/lib/node_modules -------------------------------------------------------------------------------- /src/plugins/felixhayashi/topstoryview/config.js: -------------------------------------------------------------------------------- 1 | /*\ 2 | 3 | title: $:/plugins/felixhayashi/topstoryview/config.js 4 | type: application/javascript 5 | module-type: library 6 | 7 | @preserve 8 | 9 | \*/ 10 | 11 | (function(){ 12 | 13 | /*jslint node: true, browser: true */ 14 | /*global $tw: false */ 15 | "use strict"; 16 | 17 | /***************************** CODE ******************************/ 18 | 19 | exports.config = { 20 | 21 | // Essential tiddlywiki classes that we depend on 22 | classNames: { 23 | 24 | // contains all tiddlers and some other stuff 25 | storyRiver: "tc-story-river", 26 | 27 | // back drop element of the sr 28 | backDrop: "story-backdrop", 29 | 30 | // wraps a tiddler and contains the title 31 | tiddlerFrame: "tc-tiddler-frame", 32 | 33 | // elements with this class contain the title 34 | tiddlerTitle: "tc-title" 35 | 36 | }, 37 | 38 | references: { 39 | 40 | // User configuration 41 | userConfig: "$:/config/topStoryView", 42 | 43 | // This tiddler holds a reference to the currently focussed 44 | // tiddler. A tiddler is focussed if it was scrolled to 45 | // reach the top offset. 46 | focussedTiddlerStore: "$:/temp/focussedTiddler", 47 | 48 | // back drop element of the sr 49 | refreshTrigger: "$:/temp/focussedTiddler/refresh" 50 | 51 | }, 52 | 53 | // Time after a scroll event that has to elapse before we 54 | // check which tiddler is actually focussed. This is necessary 55 | // to avoid updates that only result from scroll animations. 56 | checkbackTime: $tw.utils.getAnimationDuration() 57 | 58 | }; 59 | 60 | })(); 61 | -------------------------------------------------------------------------------- /src/plugins/felixhayashi/topstoryview/files/layout.scss: -------------------------------------------------------------------------------- 1 | html .tc-story-river:after { 2 | content: ""; 3 | display: block; 4 | } -------------------------------------------------------------------------------- /src/plugins/felixhayashi/topstoryview/files/tiddlywiki.files: -------------------------------------------------------------------------------- 1 | { 2 | "tiddlers": [ 3 | { 4 | "file": "layout.css", "fields": { 5 | "title": "$:/plugins/felixhayashi/topstoryview/layout", 6 | "type": "text/vnd.tiddlywiki", 7 | "tags": [ "$:/tags/Stylesheet" ] 8 | }} 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/plugins/felixhayashi/topstoryview/plugin.info: -------------------------------------------------------------------------------- 1 | { 2 | "title": "$:/plugins/felixhayashi/topstoryview", 3 | "description": "TopStoryView – Always scroll navigated tiddlers to the top", 4 | "author": "Felix Küppers", 5 | "version": "0.2.0", 6 | "released": "", 7 | "core-version": ">=5.1.5", 8 | "source": "https://github.com/felixhayashi/TW5-TopStoryView", 9 | "type": "application/json", 10 | "plugin-type": "plugin", 11 | "list": "Configuration License Readme", 12 | "dependents": "" 13 | } -------------------------------------------------------------------------------- /src/plugins/felixhayashi/topstoryview/tiddlers/config.tid: -------------------------------------------------------------------------------- 1 | title: $:/plugins/felixhayashi/topstoryview/Configuration 2 | 3 | Please see the [[GitHub page|https://github.com/felixhayashi/TW5-TopStoryView]] for more information on the options. 4 | 5 | Save and reload the wiki to activate changes. 6 | 7 | 8 | 9 | 10 | 11 | 12 |
Scroll offset:<$edit-text tiddler="$:/config/topStoryView" field="scroll-offset" tag="input" default="150px" />
-------------------------------------------------------------------------------- /src/plugins/felixhayashi/topstoryview/tiddlers/license.tid: -------------------------------------------------------------------------------- 1 | title: $:/plugins/felixhayashi/topstoryview/License 2 | 3 | This code is released under the BSD license. For the exact terms visit: 4 | 5 | https://github.com/felixhayashi/TW5-TopStoryView/blob/master/LICENSE -------------------------------------------------------------------------------- /src/plugins/felixhayashi/topstoryview/tiddlers/readme.tid: -------------------------------------------------------------------------------- 1 | title: $:/plugins/felixhayashi/topstoryview/Readme 2 | 3 | Please visit the [[GitHub page|https://github.com/felixhayashi/TW5-TopStoryView]] for more information. -------------------------------------------------------------------------------- /src/plugins/felixhayashi/topstoryview/top_story_view.js: -------------------------------------------------------------------------------- 1 | /*\ 2 | title: $:/plugins/felixhayashi/topstoryview/top.js 3 | type: application/javascript 4 | module-type: storyview 5 | 6 | Views the story as a linear sequence 7 | 8 | @preserve 9 | 10 | \*/ 11 | (function(){ 12 | 13 | /*jslint node: true, browser: true */ 14 | /*global $tw: false */ 15 | "use strict"; 16 | 17 | /**************************** IMPORTS ****************************/ 18 | 19 | var config = require("$:/plugins/felixhayashi/topstoryview/config.js").config; 20 | 21 | /***************************** CODE ******************************/ 22 | 23 | var easing = "cubic-bezier(0.645, 0.045, 0.355, 1)"; // From http://easings.net/#easeInOutCubic 24 | 25 | 26 | var TopStoryView = function(listWidget) { 27 | 28 | this.listWidget = listWidget; 29 | 30 | this.pageScroller = new $tw.utils.PageScroller(); 31 | this.pageScroller.scrollIntoView = this.scrollIntoView; 32 | this.pageScroller.storyRiverDomNode = document.getElementsByClassName(config.classNames.storyRiver)[0]; 33 | 34 | // load user config 35 | var confRef = $tw.wiki.getTiddler(config.references.userConfig); 36 | var userConf = (confRef ? confRef.fields : {}); 37 | 38 | // add hooks 39 | $tw.hooks.addHook("th-opening-default-tiddlers-list", 40 | this.hookOpenDefaultTiddlers); 41 | 42 | var scrollOffset = parseInt(userConf["scroll-offset"]); 43 | this.pageScroller.scrollOffset = (isNaN(scrollOffset) ? 71 : scrollOffset); // px 44 | //~ console.log("tsv:", "scrollOffset", this.pageScroller.scrollOffset); 45 | 46 | this.recalculateBottomSpace(); 47 | 48 | }; 49 | 50 | /** 51 | * This function is called by the list widget that is associated with 52 | * the wiki's story view on every refresh before it started to refresh itself 53 | */ 54 | TopStoryView.prototype.refreshStart = function(changedTiddlers,changedAttributes) { 55 | // - 56 | }; 57 | 58 | /** 59 | * This function is called by the list widget that is associated with 60 | * the wiki's story view on every refresh after it refreshed itself 61 | */ 62 | TopStoryView.prototype.refreshEnd = function(changedTiddlers,changedAttributes) { 63 | // - 64 | }; 65 | 66 | /** 67 | * Hook invoked by TW5 before tm-home is executed. 68 | */ 69 | TopStoryView.prototype.hookOpenDefaultTiddlers = function(storyList) { 70 | 71 | return storyList; 72 | 73 | }; 74 | 75 | /** 76 | * This function is called by the list widget that is associated with 77 | * the wiki's story view everytime the history changes. In other words, 78 | * if the history list tiddler (i.e usually $:/HistoryList) is contained 79 | * in the changedTiddlers hashmap, navigateTo will be called. 80 | * 81 | * If a list item has been newly inserted into the list, the `insert()` 82 | * method is called first by the widget and `navigateTo()` will be called 83 | * subsequently. 84 | */ 85 | TopStoryView.prototype.navigateTo = function(historyInfo) { 86 | 87 | var listElIndex = this.listWidget.findListItem(0, historyInfo.title); 88 | 89 | if(listElIndex === undefined) return; 90 | 91 | var listItemWidget = this.listWidget.children[listElIndex]; 92 | var targetElement = listItemWidget.findFirstDomNode(); 93 | 94 | // when should this ever happen? Anyhow... 95 | if(!(targetElement instanceof Element)) return; 96 | 97 | this.pageScroller.scrollIntoView(targetElement); 98 | 99 | }; 100 | 101 | /** 102 | * Function is called when a list item is inserted into the list 103 | * widget that is associated with the story river. 104 | * 105 | * The insert function does not call `scrollIntoView()` – this 106 | * is done later when TW invokes `navigateTo()`! 107 | */ 108 | TopStoryView.prototype.insert = function(listItemWidget) { 109 | 110 | if(!listItemWidget) return; 111 | 112 | var targetElement = listItemWidget.findFirstDomNode(); 113 | if(!(targetElement instanceof Element)) return; 114 | 115 | this.startInsertAnimation(targetElement, function() { 116 | // already recalculate (even if not visible yet) so navigateTo will be possible 117 | this.recalculateBottomSpace(); 118 | }.bind(this)); 119 | 120 | }; 121 | 122 | /** 123 | * Function is called when an item is removed from the list widget 124 | * associated with the story river. At the time `remove()` is called, 125 | * the listItemWidget passed as parameter is still contained in 126 | * `this.storyList.children`. This means that `this.storyList.children.length` 127 | * will always (!) be greater 0. 128 | */ 129 | TopStoryView.prototype.remove = function(listItemWidget) { 130 | 131 | if(!listItemWidget) return; 132 | 133 | var targetElement = listItemWidget.findFirstDomNode(); 134 | if(!(targetElement instanceof Element)) { 135 | // when would this happen? anyhow... 136 | listItemWidget.removeChildDomNodes(); 137 | return; 138 | } 139 | 140 | // needs to be calculated before remove animation 141 | var isLast = (this.getLastFrame() === targetElement); 142 | 143 | this.startRemoveAnimation(listItemWidget, targetElement, function() { 144 | 145 | // since it is not visible anymore, we can already remove the frame 146 | listItemWidget.removeChildDomNodes(); 147 | 148 | // update state 149 | this.recalculateBottomSpace(); 150 | 151 | if(isLast) { 152 | // focus new last frame 153 | //~ console.log("tsv:", "lastframe", this.getLastFrame()); 154 | this.pageScroller.scrollIntoView(this.getLastFrame()); 155 | } 156 | 157 | }.bind(this)); 158 | 159 | }; 160 | 161 | /** 162 | * Returns the last tiddler frame or null if no tiddler is contained 163 | * in the river. 164 | */ 165 | TopStoryView.prototype.getLastFrame = function() { 166 | 167 | var lastItem = this.listWidget.children[this.listWidget.children.length-1]; 168 | return (lastItem ? lastItem.findFirstDomNode() : null); 169 | 170 | }; 171 | 172 | /** 173 | * Called after insert- or remove-animations finished when the last 174 | * tiddler changed. Recalculates the bottom space: In order to be 175 | * able to scroll the last frame to the top of the window, 176 | * we need to add a padding below. 177 | */ 178 | TopStoryView.prototype.recalculateBottomSpace = function() { 179 | 180 | var sr = this.pageScroller.storyRiverDomNode; 181 | 182 | if(this.getLastFrame()) { 183 | 184 | var rect = this.getLastFrame().getBoundingClientRect(); 185 | var windowHeight = window.innerHeight; 186 | 187 | if(rect.height < windowHeight) { 188 | 189 | // recalculate style 190 | sr.style["paddingBottom"] = (windowHeight - rect.height) + "px"; 191 | 192 | return; 193 | } 194 | 195 | } 196 | 197 | // in any other case 198 | sr.style["paddingBottom"] = ""; 199 | 200 | }; 201 | 202 | /** 203 | * Starts an animated scroll to bring the specified element to the top 204 | * of the window's viewport. 205 | */ 206 | TopStoryView.prototype.scrollIntoView = function(element) { 207 | 208 | if(this.preventNextScrollAttempt) { 209 | this.preventNextScrollAttempt = false; 210 | } 211 | 212 | if(!element) return; 213 | 214 | var duration = $tw.utils.getAnimationDuration(); 215 | // Now get ready to scroll the body 216 | this.cancelScroll(); 217 | this.startTime = Date.now(); 218 | var scrollPosition = $tw.utils.getScrollPosition(); 219 | 220 | // Get the client bounds of the element and adjust by the scroll position 221 | var clientBounds = element.getBoundingClientRect(), 222 | bounds = { 223 | left: clientBounds.left + scrollPosition.x, 224 | top: clientBounds.top + scrollPosition.y, 225 | width: clientBounds.width, 226 | height: clientBounds.height 227 | }; 228 | // We'll consider the horizontal and vertical scroll directions separately via this function 229 | var getEndPos = function(targetPos,targetSize,currentPos,currentSize) { 230 | // If the target is above/left of the current view, then scroll to it's top/left 231 | if(targetPos <= currentPos) { 232 | return targetPos; 233 | // If the target is smaller than the window and the scroll position is too far up, then scroll till the target is at the bottom of the window 234 | } else if(targetSize < currentSize && currentPos < (targetPos + targetSize - currentSize)) { 235 | return targetPos + targetSize - currentSize; 236 | // If the target is big, then just scroll to the top 237 | } else if(currentPos < targetPos) { 238 | return targetPos; 239 | // Otherwise, stay where we are 240 | } else { 241 | return currentPos; 242 | } 243 | }, 244 | endX = getEndPos(bounds.left,bounds.width,scrollPosition.x,window.innerWidth), 245 | endY = bounds.top - this.scrollOffset; 246 | // Only scroll if necessary 247 | if(endX !== scrollPosition.x || endY !== scrollPosition.y) { 248 | var self = this, 249 | drawFrame; 250 | drawFrame = function () { 251 | var t; 252 | if(duration <= 0) { 253 | t = 1; 254 | } else { 255 | t = ((Date.now()) - self.startTime) / duration; 256 | } 257 | if(t >= 1) { 258 | self.cancelScroll(); 259 | t = 1; 260 | } 261 | t = $tw.utils.slowInSlowOut(t); 262 | window.scrollTo(scrollPosition.x + (endX - scrollPosition.x) * t, 263 | scrollPosition.y + (endY - scrollPosition.y) * t); 264 | 265 | if(t < 1) { 266 | self.idRequestFrame = self.requestAnimationFrame.call(window,drawFrame); 267 | } 268 | }; 269 | drawFrame(); 270 | } 271 | }; 272 | 273 | /** 274 | * Animation for when a tiddler is inserted 275 | */ 276 | TopStoryView.prototype.startInsertAnimation = function(targetElement, callback) { 277 | 278 | var duration = $tw.utils.getAnimationDuration(); 279 | 280 | // Get the current height of the tiddler 281 | var computedStyle = window.getComputedStyle(targetElement), 282 | currMarginBottom = parseInt(computedStyle.marginBottom,10), 283 | currMarginTop = parseInt(computedStyle.marginTop,10), 284 | currHeight = targetElement.offsetHeight + currMarginTop; 285 | // Reset the margin once the transition is over 286 | setTimeout(function() { 287 | $tw.utils.setStyle(targetElement,[ 288 | {transition: "none"}, 289 | {marginBottom: ""} 290 | ]); 291 | callback(); 292 | },duration); 293 | // Set up the initial position of the element 294 | $tw.utils.setStyle(targetElement,[ 295 | {transition: "none"}, 296 | {marginBottom: (-currHeight) + "px"}, 297 | {opacity: "0.0"} 298 | ]); 299 | $tw.utils.forceLayout(targetElement); 300 | // Transition to the final position 301 | $tw.utils.setStyle(targetElement,[ 302 | {transition: "opacity " + duration + "ms " + easing + ", " + 303 | "margin-bottom " + duration + "ms " + easing}, 304 | {marginBottom: currMarginBottom + "px"}, 305 | {opacity: "1.0"} 306 | ]); 307 | }; 308 | 309 | /** 310 | * Animation for when a tiddler is removed 311 | */ 312 | TopStoryView.prototype.startRemoveAnimation = function(listItemWidget, targetElement, callback) { 313 | 314 | var duration = $tw.utils.getAnimationDuration(); 315 | 316 | // Get the current height of the tiddler 317 | var currWidth = targetElement.offsetWidth, 318 | computedStyle = window.getComputedStyle(targetElement), 319 | currMarginBottom = parseInt(computedStyle.marginBottom,10), 320 | currMarginTop = parseInt(computedStyle.marginTop,10), 321 | currHeight = targetElement.offsetHeight + currMarginTop; 322 | // Remove the dom nodes of the listItemWidget at the end of the transition 323 | setTimeout(callback,duration); 324 | // Animate the closure 325 | $tw.utils.setStyle(targetElement,[ 326 | {transition: "none"}, 327 | {transform: "translateX(0px)"}, 328 | {marginBottom: currMarginBottom + "px"}, 329 | {opacity: "1.0"} 330 | ]); 331 | $tw.utils.forceLayout(targetElement); 332 | $tw.utils.setStyle(targetElement,[ 333 | {transition: $tw.utils.roundTripPropertyName("transform") + " " + duration + "ms " + easing + ", " + 334 | "opacity " + duration + "ms " + easing + ", " + 335 | "margin-bottom " + duration + "ms " + easing}, 336 | {transform: "translateX(-" + currWidth + "px)"}, 337 | {marginBottom: (-currHeight) + "px"}, 338 | {opacity: "0.0"} 339 | ]); 340 | 341 | }; 342 | 343 | exports.top = TopStoryView; 344 | 345 | })(); --------------------------------------------------------------------------------