') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); GitHub - alexfilimon/PluginableFlowLayout: Reusable layout with alignment. Could be extended with plugins. · GitHub
Skip to content

Repository files navigation

PluginableFlowLayout

Reusable layout with alignment. Could be extended with plugins.

Example of layout

How to use PluginableFlowLayout

  1. When creating collection layout use PluginableFlowLayout
  2. Setup base UICollectionViewFlowLayout with your needs
    • properly setup scrollDirection
    • other settings
  3. If needed custom alignment, modify UICollectionView - turn off isPagingEnabled (default value is false) and set decelerationRate to .fast.

PluginableFlowLayout

Layout descendant of UICollectionViewFlowLayout.

Has feature for alignment:

  • lineStart - left alignment
  • lineCenter - center alignment
  • lineEnd - right alignment

If pass nil - there will not be any modifications for targetContentOffset (you can use default isPagingEnabled = true property for items with equal sizes).

Supports items with different sizes.

FlowLayoutPlugin

Protocol for custom plugins

PercentageFlowLayoutPlugin

Realization of FlowLayoutPlugin protocol for adding visibility percentage of items.

Example of horizontal layout:

Horizontal example

Example of vertical layout:

Vertical example

Different percentage values

Each percentage value contains number in range [-1; 1] where

  • value -1 means that cell is in before position (left position respectfull to center cell)
  • value 0 means that cell is in center position (in case of alignment not in center - PercentageFlowLayoutPlugin respect alignment from PluginableFlowLayout)
  • value +1 means that cell is in after position (right position respectfull to center cell)

Percentage info contains 3 values:

  • collectionVisibility - visibility factor respectfull to bound of collectionView - intersection frame of cell in collectionView
  • idealFrameVisibility - visibility factor respectfull to "ideal frame" (respecfull to alignment PluginableFlowLayout) - intersection frame of cell and "ideal frame"
  • idealFrameAndCollectionVisibility - mixed visibility factor

Note: if cell has width of collectionView and layout has horizontal scrollDirection (height/vertical) - all values will be equal.

How to use PercentageFlowLayoutPlugin

  • pass PercentageFlowLayoutPlugin in constructor of PluginableFlowLayout
letcollectionView=UICollectionView(
frame:.zero,
collectionViewLayout:PluginableFlowLayout(
alignment:.lineStart,
plugins:[PercentageFlowLayoutPlugin()]))
  • override method apply(_ layoutAttributes: UICollectionViewLayoutAttributes) and use custom attribute values
overridefunc apply(_ layoutAttributes:UICollectionViewLayoutAttributes){
// convert `UICollectionViewLayoutAttributes` to `PluginableFlowLayoutAttributes`
guardlet pluginableAttributes = layoutAttributes as?PluginableFlowLayoutAttributeselse{return super.apply(layoutAttributes)}
// get custom percentage attribute value
letpercentage= pluginableAttributes.percentageInfo.collectionVisibility ??0
// set UIImageView translation
lettranslationX=-percentage /2* imageView.bounds.width
imageView.transform =.init(
translationX: translationX,
y:0)
// set alpha for dimm view
dimmView.alpha =1- abs(percentage *2)}

Custom FlowLayoutPlugin

To add custom plugin, you need to realize protocol FlowLayoutPlugin and return modified PluginableFlowLayoutAttributes.

PluginableFlowLayoutAttributes

Kind of sttributes, that store values in dictionary. You can write extension to these class and realize setter/getter for values (like in file PercentageFlowLayoutPlugin.swift).

About

Reusable layout with alignment. Could be extended with plugins.

Resources

Stars

7 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages