Create New Item
Item Type
File
Folder
Item Name
Search file in folder and subfolders...
Are you sure want to rename?
File Manager
/
portableapps
/
theme
/
default
:
themestrap.plugin.scrollshadow.js
Advanced Search
Upload
New Item
Settings
Back
Back Up
Advanced Editor
Save
/** * Themestrap ScrollShadow * * Edge shadows that show there is more to see. Two scopes: * * scope: 'element' (default) masks the edges of a scrollable container, as before. * scope: 'page' for content that scrolls with the page, such as a blog post. * Draws a fixed fade at the viewport edge (and an optional hint pill) * while the element is on screen and more of it lies beyond that edge, * and removes it once the element's end is in view. * * $('.post-body').themestrapPluginScrollShadow({ * scope: 'page', edges: ['bottom'], size: '96px', hint: 'Keep reading' * }); * * Page scope options: edges (['top', 'bottom']), fade (CSS colour of the page background, or set * --ts-ss-fade on :root), hint (text for a pill that scrolls down when clicked). * The element also receives .ts-ss--top / .ts-ss--bottom so CSS can react. */ // Scroll Shadow (((themestrap = {}, $) => { const instanceName = '__pluginScrollShadow'; // Injected stylesheet — runs once per page, keyed to the plugin stylesheet ID const STYLE_ID = 'ts-scroll-shadow-styles'; let instanceCount = 0; let seq = 0; // CSS_TEXT is injected lazily by injectStyles() on first build (see below), // so loading this script never adds CSS to pages that don't use the plugin. const CSS_TEXT = ` /* Themestrap — PluginScrollShadow */ .ts-scroll-shadow { --ts-ss-size: 40px; --ts-ss-color: black; /* mask fades toward transparent */ position: relative; overflow: auto; /* Mask compositing shorthand — overridden by state classes */ -webkit-mask-image: none; mask-image: none; -webkit-mask-size: 100% 100%; mask-size: 100% 100%; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat; } /* top edge only visible - fade bottom */ .ts-scroll-shadow[data-orientation="vertical"].ts-ss--bottom, .ts-scroll-shadow:not([data-orientation]).ts-ss--bottom { -webkit-mask-image: linear-gradient(to bottom, var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100% ); mask-image: linear-gradient(to bottom, var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100% ); } /* bottom edge only visible - fade top */ .ts-scroll-shadow[data-orientation="vertical"].ts-ss--top, .ts-scroll-shadow:not([data-orientation]).ts-ss--top { -webkit-mask-image: linear-gradient(to top, var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100% ); mask-image: linear-gradient(to top, var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100% ); } /* both top and bottom overflow - fade both ends */ .ts-scroll-shadow[data-orientation="vertical"].ts-ss--top.ts-ss--bottom, .ts-scroll-shadow:not([data-orientation]).ts-ss--top.ts-ss--bottom { -webkit-mask-image: linear-gradient(to bottom, transparent 0%, var(--ts-ss-color) var(--ts-ss-size), var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100% ); mask-image: linear-gradient(to bottom, transparent 0%, var(--ts-ss-color) var(--ts-ss-size), var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100% ); } .ts-scroll-shadow[data-orientation="horizontal"].ts-ss--right { -webkit-mask-image: linear-gradient(to right, var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100% ); mask-image: linear-gradient(to right, var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100% ); } .ts-scroll-shadow[data-orientation="horizontal"].ts-ss--left { -webkit-mask-image: linear-gradient(to left, var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100% ); mask-image: linear-gradient(to left, var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100% ); } .ts-scroll-shadow[data-orientation="horizontal"].ts-ss--left.ts-ss--right { -webkit-mask-image: linear-gradient(to right, transparent 0%, var(--ts-ss-color) var(--ts-ss-size), var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100% ); mask-image: linear-gradient(to right, transparent 0%, var(--ts-ss-color) var(--ts-ss-size), var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100% ); } /* Vertical faces only — reuse vertical rules above for :not([data-orientation]) */ .ts-scroll-shadow[data-orientation="both"].ts-ss--bottom:not(.ts-ss--top):not(.ts-ss--left):not(.ts-ss--right) { -webkit-mask-image: linear-gradient(to bottom, var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100% ); mask-image: linear-gradient(to bottom, var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100% ); } .ts-scroll-shadow[data-orientation="both"].ts-ss--top:not(.ts-ss--bottom):not(.ts-ss--left):not(.ts-ss--right) { -webkit-mask-image: linear-gradient(to top, var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100% ); mask-image: linear-gradient(to top, var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100% ); } .ts-scroll-shadow[data-orientation="both"].ts-ss--top.ts-ss--bottom:not(.ts-ss--left):not(.ts-ss--right) { -webkit-mask-image: linear-gradient(to bottom, transparent 0%, var(--ts-ss-color) var(--ts-ss-size), var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100% ); mask-image: linear-gradient(to bottom, transparent 0%, var(--ts-ss-color) var(--ts-ss-size), var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100% ); } /* When BOTH axes are in play — compose with a two-gradient mask */ .ts-scroll-shadow[data-orientation="both"].ts-ss--top.ts-ss--bottom.ts-ss--left.ts-ss--right { -webkit-mask-image: linear-gradient(to bottom, transparent 0%, var(--ts-ss-color) var(--ts-ss-size), var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100% ), linear-gradient(to right, transparent 0%, var(--ts-ss-color) var(--ts-ss-size), var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100% ); mask-image: linear-gradient(to bottom, transparent 0%, var(--ts-ss-color) var(--ts-ss-size), var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100% ), linear-gradient(to right, transparent 0%, var(--ts-ss-color) var(--ts-ss-size), var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100% ); -webkit-mask-composite: destination-in; mask-composite: intersect; -webkit-mask-size: 100% 100%, 100% 100%; mask-size: 100% 100%, 100% 100%; } /* Partial two-axis variants */ .ts-scroll-shadow[data-orientation="both"].ts-ss--bottom.ts-ss--left.ts-ss--right:not(.ts-ss--top) { -webkit-mask-image: linear-gradient(to bottom, var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100%), linear-gradient(to right, transparent 0%, var(--ts-ss-color) var(--ts-ss-size), var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100%); mask-image: linear-gradient(to bottom, var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100%), linear-gradient(to right, transparent 0%, var(--ts-ss-color) var(--ts-ss-size), var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100%); -webkit-mask-composite: destination-in; mask-composite: intersect; -webkit-mask-size: 100% 100%, 100% 100%; mask-size: 100% 100%, 100% 100%; } .ts-scroll-shadow[data-orientation="both"].ts-ss--top.ts-ss--left.ts-ss--right:not(.ts-ss--bottom) { -webkit-mask-image: linear-gradient(to top, var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100%), linear-gradient(to right, transparent 0%, var(--ts-ss-color) var(--ts-ss-size), var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100%); mask-image: linear-gradient(to top, var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100%), linear-gradient(to right, transparent 0%, var(--ts-ss-color) var(--ts-ss-size), var(--ts-ss-color) calc(100% - var(--ts-ss-size)), transparent 100%); -webkit-mask-composite: destination-in; mask-composite: intersect; -webkit-mask-size: 100% 100%, 100% 100%; mask-size: 100% 100%, 100% 100%; } /* Page scope: fixed fades at the viewport edge */ .ts-ss-page { position: fixed; left: 0; right: 0; height: var(--ts-ss-size, 40px); pointer-events: none; z-index: 40; opacity: 0; transition: opacity .25s ease; } .ts-ss-page.is-on { opacity: 1; } .ts-ss-page--bottom { bottom: 0; background: linear-gradient(to top, var(--ts-ss-fade, Canvas) 15%, transparent); } .ts-ss-page--top { top: 0; background: linear-gradient(to bottom, var(--ts-ss-fade, Canvas) 15%, transparent); } .ts-ss-hint { position: absolute; left: 50%; bottom: 14px; transform: translateX(-50%); pointer-events: auto; padding: 5px 14px; border: 0; border-radius: 999px; background: var(--ts-ss-hint-bg, rgba(20, 20, 20, .78)); color: var(--ts-ss-hint-fg, #fff); font: 600 12px/1.4 system-ui, sans-serif; letter-spacing: .02em; cursor: pointer; box-shadow: 0 2px 10px rgba(0, 0, 0, .25); } .ts-ss-page:not(.is-on) .ts-ss-hint { pointer-events: none; } @media (prefers-reduced-motion: reduce) { .ts-ss-page { transition: none; } } `; // Inject the stylesheet only when the plugin is actually used (called // from build()), so loading the script never adds CSS to unused pages. function injectStyles() { if (document.getElementById(STYLE_ID)) return; const style = document.createElement('style'); style.id = STYLE_ID; style.textContent = CSS_TEXT; (document.head || document.documentElement).appendChild(style); } class PluginScrollShadow { constructor($el, opts) { return this.initialize($el, opts); } initialize($el, opts) { if ($el.data(instanceName)) { return this; } this.$el = $el; this .setData() .setOptions(opts) .build() .events(); return this; } setData() { this.$el.data(instanceName, this); return this; } setOptions(opts) { this.options = $.extend(true, {}, PluginScrollShadow.defaults, opts, { wrapper: this.$el }); // Arrays are replaced, never merged index by index if (opts && Array.isArray(opts.edges)) this.options.edges = opts.edges.slice(); return this; } build() { // Inject component CSS lazily - only when an instance is built. injectStyles(); const self = this; const $el = self.$el; const options = self.options; instanceCount++; self._uid = ++seq; if (options.scope === 'page') { return self._buildPage(); } // Apply base class + orientation attribute $el.addClass('ts-scroll-shadow'); if (options.orientation) { $el.attr('data-orientation', options.orientation); } // Apply CSS custom properties from options if (options.size !== PluginScrollShadow.defaults.size) { $el.css('--ts-ss-size', options.size); } if (options.color !== PluginScrollShadow.defaults.color) { $el.css('--ts-ss-color', options.color); } if (!options.isEnabled) { return this; } // Build a ResizeObserver to recalculate when the container resizes if (typeof ResizeObserver !== 'undefined') { self._resizeObserver = new ResizeObserver(() => { self._updateShadows(); }); self._resizeObserver.observe($el[0]); } // Initial calculation self._updateShadows(); return this; } // Page scope: overlays on <body>, driven by window scroll and resize _buildPage() { const self = this; const o = self.options; const edges = Array.isArray(o.edges) && o.edges.length ? o.edges : ['top', 'bottom']; self._overlays = {}; edges.forEach((edge) => { if (edge !== 'top' && edge !== 'bottom') return; const el = document.createElement('div'); el.className = 'ts-ss-page ts-ss-page--' + edge; el.setAttribute('aria-hidden', 'true'); el.style.setProperty('--ts-ss-size', o.size); if (o.fade) el.style.setProperty('--ts-ss-fade', o.fade); if (edge === 'bottom' && o.hint) { const b = document.createElement('button'); b.type = 'button'; b.className = 'ts-ss-hint'; b.tabIndex = -1; b.textContent = o.hint; b.addEventListener('click', () => { const calm = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; window.scrollBy({ top: Math.round(window.innerHeight * 0.8), behavior: calm ? 'auto' : 'smooth' }); }); el.appendChild(b); } document.body.appendChild(el); self._overlays[edge] = el; }); self._raf = 0; self._onPageScroll = () => { if (self._raf) return; self._raf = requestAnimationFrame(() => { self._raf = 0; self._updateShadows(); }); }; window.addEventListener('scroll', self._onPageScroll, { passive: true }); window.addEventListener('resize', self._onPageScroll, { passive: true }); if (typeof ResizeObserver !== 'undefined') { self._resizeObserver = new ResizeObserver(self._onPageScroll); self._resizeObserver.observe(self.$el[0]); } self._updateShadows(); return self; } events() { const self = this; if (self.options.scope === 'page') return this; self.$el.on('scroll.scrollshadow', () => { self._updateShadows(); }); return this; } /** * Measures the element's scroll state and toggles the four shadow * edge classes accordingly. * * State classes added to the wrapper element: * .ts-ss--top — content above the visible area * .ts-ss--bottom — content below the visible area * .ts-ss--left — content to the left * .ts-ss--right — content to the right */ _updateShadows() { const self = this; const el = self.$el[0]; const options = self.options; const offset = options.offset; if (options.scope === 'page') { const r = el.getBoundingClientRect(); const vh = window.innerHeight || document.documentElement.clientHeight; const on = options.isEnabled; // Only while the element is actually on screen: it has more above, or more below const top = on && r.top < -offset && r.bottom > 0; const bottom = on && r.bottom > vh + offset && r.top < vh; self.$el.toggleClass('ts-ss--top', top).toggleClass('ts-ss--bottom', bottom); if (self._overlays.top) self._overlays.top.classList.toggle('is-on', top); if (self._overlays.bottom) self._overlays.bottom.classList.toggle('is-on', bottom); return; } if (!options.isEnabled) { self.$el.removeClass('ts-ss--top ts-ss--bottom ts-ss--left ts-ss--right'); return; } const scrollTop = el.scrollTop; const scrollLeft = el.scrollLeft; const scrollableY = el.scrollHeight - el.clientHeight; const scrollableX = el.scrollWidth - el.clientWidth; const orientation = options.orientation; const trackV = orientation === 'vertical' || orientation === 'both' || !orientation; const trackH = orientation === 'horizontal' || orientation === 'both'; self.$el .toggleClass('ts-ss--top', trackV && scrollTop > offset) .toggleClass('ts-ss--bottom', trackV && scrollTop < scrollableY - offset) .toggleClass('ts-ss--left', trackH && scrollLeft > offset) .toggleClass('ts-ss--right', trackH && scrollLeft < scrollableX - offset); } /** * Force a shadow recalculation — useful when the container's content * changes programmatically. */ update() { this._updateShadows(); return this; } /** * Enable or disable shadows without destroying the plugin instance. * @param {boolean} state */ setEnabled(state) { this.options.isEnabled = !!state; this._updateShadows(); return this; } destroy() { const self = this; if (self.options.scope === 'page') { window.removeEventListener('scroll', self._onPageScroll); window.removeEventListener('resize', self._onPageScroll); if (self._raf) cancelAnimationFrame(self._raf); Object.keys(self._overlays || {}).forEach((k) => self._overlays[k].remove()); self._overlays = {}; self.$el.removeClass('ts-ss--top ts-ss--bottom'); } self.$el.off('.scrollshadow'); if (self._resizeObserver) { self._resizeObserver.disconnect(); self._resizeObserver = null; } self.$el .removeClass('ts-scroll-shadow ts-ss--top ts-ss--bottom ts-ss--left ts-ss--right') .removeAttr('data-orientation') .css({ '--ts-ss-size': '', '--ts-ss-color': '' }) .removeData(instanceName); instanceCount--; if (instanceCount <= 0) { instanceCount = 0; const styleEl = document.getElementById(STYLE_ID); if (styleEl) styleEl.remove(); } return this; } } PluginScrollShadow.defaults = { /** * Scroll axis to track. * 'vertical' — top / bottom shadows (default) * 'horizontal' — left / right shadows * 'both' — all four edges */ orientation: 'vertical', /** * Shadow feather size. Maps to --ts-ss-size CSS custom property. * Accepts any CSS length string: '40px', '3rem', '10%', etc. */ size: '40px', /** * Mask color. The gradient fades from this value to transparent. * Use a named color or any valid CSS <color>. 'black' is the * correct value for mask-image use — do NOT pass rgba() here * unless you want a semi-transparent mask at the centre. * Maps to --ts-ss-color CSS custom property. */ color: 'black', /** * Pixel threshold before shadows appear. Adds a small "dead zone" * so a 1px rounding error doesn't falsely trigger a shadow. */ offset: 2, /** * Set to false to disable all shadow rendering while keeping the * plugin instance alive. Useful for toggling based on viewport size. */ isEnabled: true, /** * 'element' masks a scrollable container (default). * 'page' draws fixed fades at the viewport edge for content that scrolls with the page. */ scope: 'element', /** Page scope: which viewport edges get a fade. */ edges: ['top', 'bottom'], /** Page scope: colour of the page background the fade dissolves into. Defaults to --ts-ss-fade. */ fade: null, /** Page scope: text of a "keep reading" pill on the bottom fade. Empty for none. */ hint: '' }; $.extend(themestrap, { PluginScrollShadow }); $.fn.themestrapPluginScrollShadow = function(opts) { return this.map(function() { const $this = $(this); if ($this.data(instanceName)) { return $this.data(instanceName); } else { return new PluginScrollShadow($this, opts); } }); }; })).apply(this, [window.themestrap, jQuery]);