svelte-lazy
Advanced tools
Comparing version 0.1.2 to 0.1.3
475
index.js
@@ -8,2 +8,3 @@ (function (global, factory) { | ||
function noop() { } | ||
const identity = x => x; | ||
function assign(tar, src) { | ||
@@ -46,2 +47,41 @@ // @ts-ignore | ||
} | ||
const is_client = typeof window !== 'undefined'; | ||
let now = is_client | ||
? () => window.performance.now() | ||
: () => Date.now(); | ||
let raf = cb => requestAnimationFrame(cb); | ||
const tasks = new Set(); | ||
let running = false; | ||
function run_tasks() { | ||
tasks.forEach(task => { | ||
if (!task[0](now())) { | ||
tasks.delete(task); | ||
task[1](); | ||
} | ||
}); | ||
running = tasks.size > 0; | ||
if (running) | ||
raf(run_tasks); | ||
} | ||
function loop(fn) { | ||
let task; | ||
if (!running) { | ||
running = true; | ||
raf(run_tasks); | ||
} | ||
return { | ||
promise: new Promise(fulfil => { | ||
tasks.add(task = [fn, fulfil]); | ||
}), | ||
abort() { | ||
tasks.delete(task); | ||
} | ||
}; | ||
} | ||
function append(target, node) { | ||
target.appendChild(node); | ||
} | ||
function insert(target, node, anchor) { | ||
@@ -59,6 +99,70 @@ target.insertBefore(node, anchor || null); | ||
} | ||
function empty() { | ||
return text(''); | ||
} | ||
function children(element) { | ||
return Array.from(element.childNodes); | ||
} | ||
function custom_event(type, detail) { | ||
const e = document.createEvent('CustomEvent'); | ||
e.initCustomEvent(type, false, false, detail); | ||
return e; | ||
} | ||
let stylesheet; | ||
let active = 0; | ||
let current_rules = {}; | ||
// https://github.com/darkskyapp/string-hash/blob/master/index.js | ||
function hash(str) { | ||
let hash = 5381; | ||
let i = str.length; | ||
while (i--) | ||
hash = ((hash << 5) - hash) ^ str.charCodeAt(i); | ||
return hash >>> 0; | ||
} | ||
function create_rule(node, a, b, duration, delay, ease, fn, uid = 0) { | ||
const step = 16.666 / duration; | ||
let keyframes = '{\n'; | ||
for (let p = 0; p <= 1; p += step) { | ||
const t = a + (b - a) * ease(p); | ||
keyframes += p * 100 + `%{${fn(t, 1 - t)}}\n`; | ||
} | ||
const rule = keyframes + `100% {${fn(b, 1 - b)}}\n}`; | ||
const name = `__svelte_${hash(rule)}_${uid}`; | ||
if (!current_rules[name]) { | ||
if (!stylesheet) { | ||
const style = element('style'); | ||
document.head.appendChild(style); | ||
stylesheet = style.sheet; | ||
} | ||
current_rules[name] = true; | ||
stylesheet.insertRule(`@keyframes ${name} ${rule}`, stylesheet.cssRules.length); | ||
} | ||
const animation = node.style.animation || ''; | ||
node.style.animation = `${animation ? `${animation}, ` : ``}${name} ${duration}ms linear ${delay}ms 1 both`; | ||
active += 1; | ||
return name; | ||
} | ||
function delete_rule(node, name) { | ||
node.style.animation = (node.style.animation || '') | ||
.split(', ') | ||
.filter(name | ||
? anim => anim.indexOf(name) < 0 // remove specific animation | ||
: anim => anim.indexOf('__svelte') === -1 // remove all Svelte animations | ||
) | ||
.join(', '); | ||
if (name && !--active) | ||
clear_rules(); | ||
} | ||
function clear_rules() { | ||
raf(() => { | ||
if (active) | ||
return; | ||
let i = stylesheet.cssRules.length; | ||
while (i--) | ||
stylesheet.deleteRule(i); | ||
current_rules = {}; | ||
}); | ||
} | ||
let current_component; | ||
@@ -123,2 +227,16 @@ function set_current_component(component) { | ||
} | ||
let promise; | ||
function wait() { | ||
if (!promise) { | ||
promise = Promise.resolve(); | ||
promise.then(() => { | ||
promise = null; | ||
}); | ||
} | ||
return promise; | ||
} | ||
function dispatch(node, direction, kind) { | ||
node.dispatchEvent(custom_event(`${direction ? 'intro' : 'outro'}${kind}`)); | ||
} | ||
const outroing = new Set(); | ||
@@ -161,2 +279,107 @@ let outros; | ||
} | ||
function create_bidirectional_transition(node, fn, params, intro) { | ||
let config = fn(node, params); | ||
let t = intro ? 0 : 1; | ||
let running_program = null; | ||
let pending_program = null; | ||
let animation_name = null; | ||
function clear_animation() { | ||
if (animation_name) | ||
delete_rule(node, animation_name); | ||
} | ||
function init(program, duration) { | ||
const d = program.b - t; | ||
duration *= Math.abs(d); | ||
return { | ||
a: t, | ||
b: program.b, | ||
d, | ||
duration, | ||
start: program.start, | ||
end: program.start + duration, | ||
group: program.group | ||
}; | ||
} | ||
function go(b) { | ||
const { delay = 0, duration = 300, easing = identity, tick = noop, css } = config; | ||
const program = { | ||
start: now() + delay, | ||
b | ||
}; | ||
if (!b) { | ||
// @ts-ignore todo: improve typings | ||
program.group = outros; | ||
outros.r += 1; | ||
} | ||
if (running_program) { | ||
pending_program = program; | ||
} | ||
else { | ||
// if this is an intro, and there's a delay, we need to do | ||
// an initial tick and/or apply CSS animation immediately | ||
if (css) { | ||
clear_animation(); | ||
animation_name = create_rule(node, t, b, duration, delay, easing, css); | ||
} | ||
if (b) | ||
tick(0, 1); | ||
running_program = init(program, duration); | ||
add_render_callback(() => dispatch(node, b, 'start')); | ||
loop(now => { | ||
if (pending_program && now > pending_program.start) { | ||
running_program = init(pending_program, duration); | ||
pending_program = null; | ||
dispatch(node, running_program.b, 'start'); | ||
if (css) { | ||
clear_animation(); | ||
animation_name = create_rule(node, t, running_program.b, running_program.duration, 0, easing, config.css); | ||
} | ||
} | ||
if (running_program) { | ||
if (now >= running_program.end) { | ||
tick(t = running_program.b, 1 - t); | ||
dispatch(node, running_program.b, 'end'); | ||
if (!pending_program) { | ||
// we're done | ||
if (running_program.b) { | ||
// intro — we can tidy up immediately | ||
clear_animation(); | ||
} | ||
else { | ||
// outro — needs to be coordinated | ||
if (!--running_program.group.r) | ||
run_all(running_program.group.c); | ||
} | ||
} | ||
running_program = null; | ||
} | ||
else if (now >= running_program.start) { | ||
const p = now - running_program.start; | ||
t = running_program.a + running_program.d * easing(p / running_program.duration); | ||
tick(t, 1 - t); | ||
} | ||
} | ||
return !!(running_program || pending_program); | ||
}); | ||
} | ||
} | ||
return { | ||
run(b) { | ||
if (is_function(config)) { | ||
wait().then(() => { | ||
// @ts-ignore | ||
config = config(); | ||
go(b); | ||
}); | ||
} | ||
else { | ||
go(b); | ||
} | ||
}, | ||
end() { | ||
clear_animation(); | ||
running_program = pending_program = null; | ||
} | ||
}; | ||
} | ||
function mount_component(component, target, anchor) { | ||
@@ -270,2 +493,11 @@ const { fragment, on_mount, on_destroy, after_update } = component.$$; | ||
function fade(node, { delay = 0, duration = 400 }) { | ||
const o = +getComputedStyle(node).opacity; | ||
return { | ||
delay, | ||
duration, | ||
css: t => `opacity: ${t * o}` | ||
}; | ||
} | ||
/* src\index.svelte generated by Svelte v3.6.10 */ | ||
@@ -275,4 +507,78 @@ | ||
function create_if_block(ctx) { | ||
var t, current; | ||
var current_block_type_index, if_block, if_block_anchor, current; | ||
var if_block_creators = [ | ||
create_if_block_1, | ||
create_else_block | ||
]; | ||
var if_blocks = []; | ||
function select_block_type(ctx) { | ||
if (ctx.fadeOption) return 0; | ||
return 1; | ||
} | ||
current_block_type_index = select_block_type(ctx); | ||
if_block = if_blocks[current_block_type_index] = if_block_creators[current_block_type_index](ctx); | ||
return { | ||
c() { | ||
if_block.c(); | ||
if_block_anchor = empty(); | ||
}, | ||
m(target, anchor) { | ||
if_blocks[current_block_type_index].m(target, anchor); | ||
insert(target, if_block_anchor, anchor); | ||
current = true; | ||
}, | ||
p(changed, ctx) { | ||
var previous_block_index = current_block_type_index; | ||
current_block_type_index = select_block_type(ctx); | ||
if (current_block_type_index === previous_block_index) { | ||
if_blocks[current_block_type_index].p(changed, ctx); | ||
} else { | ||
group_outros(); | ||
transition_out(if_blocks[previous_block_index], 1, 1, () => { | ||
if_blocks[previous_block_index] = null; | ||
}); | ||
check_outros(); | ||
if_block = if_blocks[current_block_type_index]; | ||
if (!if_block) { | ||
if_block = if_blocks[current_block_type_index] = if_block_creators[current_block_type_index](ctx); | ||
if_block.c(); | ||
} | ||
transition_in(if_block, 1); | ||
if_block.m(if_block_anchor.parentNode, if_block_anchor); | ||
} | ||
}, | ||
i(local) { | ||
if (current) return; | ||
transition_in(if_block); | ||
current = true; | ||
}, | ||
o(local) { | ||
transition_out(if_block); | ||
current = false; | ||
}, | ||
d(detaching) { | ||
if_blocks[current_block_type_index].d(detaching); | ||
if (detaching) { | ||
detach(if_block_anchor); | ||
} | ||
} | ||
}; | ||
} | ||
// (9:4) {:else } | ||
function create_else_block(ctx) { | ||
var div, t, current; | ||
const default_slot_1 = ctx.$$slots.default; | ||
@@ -283,2 +589,4 @@ const default_slot = create_slot(default_slot_1, ctx, null); | ||
c() { | ||
div = element("div"); | ||
if (!default_slot) { | ||
@@ -292,12 +600,14 @@ t = text("Lazy load content"); | ||
l(nodes) { | ||
if (default_slot) default_slot.l(nodes); | ||
if (default_slot) default_slot.l(div_nodes); | ||
}, | ||
m(target, anchor) { | ||
insert(target, div, anchor); | ||
if (!default_slot) { | ||
insert(target, t, anchor); | ||
append(div, t); | ||
} | ||
else { | ||
default_slot.m(target, anchor); | ||
default_slot.m(div, null); | ||
} | ||
@@ -326,9 +636,84 @@ | ||
d(detaching) { | ||
if (detaching) { | ||
detach(div); | ||
} | ||
if (default_slot) default_slot.d(detaching); | ||
} | ||
}; | ||
} | ||
// (3:4) {#if fadeOption} | ||
function create_if_block_1(ctx) { | ||
var div, t, div_transition, current; | ||
const default_slot_1 = ctx.$$slots.default; | ||
const default_slot = create_slot(default_slot_1, ctx, null); | ||
return { | ||
c() { | ||
div = element("div"); | ||
if (!default_slot) { | ||
if (detaching) { | ||
detach(t); | ||
} | ||
t = text("Lazy load content"); | ||
} | ||
if (default_slot) default_slot.c(); | ||
}, | ||
l(nodes) { | ||
if (default_slot) default_slot.l(div_nodes); | ||
}, | ||
m(target, anchor) { | ||
insert(target, div, anchor); | ||
if (!default_slot) { | ||
append(div, t); | ||
} | ||
else { | ||
default_slot.m(div, null); | ||
} | ||
current = true; | ||
}, | ||
p(changed, ctx) { | ||
if (default_slot && default_slot.p && changed.$$scope) { | ||
default_slot.p(get_slot_changes(default_slot_1, ctx, changed, null), get_slot_context(default_slot_1, ctx, null)); | ||
} | ||
}, | ||
i(local) { | ||
if (current) return; | ||
transition_in(default_slot, local); | ||
add_render_callback(() => { | ||
if (!div_transition) div_transition = create_bidirectional_transition(div, fade, ctx.fadeOption, true); | ||
div_transition.run(1); | ||
}); | ||
current = true; | ||
}, | ||
o(local) { | ||
transition_out(default_slot, local); | ||
if (!div_transition) div_transition = create_bidirectional_transition(div, fade, ctx.fadeOption, false); | ||
div_transition.run(0); | ||
current = false; | ||
}, | ||
d(detaching) { | ||
if (detaching) { | ||
detach(div); | ||
} | ||
if (default_slot) default_slot.d(detaching); | ||
if (detaching) { | ||
if (div_transition) div_transition.end(); | ||
} | ||
} | ||
@@ -398,11 +783,53 @@ }; | ||
function throttle(func, wait, options) { | ||
let context, args, result; | ||
let timeout = null; | ||
let previous = 0; | ||
if (!options) options = {}; | ||
const later = function() { | ||
previous = options.leading === false ? 0 : new Date(); | ||
timeout = null; | ||
result = func.apply(context, args); | ||
if (!timeout) context = args = null; | ||
}; | ||
return function(event) { | ||
const now = new Date(); | ||
if (!previous && options.leading === false) previous = now; | ||
const remaining = wait - (now - previous); | ||
context = this; | ||
args = [event.target.value]; | ||
if (remaining <= 0 || remaining > wait) { | ||
if (timeout) { | ||
clearTimeout(timeout); | ||
timeout = null; | ||
} | ||
previous = now; | ||
result = func.apply(context, args); | ||
if (!timeout) context = args = null; | ||
} else if (!timeout && options.trailing !== false) { | ||
timeout = setTimeout(later, remaining); | ||
} | ||
return result; | ||
}; | ||
} | ||
function instance($$self, $$props, $$invalidate) { | ||
let { loaded = false, height = 0, offset = 150 } = $$props; | ||
let { height = 0, offset = 150, fadeOption = { | ||
delay: 0, | ||
duration: 400, | ||
}, onload = null } = $$props; | ||
let loaded = false; | ||
function load(node) { | ||
if (height) { | ||
node.style.height = height + 'px'; | ||
if (typeof height === 'number') { | ||
node.style.height = height + 'px'; | ||
} else { | ||
node.style.height = height; | ||
} | ||
} | ||
const loadHandler = () => { | ||
const loadHandler = throttle(() => { | ||
const top = node.getBoundingClientRect().top; | ||
@@ -414,21 +841,31 @@ const expectedTop = window.innerHeight + offset; | ||
node.style.height = ''; | ||
document.removeEventListener('scroll', loadHandler); | ||
onload && onload(); | ||
removeListeners(); | ||
} | ||
}; | ||
}, 200); | ||
addListeners(); | ||
document.addEventListener('scroll', loadHandler); | ||
function addListeners() { | ||
document.addEventListener('scroll', loadHandler); | ||
window.addEventListener('resize', loadHandler); | ||
} | ||
function removeListeners() { | ||
document.removeEventListener('scroll', loadHandler); | ||
window.removeEventListener('resize', loadHandler); | ||
} | ||
return { | ||
destroy: () => { | ||
document.removeEventListener('scroll', loadHandler); | ||
removeListeners(); | ||
}, | ||
}; | ||
} | ||
let { $$slots = {}, $$scope } = $$props; | ||
$$self.$set = $$props => { | ||
if ('loaded' in $$props) $$invalidate('loaded', loaded = $$props.loaded); | ||
if ('height' in $$props) $$invalidate('height', height = $$props.height); | ||
if ('offset' in $$props) $$invalidate('offset', offset = $$props.offset); | ||
if ('fadeOption' in $$props) $$invalidate('fadeOption', fadeOption = $$props.fadeOption); | ||
if ('onload' in $$props) $$invalidate('onload', onload = $$props.onload); | ||
if ('$$scope' in $$props) $$invalidate('$$scope', $$scope = $$props.$$scope); | ||
@@ -438,5 +875,7 @@ }; | ||
return { | ||
loaded, | ||
height, | ||
offset, | ||
fadeOption, | ||
onload, | ||
loaded, | ||
load, | ||
@@ -451,3 +890,3 @@ $$slots, | ||
super(); | ||
init(this, options, instance, create_fragment, safe_not_equal, ["loaded", "height", "offset"]); | ||
init(this, options, instance, create_fragment, safe_not_equal, ["height", "offset", "fadeOption", "onload"]); | ||
} | ||
@@ -454,0 +893,0 @@ } |
@@ -6,3 +6,3 @@ { | ||
"main": "index.js", | ||
"version": "0.1.2", | ||
"version": "0.1.3", | ||
"scripts": { | ||
@@ -9,0 +9,0 @@ "build": "rollup -c", |
# svelte-lazy | ||
A svelte component to lazyload any content including images. | ||
## Install | ||
@@ -10,4 +12,6 @@ | ||
```html | ||
import Lazy from 'svelte-lazy'; | ||
<Lazy height={500} offset={150}> | ||
<img alt="" src={imgSrc} /> | ||
<img alt="" src="https://picsum.photos/id/412/200/300" /> | ||
</Lazy> | ||
@@ -20,5 +24,8 @@ ``` | ||
| Property | Description | Type | Default Value | | ||
|----------|------------------------------------------------------------------------------------------------------|--------|---------------| | ||
| height | Height of the placeholder before the component is loaded. Set a proper value to avoid scroll bounce. | number | 0 | | ||
| offset | Offset to the bottom of viewport that triggers loading when the component is in. | number | 150 | | ||
- `height: Number/String` Default: `0`. Height of the placeholder before the component is loaded. Set a proper value to avoid scroll bounce. | ||
- `offset: Number` Default: `150`. Offset to the bottom of viewport that triggers loading when the component is within the scope. | ||
- `fadeOption: Object/Boolean` Default: `{ delay: 0, duration: 400 }`. Option for the fade transition. Set `null/false` to disable. | ||
- `onload: Function` Default: `null`. Callback when the component is loaded. |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
58580
6
1596
30