Skip to content

Commit

Permalink
Adds active="" attribute for setting initial active slide, and improv…
Browse files Browse the repository at this point in the history
…es activation response
  • Loading branch information
stephband committed May 15, 2022
1 parent dbfcadf commit 8dcc092
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 71 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"error",
"unix"
],
"lines-around-comment": "error",
"lines-around-comment": "off",
"lines-around-directive": "error",
"max-depth": "off",
"max-len": "off",
Expand Down
28 changes: 14 additions & 14 deletions modules/active.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ scroll-snap alignment.

import { px } from '../../dom/modules/parse-length.js';
import rect from '../../dom/modules/rect.js';
import { trigger } from '../../dom/modules/trigger.js';


function getPaddedBox(scroller) {
Expand Down Expand Up @@ -55,21 +54,23 @@ function scrollToTarget(scroller, target, behavior) {

export function scrollTo(scroller, target) {
scrollToTarget(scroller, target, 'smooth');
return target;
}

export function jumpTo(scroller, target) {
scroller.style.setProperty('scroll-behavior', 'auto', 'important');
scrollToTarget(scroller, target, 'auto');
scroller.style.setProperty('scroll-behavior', '');
return target;
}

function getActive(scroller, children) {
function getAligned(scroller, elements) {
const { leftPadding, rightPadding, centrePadding } = getPaddedBox(scroller);

let n = children.length;
let n = elements.length;
let slide;

while ((slide = children[--n])) {
while ((slide = elements[--n])) {
const slideRect = rect(slide);
if (!slideRect) { continue; }

Expand Down Expand Up @@ -105,14 +106,17 @@ function isGhost(slide) {
return !!slide.dataset.slideIndex;
}

/**
'slide-active'
Emitted by a slide when it is brought into scroll-snap alignment.
**/
export function getActive(data) {
const { scroller, elements, children } = data;
const aligned = getAligned(scroller, elements);
return isGhost(aligned) ?
children[aligned.dataset.slideIndex] :
aligned ;
}

export function updateActive(data) {
const { scroller, children, elements } = data;
const current = getActive(scroller, elements);
const current = getAligned(scroller, elements);
let active;

// If current is a loop ghost jump to the actual slide it references
Expand All @@ -125,9 +129,5 @@ export function updateActive(data) {
active = current;
}

if (active === data.active) { return; }
data.active = active;
if (active === undefined) { return; }
data.actives.push(active);
trigger('slide-active', active);
data.aaa.push(active);
}
94 changes: 62 additions & 32 deletions modules/lifecycle.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@

import equals from '../../fn/modules/equals.js';
import noop from '../../fn/modules/noop.js';
import nothing from '../../fn/modules/nothing.js';
import Stream from '../../fn/modules/stream.js';
import create from '../../dom/modules/create.js';
import events, { isPrimaryButton } from '../../dom/modules/events.js';
import gestures from '../../dom/modules/gestures.js';
import { px } from '../../dom/modules/parse-length.js';
import rect from '../../dom/modules/rect.js';
import scrollStreams from '../../dom/modules/scrolls.js';

import { $data } from './consts.js';
import { $data } from './consts.js';
import { scrollTo, jumpTo, updateActive } from './active.js';
import { processPointers } from './swipes.js';
import scrollends from './scrollends.js';


import { trigger } from '../../dom/modules/trigger.js';


function getWidth(scroller, slides, children) {
Expand Down Expand Up @@ -46,6 +48,11 @@ function isSlide(slide) {

/* Lifecycle */

/**
'slide-active'
Emitted by a slide when it is brought into scroll-snap alignment.
**/

export default {
construct: function(shadow) {
// Shadow DOM
Expand All @@ -56,19 +63,17 @@ export default {
// Add slots to shadow
shadow.append(scroller, controls);

// Create streams from things that happen to slide-show
// Stream to push load to
const load = Stream.of();

// In Chrome and FF initial `slotchange` event is always sent before
// load, but not so in Safari where either order may happen, at a guess
// due to some caching strategy. Here we sanitise order by making
// slotchanges stream dependent on load. There's little point in doing
// much before load anyway, as active slide is dependent on loaded
// stylesheet.
// load, but not so in Safari where either order may happen (at a guess
// due to some caching strategy). Here we sanitise order by making
// slotchanges stream always fire after load.
const slotchanges = Stream
.combine({
host: load,
elements: events({ type: 'slotchange', select: '[part="slides"]' }, slides)
elements: events('slotchange', slides)
.map((e) => data.elements = slides.assignedElements()),
})
.broadcast({ memory: true });
Expand All @@ -82,20 +87,29 @@ export default {
})
.broadcast({ memory: true });

// Buffer stream for pushing children to scroll to and activate
const activates = Stream.of(null);

// Buffer stream for pushing children to activate
const aaa = Stream.of();

// Broadcast stream for listen to changes to active
const actives = aaa
.filter((child) => (data.active !== child && trigger('slide-active', child)))
.map((child) => data.active = child)
.broadcast({ memory: true });

const focuses = events('focusin', this);
const resizes = events('resize', window);
const fullscreens = events('fullscreenchange', window);

const clicks = events('click', shadow)
.filter(isPrimaryButton)
.broadcast();

const swipes = gestures({ threshold: '0.25rem', device: 'mouse' }, shadow)
.filter(() => data.children.length > 1);

const scrolls = scrollStreams(scroller);
const focuses = events('focusin', this);
const resizes = events('resize', window);
const fullscreens = events('fullscreenchange', window);
const activates = Stream.of();
const actives = Stream.broadcast({ memory: true });

// Private data
const data = this[$data] = {
clickSuppressTime: -Infinity,
Expand All @@ -109,6 +123,7 @@ export default {
controls,
load,
activates,
aaa,
actives,
slotchanges,
mutations,
Expand All @@ -118,17 +133,39 @@ export default {
// Create a stream of width updates
Stream
.merge(slotchanges, resizes)
.each(() => updateWidth(data.scroller, data.slides, data.elements));
.each((e) => updateWidth(scroller, slides, data.elements));

// let count = 0;
// Wait for fist slotchange/load, then on mutation maintain active
// position, or on activation scroll to new child, then pipe the child
// to be activated
Stream
.combine({ changes: slotchanges, child: activates })
.each((state) => (data.loaded ?
scrollTo(data.scroller, state.child) :
jumpTo(data.scroller, state.child)
));
.combine({ children: mutations, child: activates })
.map((state) => {
// Is previous child not yet defined, or the new one the same as it?
if (!data.active || data.active === state.child) {
// This is a mutation, so jump to the active child if it is
// still there, or the first child if not
return jumpTo(scroller, state.children.includes(state.child) ?
state.child :
state.children[0]
);
}

// This is an activation, scroll to the new active child without
// a check for being in children, we may be activating a ghost
scrollTo(scroller, state.child);

slotchanges.each((state) => updateActive(data));
// If it is a ghost, activate the original not the ghost
return state.child.dataset.slideIndex ?
state.children[state.child.dataset.slideIndex] :
state.child ;
})
.pipe(aaa);

// Update active when scroll comes to rest, but not mid-gesture
scrollends(scroller)
.filter(() => !data.gesturing)
.each((stream) => updateActive(data));

// Prevent default on immediate clicks after a gesture, and don't let
// them out: this is a gesture not a click
Expand Down Expand Up @@ -158,13 +195,6 @@ export default {
}
});

// Update active when scroll comes to rest
scrolls
.each((stream) => stream
.each(noop)
.done(() => updateActive(data))
);

// Chrome behaves nicely when shifting focus between slides, Safari and
// FF not so much. Let's give them a helping hand at displaying the
// focused slide. Todo: FF not getting this.
Expand Down
17 changes: 12 additions & 5 deletions modules/properties.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,15 @@ import * as fullscreen from './fullscreen.js';
/* Properties */

export default {
/**
active="#id"
Sets the initially scroll-snapped slide. `#id` must be the id of a direct
child of the `<slide-show>`.
**/

/**
.active
Gets or sets the currently scroll-snapped child element.
Gets or sets the currently scroll-snapped slide.
```js
const activeSlide = slideshow.active;
Expand All @@ -41,10 +47,11 @@ export default {
// Accept an id
const child = typeof id === 'object' ?
id :
this.querySelector('#' + (/^\d/.test((id + '')[0]) ?
'\\3' + (id + '')[0] + ' ' + (id + '').slice(1) :
id)
) ;
/^\d/.test(id + '') ?
this.querySelector('#\\3' + (id + '')[0] + ' ' + (id + '').slice(1)) :
/^\#/.test(id + '') ?
this.querySelector(id) :
this.querySelector('#' + id) ;

if (!child) {
throw new Error('Cannot set active – not a child of slide-show');
Expand Down
84 changes: 84 additions & 0 deletions modules/scrollends.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@

// Much of this code has been purloined from targetable.js – do we need the
// hashchange tracking here? I have commented it

import Stream from '../../fn/modules/stream.js';
import Producer from '../../fn/modules/stream/producer.js';

const assign = Object.assign;

// Capture scroll events in capture phase, as scroll events from elements
// other than document do not bubble.
const captureOptions = {
capture: true,
passive: true
};

const config = {
minScrollEventInterval: 0.0375,
maxScrollEventInterval: 0.18
};

var trackingInterval = config.maxScrollEventInterval;

function adjustTrackingInterval(times) {
// Dynamically adjust maxScrollEventInterval to tighten it up,
// imposing a baseline of 60ms (0.0375s * 1.6)

let n = times.length;
let interval = 0;

while (--n) {
const t = times[n] - times[n - 1];
interval = t > interval ? t : interval;
}

interval = interval < config.minScrollEventInterval ?
config.minScrollEventInterval :
interval ;

trackingInterval = (1.4 * interval) > config.maxScrollEventInterval ?
config.maxScrollEventInterval :
(1.4 * interval) ;
}

function fire(producer, e) {
producer.timer = undefined;
producer.stream.push(e);

const times = producer.times;
if (times.length > 1) { adjustTrackingInterval(times); }
times.length = 0;
}

function ScrollendsProducer(element) {
this.element = element;
this.times = [];
}

assign(ScrollendsProducer.prototype, Producer.prototype, {
pipe: function(stream) {
this.stream = stream;
this.element.addEventListener('scroll', this, captureOptions);
},

handleEvent: function(e) {
const time = e.timeStamp / 1000;
this.times.push(time);

if (this.timer) {
clearTimeout(this.timer);
}

this.timer = setTimeout(fire, trackingInterval * 1000, this, e);
},

stop: function() {
this.element.removeEventListener('scroll', this);
Producer.prototype.stop.apply(this, arguments);
}
});

export default function scrollends(element) {
return new Stream(new ScrollendsProducer(element));
}
8 changes: 6 additions & 2 deletions modules/swipes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import events from '../../dom/modules/events.js';
import overload from '../../fn/modules/overload.js';

import { updateActive } from './active.js';
import { getActive } from './active.js';

export const processPointers = overload((data, e) => e.type, {
pointerdown: function(data, e) {
Expand Down Expand Up @@ -68,7 +68,11 @@ export const processPointers = overload((data, e) => e.type, {
// We may as well preemptively update the active slide now, since we
// are sitting in the new position. This is not a crucial step, just
// makes the UI react a bit more quickly.
updateActive(data);
// Dont, actually, if there are ghosts this causes some jumping
// around.
//updateActive(data);
const active = getActive(data);
data.aaa.push(active);

// Otherwise we have to do things the hard way. Switch scroll-snap
// off again and put scroll back to position 1, ...
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"title": "slide-show",
"version": "1.0.0",
"version": "1.1.0",
"description": "The accessible, scrollable, styleable, horizontal carousel custom element. No dependencies, about 12kB minified and gzipped.",
"author": {
"name": "Stephen Band",
Expand Down
Loading

0 comments on commit 8dcc092

Please sign in to comment.