UNPKG

framer-controller

Version:

Control components and state in Framer X with reusable controllers.

296 lines (295 loc) 11.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const Controller_1 = require("./Controller"); const utils_1 = require("./utils"); class ScrollController extends Controller_1.Controller { constructor(options = {}) { super(Object.assign({ scrollX: 0, scrollY: 0, useMarkers: true, direction: { x: "none", y: "none", }, scrollPoint: { x: 0, y: 0, }, progress: { x: 0, y: 0, }, throttle: 16, markers: {}, onMove: point => this.handleScroll(point), onTapStart: () => this.pauseAnimation() }, options)); this._scrollPoint = { x: 0, y: 0, }; this.onConnect = (_, props) => { if (!props || !props.children) { console.error("Error: Connect this ScrollController to the props of a Scroll component (using an Override)."); return this.state; } const [component] = props.children; const [content] = component.props.children; if (!content) { console.error("Error: Your Scroll component must be connected to a content Frame."); return this.state; } this._scrollComponent = component; this._content = content; this._markersProps = this.getMarkersFromContent(); this.scrollPoint = { x: component.props.contentOffsetX || 0, y: component.props.contentOffsetY || 0, }; this.updateMarkers(); return this.state; }; /** Find a child with a given prop / value pair somewhere in its children */ this.getMarkersFromContent = () => { const { height: contentHeight, width: contentWidth } = this.content.props; const ids = []; const markers = []; const recursivelySearchForProp = (parent, component) => { const { props } = component; const { _overrideForwardingDescription: overrides } = props; if (props["markerId"]) { ids.push(props["markerId"]); markers.push(parent); } else if (overrides) { let key = Object.keys(overrides).find(k => overrides[k] === "markerId"); if (key) { ids.push(props.id); markers.push(component); } } if (Array.isArray(props.children)) { props.children.forEach(c => recursivelySearchForProp(component, c)); } return false; }; recursivelySearchForProp(this, this.content); return markers.reduce((acc, marker, index) => { const { centerX, centerY, height, width } = marker.props; const cx = parseFloat(centerX) / 100; const cy = parseFloat(centerY) / 100; let id = ids[index]; if (acc[id]) { console.warn(`Warning: Found markers with the same markerId value, ${id}! The second will overwrite the first.`); } acc[id] = { top: contentHeight * cy - height / 2, bottom: contentHeight * cy + height / 2, left: contentWidth * cx - width / 2, right: contentWidth * cx + width / 2, height, width, }; return acc; }, {}); }; this.updateMarkers = () => { const { _scrollPoint, _markersProps, connected } = this; const { x, y } = _scrollPoint; if (!connected) return; const { height: containerHeight, width: containerWidth } = connected; const { height: contentHeight, width: contentWidth } = this.content.props; let minX = 0, maxX = containerWidth, minY = 0, maxY = containerHeight; let markers = {}; for (let key in _markersProps) { const marker = _markersProps[key]; const { top, bottom, left, right, height, width } = marker; let intersect = { x: 0, y: 0, }; let visible = false; let progress = { x: 0, y: 0 }; let clip = { x: "", y: "" }; const absolute = { top, bottom, left, right }; const offset = { top: top + y, right: right + x, bottom: bottom + y, left: left + x, }; if (offset.top > maxY) { clip.y = "below"; } else if (offset.bottom < minY) { clip.y = "above"; } else if (offset.top > minY && offset.top < maxY && offset.bottom > maxY) { intersect.y = (maxY - offset.top) / height; clip.y = "clip-bottom"; visible = true; } else if (offset.top < minY && offset.bottom < maxY && offset.bottom > minY) { intersect.y = offset.bottom / height; clip.y = "clip-top"; visible = true; } else if (offset.top < minY && offset.bottom > maxY) { intersect.y = containerHeight / height; clip.y = "overflow"; visible = true; } else { intersect.y = 1; clip.y = "contain"; visible = true; } if (offset.left > maxX) { clip.x = "right"; } else if (offset.right < minX) { clip.x = "left"; } else if (offset.left > minX && offset.left < maxX && offset.right > maxX) { intersect.x = (maxX - offset.left) / width; clip.x = "clip-right"; visible = true; } else if (offset.left < minX && offset.right < maxX && offset.right > minX) { intersect.x = offset.right / width; clip.x = "clip-left"; visible = true; } else if (offset.left < minX && offset.right > maxX) { intersect.x = containerWidth / width; clip.x = "overflow"; visible = true; } else { intersect.x = 1; clip.x = "contain"; visible = true; } progress.y = utils_1.modulate(offset.top, [containerHeight, -height], [0, 1]); progress.x = utils_1.modulate(offset.left, [containerWidth, -width], [0, 1]); markers[key] = { intersect, absolute, offset, clip, visible, progress, }; } this._markerStates = markers; this.setState(Object.assign({ markers, progress: { x: x / -(contentWidth - containerWidth), y: y / -(contentHeight - containerHeight), }, direction: this._direction }, (this.isAnimating ? {} : { scrollX: -this._scrollPoint.x, scrollY: -this._scrollPoint.y }))); }; this.getMarker = (props) => { if (props.componentIdentifier) { if (!props.children) { console.warn(`Error: you must call getMarkers with the props provided by your component's override.`); return; } const [component] = props.children; const { markerId } = component.props; return this.markers[markerId]; } return this.markers[props.id]; }; this.handleScroll = (point) => { this.scrollPoint = point; }; this.scrollToPoint = (point, options = {}) => { if (this.animation) { this.animation.pause(); } point = Object.assign({}, this.scrollPoint, point); return this.animate(Object.assign({ scrollX: point.x, scrollY: point.y, duration: 1500 }, options)); }; this.scrollToMarker = (id, edge, offset = 0, options = {}) => { const marker = this.state.markers[id]; if (!marker) { console.warn(`Error: No marker found for ${id}. Available ids are: ${Object.keys(this.markers).join(", ")}.`); } if (this.animation) { this.animation.pause(); } if (!Array.isArray(edge)) { edge = [edge]; } const edgeX = edge.find(e => e === "left" || e === "right"); const edgeY = edge.find(e => e === "top" || e === "bottom"); let anim = {}; if (edgeX) { anim.scrollX = marker.absolute[edgeX] - offset; } if (edgeY) { anim.scrollY = marker.absolute[edgeY] - offset; } return this.animate(Object.assign({}, anim, { duration: 1500 }, options)); }; this.updateMarkers = utils_1.throttle(this.updateMarkers, this.state.throttle); this.updateMarkers(); } get scrollPoint() { return { x: this.state.scrollX, y: this.state.scrollY, }; } set scrollPoint(point) { this._direction = { y: point.y < this._scrollPoint.y ? "down" : point.y > this._scrollPoint.y ? "up" : "none", x: point.x < this._scrollPoint.x ? "right" : point.x > this._scrollPoint.x ? "left" : "none", }; this._scrollPoint = point; this.updateMarkers(); } get scrollY() { return this.state.scrollY; } set scrollY(scrollY) { this._scrollPoint.y = -scrollY; this.updateMarkers(); } get scrollX() { return this.state.scrollX; } set scrollX(scrollX) { this._scrollPoint.x = -scrollX; this.setState({ scrollX, }); } get contentOffset() { return { contentOffsetX: -this.scrollX, contentOffsetY: -this.scrollY, }; } get content() { return this._content; } get markers() { return this.state.markers; } get direction() { return this.state.direction; } get progress() { return this.state.progress; } } exports.ScrollController = ScrollController;