diff --git a/core/modules/widgets/scrollable.js b/core/modules/widgets/scrollable.js new file mode 100644 index 000000000..a1cb7a56a --- /dev/null +++ b/core/modules/widgets/scrollable.js @@ -0,0 +1,182 @@ +/*\ +title: $:/core/modules/widgets/scrollable.js +type: application/javascript +module-type: widget + +Scrollable widget + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +var Widget = require("$:/core/modules/widgets/widget.js").widget; + +var ScrollableWidget = function(parseTreeNode,options) { + this.initialise(parseTreeNode,options); + this.scaleFactor = 1; + this.addEventListeners([ + {type: "tw-scroll", handler: "handleScrollEvent"} + ]); + this.requestAnimationFrame = window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + function(callback) { + return window.setTimeout(callback, 1000/60); + }; + this.cancelAnimationFrame = window.cancelAnimationFrame || + window.webkitCancelAnimationFrame || + window.webkitCancelRequestAnimationFrame || + window.mozCancelAnimationFrame || + window.mozCancelRequestAnimationFrame || + function(id) { + window.clearTimeout(id); + }; +}; + +/* +Inherit from the base widget class +*/ +ScrollableWidget.prototype = new Widget(); + +ScrollableWidget.prototype.cancelScroll = function() { + if(this.idRequestFrame) { + this.cancelAnimationFrame.call(window,this.idRequestFrame); + this.idRequestFrame = null; + } +}; + +/* +Handle a scroll event +*/ +ScrollableWidget.prototype.handleScrollEvent = function(event) { + // Pass the scroll event through if our offsetsize is larger than our scrollsize + if(this.outerDomNode.scrollWidth <= this.outerDomNode.offsetWidth && this.outerDomNode.scrollHeight <= this.outerDomNode.offsetHeight && this.fallthrough === "yes") { + return true; + } + this.scrollIntoView(event.target); + return false; // Handled event +}; + +/* +Scroll an element into view +*/ +ScrollableWidget.prototype.scrollIntoView = function(element) { + var duration = $tw.utils.getAnimationDuration(); + this.cancelScroll(); + this.startTime = new Date(); + var scrollPosition = { + x: this.outerDomNode.scrollLeft, + y: this.outerDomNode.scrollTop + }; + // Get the client bounds of the element and adjust by the scroll position + var scrollableBounds = this.outerDomNode.getBoundingClientRect(), + clientTargetBounds = element.getBoundingClientRect(), + bounds = { + left: clientTargetBounds.left + scrollPosition.x - scrollableBounds.left, + top: clientTargetBounds.top + scrollPosition.y - scrollableBounds.top, + width: clientTargetBounds.width, + height: clientTargetBounds.height + }; + // We'll consider the horizontal and vertical scroll directions separately via this function + var getEndPos = function(targetPos,targetSize,currentPos,currentSize) { + // If the target is already visible then stay where we are + if(targetPos >= currentPos && (targetPos + targetSize) <= (currentPos + currentSize)) { + return currentPos; + // If the target is above/left of the current view, then scroll to its top/left + } else if(targetPos <= currentPos) { + return targetPos; + // 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 + } else if(targetSize < currentSize && currentPos < (targetPos + targetSize - currentSize)) { + return targetPos + targetSize - currentSize; + // If the target is big, then just scroll to the top + } else if(currentPos < targetPos) { + return targetPos; + // Otherwise, stay where we are + } else { + return currentPos; + } + }, + endX = getEndPos(bounds.left,bounds.width,scrollPosition.x,this.outerDomNode.offsetWidth), + endY = getEndPos(bounds.top,bounds.height,scrollPosition.y,this.outerDomNode.offsetHeight); + // Only scroll if necessary + if(endX !== scrollPosition.x || endY !== scrollPosition.y) { + var self = this, + drawFrame; + drawFrame = function () { + var t; + if(duration <= 0) { + t = 1; + } else { + t = ((new Date()) - self.startTime) / duration; + } + if(t >= 1) { + self.cancelScroll(); + t = 1; + } + t = $tw.utils.slowInSlowOut(t); + self.outerDomNode.scrollLeft = scrollPosition.x + (endX - scrollPosition.x) * t; + self.outerDomNode.scrollTop = scrollPosition.y + (endY - scrollPosition.y) * t; + if(t < 1) { + self.idRequestFrame = self.requestAnimationFrame.call(window,drawFrame); + } + }; + drawFrame(); + } +}; + +/* +Render this widget into the DOM +*/ +ScrollableWidget.prototype.render = function(parent,nextSibling) { + var self = this; + // Remember parent + this.parentDomNode = parent; + // Compute attributes and execute state + this.computeAttributes(); + this.execute(); + // Create elements + this.outerDomNode = this.document.createElement("div"); + $tw.utils.setStyle(this.outerDomNode,[ + {overflowY: "auto"}, + {overflowX: "auto"}, + {webkitOverflowScrolling: "touch"} + ]); + this.innerDomNode = this.document.createElement("div"); + this.outerDomNode.appendChild(this.innerDomNode); + // Assign classes + this.outerDomNode.className = this["class"] || ""; + // Insert element + parent.insertBefore(this.outerDomNode,nextSibling); + this.renderChildren(this.innerDomNode,null); + this.domNodes.push(this.outerDomNode); +}; + +/* +Compute the internal state of the widget +*/ +ScrollableWidget.prototype.execute = function() { + // Get attributes + this.fallthrough = this.getAttribute("fallthrough","yes"); + this["class"] = this.getAttribute("class"); + // Make child widgets + this.makeChildWidgets(); +}; + +/* +Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering +*/ +ScrollableWidget.prototype.refresh = function(changedTiddlers) { + var changedAttributes = this.computeAttributes(); + if(changedAttributes["class"]) { + this.refreshSelf(); + return true; + } + return this.refreshChildren(changedTiddlers); +}; + +exports.scrollable = ScrollableWidget; + +})(); diff --git a/core/ui/PageMacros.tid b/core/ui/PageMacros.tid index 294d8f5f8..849e67f6f 100644 --- a/core/ui/PageMacros.tid +++ b/core/ui/PageMacros.tid @@ -42,6 +42,16 @@ $$$text/vnd.tiddlywiki>text/html $src$ $$$ +\end +\define wikitext-example-without-html(src) +``` +$src$ +``` + +Renders as: + +$src$ + \end \define lingo-base() $:/lingo/ diff --git a/editions/tw5.com/tiddlers/system/tw5.com-styles.tid b/editions/tw5.com/tiddlers/system/tw5.com-styles.tid index de20b3355..cc993ff26 100644 --- a/editions/tw5.com/tiddlers/system/tw5.com-styles.tid +++ b/editions/tw5.com/tiddlers/system/tw5.com-styles.tid @@ -32,3 +32,10 @@ tags: $:/tags/stylesheet font-weight: 500; font-size: 16px; } + +.tw-scrollable-demo { + border: 1px solid <>; + background-color: <>; + padding: 1em; + height: 400px; +} diff --git a/editions/tw5.com/tiddlers/widgets/ScrollableWidget.tid b/editions/tw5.com/tiddlers/widgets/ScrollableWidget.tid new file mode 100644 index 000000000..eb1cde222 --- /dev/null +++ b/editions/tw5.com/tiddlers/widgets/ScrollableWidget.tid @@ -0,0 +1,46 @@ +created: 20140324223413403 +modified: 20140324223524945 +tags: widget +title: ScrollableWidget +type: text/vnd.tiddlywiki + +! Introduction + +The scrollable widget wraps its content in a scrollable frame. The user can scroll the contents with the mouse or with touch gestures. Code can use the [[WidgetMessage: tw-scroll]] to programmatically scroll specific DOM nodes into view. + +! Content and Attributes + +The content of the `<$scrollable>` widget is displayed within a pair of wrapper DIVs. If the inner DIV is larger then it scrolls within the outer one. CSS is used to specify the size of the outer wrapper. + +|!Attribute |!Description | +|class |The CSS class(es) to be applied to the outer DIV | +|fallthrough |See below | + +If a scrollable widget can't handle the `tw-scroll` message because the inner DIV fits within the outer DIV, then by default the message falls through to the parent widget. Setting the ''fallthrough'' atribute to `no` prevents this behaviour. + +! Examples + +This example requires the following CSS definitions from [[$:/_tw5.com-styles]]: + +``` +.tw-scrollable-demo { + border: 1px solid <>; + background-color: <>; + padding: 1em; + height: 400px; +} +``` + +This wiki text shows how to display a list within the scrollable widget: + +< +<$list filter='[!is[system]]'> + +<$view field='title'/>: <$list filter='[is[current]links[]sort[title]]' storyview='pop'> +<$link><$view field='title'/> + + + + +">> +