Commit 29212d95 by Иван Кубота

Debug menu:

Add new cards count bounder Store: New ELSE component and it's support in IF Account: New cards show logic update. Switch to 'No new cards' if count = 0 CardSlider: Refactor to JS class for speed, better readability, maintain and usage ease.
parent 7fa5a823
......@@ -246,11 +246,25 @@ Store.OR = Store.AGGREGATE(function(values, length) {
}
return result;
});
Store.ELSE = function(){if(!( this instanceof Store.ELSE))return new Store.ELSE()};
Store.IF = function(cfg, children){
var holders = {true: [], false: []},
holder = holders.true;
for( var i = 0, _i = children.length; i < _i; i++ ){
var child = children[ i ];
if(child instanceof Store.ELSE){
holder = holders.false;
}else{
holder.push(child);
}
}
holders.true.length === 0 && (holders.true = null);
holders.false.length === 0 && (holders.false = null);
return function( update ){
if( 'condition' in cfg )
var hook = function( cond ){
update( cond ? children : null );
update( cond ? holders.true : holders.false );
};
if(cfg.condition instanceof HookPrototype){
cfg.condition.hook( hook );
......
......@@ -78,7 +78,8 @@ const tmpStore = new Store({
loaded: false,
navigation: {current: 'Login'},
isMobile: false,
width: window.innerWidth
width: window.innerWidth,
newItemCount: 1000
});
let lastNav = null;
......
......@@ -4,6 +4,7 @@ import Logo from '/svg/logo.svg';
import User from "/svg/user.svg";
import Logout from "/svg/logout.svg";
import { Page } from "../../page/Page";
import Input from "../../cmp/field/Input";
const {IF, NOT} = Store;
const Header = D.declare('view.block.Header', () => {
......@@ -49,7 +50,7 @@ const Header = D.declare('view.block.Header', () => {
{name}
</button>
)
}</div>
}<div>New cards count <Input type={'number'} bind={tmpStore.bind('newItemCount')}/></div></div>
</div>
<IF condition={NOT(store.valEqual( 'navigation.current', 'Login'))}>
<div className="page-header__wrapper">
......
......@@ -144,6 +144,7 @@
top: 0;
left: 0;
z-index: 1;
background: rgba(0,0,100,0.4);
.hidden {
display: none;
}
......
......@@ -5,65 +5,163 @@ import './CardSlider.scss';
const CardSlider = (function(){
const Slider = function(cfg){
this.world = Object.create(this.world);
this.preset = Object.create(this.preset);
this.cfg = cfg;
this.name = this.cfg.name || (Math.random()+'').substr(2);
let itemClick = cfg.itemClick || (()=>{});
let current = [],
hash = {},
this.itemClick = cfg.itemClick || this.itemClick;
this.hash = {};
let hash = {},
items = [],
cardWidth = 160,
minPadding = 10,
mobilePadding = 10,
wrap,
elWrap,
width,
fullCardsCount,
padding,
updateSize = function () {
if(dom.clientWidth){
width = dom.clientWidth - paddingLeft - paddingRight;
fullCardsCount = width / ( cardWidth + minPadding ) | 0;
padding = mobile ? mobilePadding : ( width - fullCardsCount * cardWidth ) / ( fullCardsCount - 1 );
wrap.style.width = ( items.length * ( padding + cardWidth ) - padding ) + 'px';
return true;
}
return false;
},
update = function () {
updateSize() && updateVisibleDom();
};
let paddingLeft = 30, paddingRight = 30, mobile = false;
let waitTimeout = false;
let tuning;
let leftestItem = 0;
let lastLeftestItem = 0;
let leftButtonDisabled = new Store.Value.Boolean(true), rightButtonDisabled = new Store.Value.Boolean(true);
let updateButtonsState = function() {
let fromLeft = dom.scrollLeft;
let firstItem = Math.round( fromLeft / ( cardWidth + padding ));
leftButtonDisabled.set(firstItem===0);
rightButtonDisabled.set(firstItem+fullCardsCount>=items.length);
};
let untune = function() {
updateButtonsState();
tuning = false;
};
let tuneScrollPosition = function (x) {
if(tuning)
return untune();
waitTimeout = false;
tuning = true;
let fromLeft = dom.scrollLeft;
padding;
this.tune = this.tune.bind(this);
this.tuneScrollPosition = this.tuneScrollPosition.bind(this);
this.updateVisibleDom = this.updateVisibleDom.bind(this);
let scroller;
this.leftButtonDisabled = new Store.Value.Boolean(true);
this.rightButtonDisabled = new Store.Value.Boolean(true);
this.initDom();
let lastFrom, lastTo, last = false;
if(typeof cfg.items === 'function'){
cfg.items((items)=>{
this.setItems(items);
});
}else if(cfg.items){
this.setItems(cfg.items);
}
this.subscribe();
};
Slider.prototype = {
tuning: false,
waitTimeout: false,
deltaMove: 0,
world: {
mobile: false,
paddingLeft: 30,
paddingRight: 30,
leftestItem: 0,
lastLeftestItem: 0
},
preset: {
cardWidth: 160,
minPadding: 10,
mobilePadding: 10,
desktopPadding: 310,
},
subscribe: function() {
tmpStore.sub('width', Store.debounce((w) => {
let firstItem = Math.round(this.scroller.scrollLeft / (this.preset.cardWidth + this.world.padding));
this.update();
this.scroller.scrollLeft = firstItem * (this.preset.cardWidth + this.world.padding);
}, 30));
tmpStore.sub('isMobile', (is)=> {
this.world.mobile = is;
this.world.paddingLeft = this.world.paddingRight = is
? this.preset.mobilePadding
: this.preset.desktopPadding;
this.update();
});
this.scroller.addEventListener('scroll', Store.debounce((e) => {
requestAnimationFrame(this.updateVisibleDom);
this.waitUntilDoNotTouchScroll();
}, 1000 / 60));
},
update: function() {
this.updateSize() && this.updateVisibleDom();
},
updateSize: function() {
const {scroller, elWrap, items} = this;
const {
minPadding,
cardWidth,
mobilePadding
} = this.preset;
const {
mobile,
paddingLeft,
paddingRight
} = this.world;
if(scroller.clientWidth){
const width = this.world.width = scroller.clientWidth - paddingLeft - paddingRight;
const fullCardsCount = this.world.fullCardsCount = width / ( cardWidth + minPadding ) | 0;
const padding = this.world.padding = mobile ? mobilePadding : ( width - fullCardsCount * cardWidth ) / ( fullCardsCount - 1 );
elWrap.style.width = ( items.length * ( padding + cardWidth ) - padding ) + 'px';
return true;
}
return false;
},
updateButtonsState: function() {
let fromLeft = this.scroller.scrollLeft;
let firstItem = Math.round( fromLeft / ( this.preset.cardWidth + this.world.padding ));
this.leftButtonDisabled.set(firstItem===0);
this.rightButtonDisabled.set(firstItem+this.world.fullCardsCount>=this.items.length);
console.log(this.leftButtonDisabled.get(), this.rightButtonDisabled.get())
},
untune: function() {
this.updateButtonsState();
this.tuning = false;
},
tune: function() {
let scroller = this.scroller,
cardWidth = this.preset.cardWidth,
padding = this.world.padding,
deltaMove = this.deltaMove,
toLeft = this.toLeft;
if (Math.abs(toLeft - scroller.scrollLeft) > (cardWidth + padding)*1.3) {
return this.untune();
}
if (Math.sign(toLeft - scroller.scrollLeft) !== Math.sign(deltaMove)) {
return this.untune();
}
if (Math.abs(scroller.scrollLeft - toLeft) < Math.abs(deltaMove)) {
scroller.scrollLeft = toLeft;
return this.untune();
} else {
scroller.scrollLeft += deltaMove;
requestAnimationFrame(this.tune);
}
},
tuneScrollPosition: function(x) {
if(this.tuning)
return this.untune();
let {scroller, items} = this;
let {cardWidth} = this.preset;
let {fullCardsCount, padding, lastLeftestItem, leftestItem} = this.world;
this.waitTimeout = false;
this.tuning = true;
let fromLeft = scroller.scrollLeft;
let firstItem, toLeft;
if(x){
firstItem = Math.round( fromLeft / ( cardWidth + padding ));
if(firstItem+x<0 || firstItem+fullCardsCount+x>items.length){
return untune();
return this.untune();
}
toLeft = Math.round( (firstItem + x) * ( cardWidth + padding ));
......@@ -71,6 +169,7 @@ const CardSlider = (function(){
firstItem = Math.round( fromLeft / ( cardWidth + padding ) - ( lastLeftestItem > leftestItem ? 0.4 : -0.4 ) );
toLeft = Math.round( firstItem * ( cardWidth + padding ) );
}
this.toLeft = toLeft;
let deltaMove = (toLeft - fromLeft) / Math.ceil(200 / (1000 / 60));
while (deltaMove > 0 && deltaMove < 1) {
deltaMove *= 2;
......@@ -78,83 +177,49 @@ const CardSlider = (function(){
if (Math.abs(fromLeft + deltaMove - toLeft) > Math.abs(fromLeft - toLeft)) {
deltaMove = toLeft - fromLeft;
}
let tune = function () {
if (Math.abs(toLeft - dom.scrollLeft) > (cardWidth + padding)*1.3) {
return untune();
}
if (Math.sign(toLeft - dom.scrollLeft) !== Math.sign(deltaMove)) {
return untune();
}
if (Math.abs(dom.scrollLeft - toLeft) < Math.abs(deltaMove)) {
dom.scrollLeft = toLeft;
return untune();
} else {
dom.scrollLeft += deltaMove;
requestAnimationFrame(tune);
}
};
this.deltaMove = deltaMove;
if (Math.abs(deltaMove) > 0) {
requestAnimationFrame(tune);
requestAnimationFrame(this.tune);
} else {
return untune();
return this.untune();
}
};
let waitUntilDoNotTouchScroll = function () {
if (tuning)
},
waitUntilDoNotTouchScroll: function () {
if (this.tuning)
return;
lastLeftestItem = leftestItem;
leftestItem = dom.scrollLeft / (cardWidth + padding);
if (waitTimeout)
clearTimeout(waitTimeout);
waitTimeout = setTimeout(tuneScrollPosition, 80);
};
let dom;
let theDOM = <div class={D.cls('card-slider', {'card-slider--no-arrow-next': rightButtonDisabled})}>
<button class={D.cls(
'button',
'button--round',
'card-slider__control',
'card-slider__control--prev',
{'card-slider__control-disabled': leftButtonDisabled}
)} type="button"
onClick={()=>{
tuneScrollPosition(-1);
}}
aria-label="Показать предыдущую карточку">
<ArrPrev width="14" height="12"/>
</button>
<button class={D.cls(
'button',
'button--round',
'card-slider__control',
'card-slider__control--next',
{'card-slider__control-disabled': rightButtonDisabled}
)} type="button"
onClick={()=>{
tuneScrollPosition(1);
}}
aria-label="Показать следующую карточку">
<ArrNext width="14" height="12"/>
</button>
{dom = this.scroller = <div class='card-slider__scroller'>
{wrap = <div class='card-slider__wrapper'>{' '}</div>}
</div>}
</div>;
this.world.lastLeftestItem = this.world.leftestItem;
this.world.leftestItem = this.scroller.scrollLeft / (this.preset.cardWidth + this.world.padding);
let lastFrom, lastTo, last = false;
let updateVisibleDom = function () {
let from = Math.floor(dom.scrollLeft / (cardWidth + padding) - 1),
count = Math.ceil(dom.clientWidth / (cardWidth + padding)),
if (this.waitTimeout)
clearTimeout(this.waitTimeout);
this.waitTimeout = setTimeout(this.tuneScrollPosition, 80);
},
setItems: function(items) {
this.items = items;
this.update();
},
save: function() {
tmpStore.set(this.name, {left: this.scroller.scrollLeft});
},
restore: function() {
this.scroller.scrollLeft = tmpStore.get([this.name, 'left'].join('.'));
},
itemClick: function(item, action) {console.log('SLIDER:itemClick', item, action)},
updateVisibleDom: function () {
let {hash, scroller, items, elWrap, itemClick} = this;
let {lastFrom, lastTo, last} = this;
let {cardWidth} = this.preset;
let {padding} = this.world;
let from = Math.floor(scroller.scrollLeft / (cardWidth + padding) - 1),
count = Math.ceil(scroller.clientWidth / (cardWidth + padding)),
to;
to = from + count * 2;
from -= count;
from = Math.max(0, from);
to = Math.min(to, items.length);
//console.log({from, to, cardWidth, padding})
for (let i = from; i < to; i++) {
if (!(i in hash)) {
hash[i] = {
......@@ -180,61 +245,59 @@ const CardSlider = (function(){
if (i < from || i >= to) {
if (hash[i].inDOM) {
hash[i].inDOM = false;
wrap.removeChild(hash[i].dom);
elWrap.removeChild(hash[i].dom);
}
}
}
}
for (let i = from; i < to; i++) {
let x = paddingLeft + i * (cardWidth + padding);
let x = this.world.paddingLeft + i * (this.preset.cardWidth + padding);
if (hash[i].x !== x) {
hash[i].dom.style.left = (hash[i].x = x) + 'px';
}
if (!hash[i].inDOM) {
wrap.appendChild(hash[i].dom);
elWrap.appendChild(hash[i].dom);
hash[i].inDOM = true;
}
}
lastFrom = from;
lastTo = to;
last = true;
updateButtonsState();
};
dom.addEventListener('scroll', Store.debounce(function (e) {
requestAnimationFrame(updateVisibleDom);
waitUntilDoNotTouchScroll();
}, 1000 / 60));
cfg.items(function (i) {
items = i;
//update();
update();
});
tmpStore.sub('width', Store.debounce(function (w) {
let firstItem = Math.round(dom.scrollLeft / (cardWidth + padding));
update();
dom.scrollLeft = firstItem * (cardWidth + padding);
}, 30));
tmpStore.sub('isMobile', function(is) {
mobile = is;
paddingLeft = paddingRight = is ? 10 : 30;
update();
});
this.dom = theDOM;
};
Slider.prototype = {
save: function() {
tmpStore.set(this.name, {left: this.scroller.scrollLeft});
this.lastFrom = from;
this.lastTo = to;
this.last = true;
this.updateButtonsState();
},
restore: function() {
this.scroller.scrollLeft = tmpStore.get([this.name, 'left'].join('.'));
initDom: function() {
this.dom = <div class={D.cls('card-slider', {'card-slider--no-arrow-next': this.rightButtonDisabled})}>
<button class={D.cls(
'button',
'button--round',
'card-slider__control',
'card-slider__control--prev',
{'card-slider__control-disabled': this.leftButtonDisabled}
)} type="button"
onClick={()=>{
this.tuneScrollPosition(-1);
}}
aria-label="Показать предыдущую карточку">
<ArrPrev width="14" height="12"/>
</button>
<button class={D.cls(
'button',
'button--round',
'card-slider__control',
'card-slider__control--next',
{'card-slider__control-disabled': this.rightButtonDisabled}
)} type="button"
onClick={()=>{
this.tuneScrollPosition(1);
}}
aria-label="Показать следующую карточку">
<ArrNext width="14" height="12"/>
</button>
{this.scroller = <div class='card-slider__scroller'>
{this.elWrap = <div class='card-slider__wrapper'>{' '}</div>}
</div>}
</div>;
}
};
Slider.prototype.constructor = Slider;
......
......@@ -5,38 +5,49 @@ import {API} from "../../../dict/Consts";
import Card from "../../cmp/card/Card";
import CardSlider from "../../cmp/cardSlider/CardSlider";
import AccountNavigation from "../../block/accountNav/AccountNavigation";
const {IF, ELSE} = Store;
const Account = D.declare('view.page.Account', () => {
const showSlider = new Store.Value.Boolean(true);
// subscribe to new cards
store.sub(
[
'loaded.cards',
tmpStore.bind( 'afterNavigation.to' ),
tmpStore.bind( 'newCardsLoaded' ),
tmpStore.bind( 'newItemCount' )
],
function( loadedCards, to, newCardsLoaded, newItemCount ){
let stamp = [loadedCards, newItemCount].join(';');
if( newCardsLoaded !== stamp ){
if( loadedCards && to === 'Account' ){
const newCards = cards
.getArray()
.filter( card => card.seen === false )
.sort( ( a, b ) => a.id - b.id )
.slice(0, newItemCount);
// finally we get new cards!
showSlider.set(newCards.length);
Slider.setItems(newCards);
tmpStore.set( 'newCardsLoaded', stamp )
}
}
}
);
let Slider = new CardSlider( {
name: "newCardsSlider",
itemClick: ( item, action ) => {
action.execute( item );
},
items: _ => store.sub(
[
'loaded.cards',
'navigation.current',
tmpStore.bind( 'afterNavigation.to' ),
tmpStore.bind( 'newCardsLoaded' ),
],
function( loadedCards, to, newCardsLoaded ){
if( newCardsLoaded !== loadedCards ){
if( loadedCards && to === 'Account' ){
_( cards
.getArray()
.filter( card => card.seen === false )
.sort( ( a, b ) => a.id - b.id )
);
tmpStore.set( 'newCardsLoaded', loadedCards )
}
}
}
)
}
} );
const dom = <div class="account-page">
......@@ -51,29 +62,33 @@ const Account = D.declare('view.page.Account', () => {
<div class="account-page__content-inner">
<div class="account-page__cards">
<h2 class="account-page__title">Новые карточки для вашей должности:</h2>
{Slider}
<IF condition={showSlider}>
{Slider}
<ELSE/>
<div className="account-page__no-cards">
<div className="account-page__no-cards-info">
<div className="account-page__no-cards-item">
<Card
type={'product'}
disabled
title={'Карточек нет'}
image={'/uploads/images/card-disabled.png'}
/>
</div>
<div className="account-page__no-cards-text">
<p>Вы&nbsp;просмотрели все новые карточки этого месяца.</p>
<p>Так держать!</p>
</div>
<div className="account-page__no-cards-info">
<div className="account-page__no-cards-item">
<Card
type={'product'}
disabled
title={'Карточек нет'}
image={'/uploads/images/card-disabled.png'}
/>
</div>
<div className="account-page__no-cards-image">
<picture>
<source srcset="uploads/images/assistant-nocards-mob.svg" media={"(max-width: 767px"}/>
<img src="uploads/images/assistant-nocards.svg" alt=""/>
</picture>
<div className="account-page__no-cards-text">
<p>Вы&nbsp;просмотрели все новые карточки этого месяца.</p>
<p>Так держать!</p>
</div>
</div>
<div className="account-page__no-cards-image">
<picture>
<source srcSet="uploads/images/assistant-nocards-mob.svg" media={"(max-width: 767px"}/>
<img src="uploads/images/assistant-nocards.svg" alt=""/>
</picture>
</div>
</div>
</IF>
<div className="account-page__nav">
<AccountNavigation/>
</div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment