appbar should be dumber

There's no need to have one object cross views.
This commit is contained in:
Timothy Farrell 2018-01-05 15:55:12 -06:00
parent 3d66205755
commit fe2dd7d783
5 changed files with 153 additions and 222 deletions

View File

@ -15,6 +15,7 @@ import { ImageType } from '../data/image.js';
import { pouchDocArrayHash, pouchDocHash, hashSet, extractID } from '../utils/conversion.js'; import { pouchDocArrayHash, pouchDocHash, hashSet, extractID } from '../utils/conversion.js';
import { SectionView } from './sectionView.js'; import { SectionView } from './sectionView.js';
import { Icon } from './components/icon.js'; import { Icon } from './components/icon.js';
import { AppBar } from './components/appbar.js';
import { injectStyle, styled } from '../services/style.js'; import { injectStyle, styled } from '../services/style.js';
import { CLICKABLE, FILL_STYLE } from './styles.js'; import { CLICKABLE, FILL_STYLE } from './styles.js';
@ -26,35 +27,26 @@ export function uploadImages(evt, files) {
} }
} }
export function AllImagesView(vm, params, key, context) { export function AllImagesView(vm, params) {
const { appbar, appbarView } = context;
const model = prop({}, pouchDocHash); const model = prop({}, pouchDocHash);
const images = container([], pouchDocArrayHash); const images = container([], pouchDocArrayHash);
const containerScrollTop = prop(0);
const selectedIds = container(new Set(), hashSet); const selectedIds = container(new Set(), hashSet);
const appBarTitle = computed(s => (s.size > 0 ? `${s.size} selected` : 'Photos'), [selectedIds]);
const selectMode = computed(sIds => sIds.size > 0, [selectedIds]); const selectMode = computed(sIds => sIds.size > 0, [selectedIds]);
const sections = computed( const appBarTitle = computed(s => (s.size > 0 ? `${s.size} selected` : 'Photos'), [selectedIds]);
imageArr => { const appBarStyle = computed(
const sectionMap = imageArr.reduce((acc, i) => { t => ({
const date = i.originalDate.substr(0, 10); boxShadow: t === 0 ? 'none' : `0px 3px 3px rgba(0, 0, 0, .2)`
return Object.assign(acc, { [date]: (acc[date] || []).concat(i) }); }),
}, {}); [containerScrollTop]
return Object.entries(sectionMap)
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([date, _images]) => ({
title: format(date, 'MMMM D, YYYY'),
sectionId: date,
images: _images
}));
},
[images]
); );
const appBarUp = computed(s => (s ? { name: 'x', action: deSelect } : undefined), [selectMode]);
function renderAppBarButtons() { const appBarActions = computed(
if (selectMode()) { s =>
return [ s
? [
trashButtonContainer( trashButtonContainer(
{ {
onclick: deleteSelectedImages onclick: deleteSelectedImages
@ -66,10 +58,8 @@ export function AllImagesView(vm, params, key, context) {
}) })
] ]
) )
]; ]
} : [
return [
uploadButton([ uploadButton([
el( el(
'label', 'label',
@ -95,7 +85,29 @@ export function AllImagesView(vm, params, key, context) {
onchange: uploadImages, onchange: uploadImages,
class: injectStyle({ display: 'none' }) class: injectStyle({ display: 'none' })
}) })
]; ],
[selectMode]
);
const sections = computed(
imageArr => {
const sectionMap = imageArr.reduce((acc, i) => {
const date = i.originalDate.substr(0, 10);
return Object.assign(acc, { [date]: (acc[date] || []).concat(i) });
}, {});
return Object.entries(sectionMap)
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([date, _images]) => ({
title: format(date, 'MMMM D, YYYY'),
sectionId: date,
images: _images
}));
},
[images]
);
function deSelect() {
selectedIds.clear();
} }
function deleteSelectedImages() { function deleteSelectedImages() {
@ -146,26 +158,7 @@ export function AllImagesView(vm, params, key, context) {
} }
function handleContentScroll(evt) { function handleContentScroll(evt) {
appbar.companionScrollTop(evt.target.scrollTop); containerScrollTop(evt.target.scrollTop);
}
function pushAppBarState() {
const up = selectMode()
? {
name: 'x',
onclick: () => selectedIds.clear()
}
: undefined;
appbar.pushState({
title: appBarTitle,
actions: renderAppBarButtons,
up
});
}
function popAppBarState() {
appbar.popState();
} }
ImageType.find( ImageType.find(
@ -174,15 +167,11 @@ export function AllImagesView(vm, params, key, context) {
}, },
{ live: true } { live: true }
).then(la => { ).then(la => {
pushAppBarState();
selectMode.subscribe(mode => {
popAppBarState();
pushAppBarState();
}),
subscribeToRender(vm, [ subscribeToRender(vm, [
selectedIds, selectedIds,
images, images,
selectMode, selectMode,
appBarStyle,
() => la.subscribe(res => images.splice(0, images.length, ...res)) () => la.subscribe(res => images.splice(0, images.length, ...res))
]); ]);
}); });
@ -196,8 +185,7 @@ export function AllImagesView(vm, params, key, context) {
selectedIds, selectedIds,
selectMode: selectMode() selectMode: selectMode()
}, },
sectionId, sectionId
context
); );
} }
@ -207,7 +195,12 @@ export function AllImagesView(vm, params, key, context) {
class: 'allImages' class: 'allImages'
}, },
[ [
iv(appbarView), AppBar({
style: appBarStyle(),
title: appBarTitle(),
actions: appBarActions(),
up: appBarUp()
}),
allImagesContent( allImagesContent(
{ {
onscroll: handleContentScroll, onscroll: handleContentScroll,

View File

@ -1,92 +1,46 @@
import { prop, computed, container, pick } from 'frptools'; import { prop, computed, container, pick } from 'frptools';
import { Icon } from './icon.js'; import { Icon } from './icon.js';
import { router } from '../../services/router.js'; import { defineElement as el } from '../../utils/domvm.js';
import { defineElement as el, subscribeToRender } from '../../utils/domvm.js';
import { injectStyle, styled } from '../../services/style.js'; import { injectStyle, styled } from '../../services/style.js';
import { CLICKABLE } from '../styles.js'; import { CLICKABLE } from '../styles.js';
let seq = 0; export function AppBar(params) {
const { title, up, actions } = params;
const props = Object.assign({}, params);
export function AppBarView(vm, params, key, opts) { delete props.title;
const stateStack = container([], arr => arr.length && arr[0]._seq); delete props.up;
const companionScrollTop = prop(0); delete props.actions;
const currentState = computed(stack => stack[0] || {}, [stateStack]); const upProps = Object.assign({}, up || {});
const title = computed(pick('title', ''), [currentState]); const { button, action } = upProps;
const renderActions = computed(pick('actions'), [currentState]);
const up = computed(pick('up'), [currentState]);
const upButton = computed(pick('name', 'arrow_left'), [up]);
const upAction = computed(
upState => (upState.onclick ? upState.onclick : [popState, upState.navigateTo]),
[up]
);
const stateStyle = computed(pick('style', {}), [currentState]);
const boxShadowStyle = computed(t => (t === 0 ? 'none' : `0px 3px 3px rgba(0, 0, 0, .2)`), [ delete upProps.button;
companionScrollTop delete upProps.action;
]);
const containerStyle = computed( return appBarContainer(props, [
(boxShadow, style) => ({ up
css: Object.assign({ boxShadow }, style)
}),
[boxShadowStyle, stateStyle]
);
if (opts.appbar) {
throw new Error('Cannot have more than one AppBar.');
}
function pushState(newState) {
companionScrollTop(0);
stateStack.unshift(Object.assign({ _seq: seq++ }, newState));
}
function popState(navigateTo) {
companionScrollTop(0);
stateStack.shift();
if (navigateTo) {
router.goto(navigateTo);
}
}
function replaceState(newState) {
companionScrollTop(0);
stateStack._.shift();
stateStack.unshift(Object.assign({ _seq: seq++ }, newState));
}
opts.appbar = {
pushState,
popState,
replaceState,
companionScrollTop
};
subscribeToRender(vm, [containerStyle, renderActions, up, title]);
return (vm, params) => {
const _buttons = renderActions() || (() => {});
return appBarContainer(containerStyle(), [
up()
? upButtonContainer( ? upButtonContainer(
{ {
onclick: upAction() onclick: up.action
}, },
[ [
Icon({ Icon(
name: upButton(), Object.assign(
{
name: up.button || 'arrow_left',
size: 0.75 size: 0.75
}) },
upProps
)
)
] ]
) )
: null, : null,
titleContainer(title()), titleContainer(title),
headerRight(_buttons()) actionContainer(actions)
]); ]);
};
} }
const appBarContainer = styled({ const appBarContainer = styled({
@ -96,7 +50,7 @@ const appBarContainer = styled({
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
width: '100%', width: '100%',
transition: 'opacity .13s cubic-bezier(0.0,0.0,0.2,1)' transition: 'opacity .5s cubic-bezier(0.0,0.0,0.2,1)'
}); });
const upButtonContainer = styled( const upButtonContainer = styled(
@ -106,7 +60,7 @@ const upButtonContainer = styled(
CLICKABLE CLICKABLE
); );
const headerRight = styled({ const actionContainer = styled({
display: 'flex', display: 'flex',
alignItems: 'center' alignItems: 'center'
}); });

View File

@ -15,19 +15,28 @@ import { pouchDocHash, pick } from '../utils/conversion.js';
import { AttachmentImageView } from './components/attachmentImage.js'; import { AttachmentImageView } from './components/attachmentImage.js';
import { Overlay } from './components/overlay.js'; import { Overlay } from './components/overlay.js';
import { Icon } from './components/icon.js'; import { Icon } from './components/icon.js';
import { AppBar } from './components/appbar.js';
import { styled, injectStyle } from '../services/style.js'; import { styled, injectStyle } from '../services/style.js';
import { error } from '../services/console.js'; import { error } from '../services/console.js';
import { CLICKABLE, FILL_STYLE } from './styles.js'; import { CLICKABLE, FILL_STYLE } from './styles.js';
export function FocusView(vm, params, key, { appbar, appbarView }) { export function FocusView(vm, params) {
const id = prop(); const id = prop();
const doc = prop({}, pouchDocHash); const doc = prop({}, pouchDocHash);
const { body } = document;
const nextLink = prop(); const nextLink = prop();
const prevLink = prop(); const prevLink = prop();
const mouseActive = prop(true); const mouseActive = prop(true);
let mouseMoveTimeout = null; let mouseMoveTimeout = null;
const appBarStyle = computed(
mA => ({
position: 'fixed',
opacity: mA ? 1 : 0,
backgroundImage: 'linear-gradient(0deg,rgba(0,0,0,0),rgba(0,0,0,0.4))'
}),
[mouseActive],
s => s.opacity
);
const imageStyle = computed( const imageStyle = computed(
({ width: iw, height: ih }, { width: vw, height: vh }) => { ({ width: iw, height: ih }, { width: vw, height: vh }) => {
const imageRatio = iw / ih; const imageRatio = iw / ih;
@ -51,20 +60,9 @@ export function FocusView(vm, params, key, { appbar, appbarView }) {
}, },
[doc, fullViewportSize] [doc, fullViewportSize]
); );
const appbarState = computed(
mA => ({
title: '',
actions: renderAppBarButtons,
style: { position: 'fixed', opacity: mA ? 1 : 0 },
up: {
navigateTo: 'home'
}
}),
[mouseActive]
);
function navBack() { function navBack() {
appbar.popState('home'); router.goto('home');
} }
async function clickTrash() { async function clickTrash() {
@ -93,35 +91,6 @@ export function FocusView(vm, params, key, { appbar, appbarView }) {
mouseActive(!mouseActive()); mouseActive(!mouseActive());
}; };
function renderAppBarButtons() {
return [
trashButtonContainer(
{
onclick: clickTrash
},
[
Icon({
name: 'trash',
size: 0.75
})
]
)
];
}
// Set the appbar title.
appbar.pushState({
title: '',
actions: renderAppBarButtons,
style: {
position: 'fixed'
},
up: {
navigateTo: 'home'
}
});
// Subscribe to our changables. // Subscribe to our changables.
subscribeToRender( subscribeToRender(
vm, vm,
@ -129,15 +98,14 @@ export function FocusView(vm, params, key, { appbar, appbarView }) {
doc, doc,
nextLink, nextLink,
prevLink, prevLink,
() => appbarState.subscribe(appbar.replaceState), appBarStyle,
// Look for our image and set it. // Look for our image and set it.
() => () =>
id.subscribe(async _id => { id.subscribe(async _id => {
if (!_id) { if (!_id) {
return; return;
} }
const image = await ImageType.find(_id); doc(await ImageType.find(_id));
doc(image);
Promise.all([ Promise.all([
ImageType.find({ _id: { $lt: _id } }, { limit: 2, sort: [{ _id: 'desc' }] }), ImageType.find({ _id: { $lt: _id } }, { limit: 2, sort: [{ _id: 'desc' }] }),
@ -156,6 +124,7 @@ export function FocusView(vm, params, key, { appbar, appbarView }) {
// Start navigation // Start navigation
id(params.vars.id); id(params.vars.id);
mouseMove();
return function() { return function() {
const _id = doc() && doc()._id; const _id = doc() && doc()._id;
@ -172,13 +141,32 @@ export function FocusView(vm, params, key, { appbar, appbarView }) {
onclick: mouseClick onclick: mouseClick
}, },
[ [
iv(appbarView), AppBar({
style: appBarStyle(),
title: '',
up: { action: navBack, fill: 'white' },
actions: [
trashButtonContainer(
{
onclick: clickTrash
},
[
Icon({
name: 'trash',
size: 0.75,
fill: 'white'
})
]
)
]
}),
focusContent([ focusContent([
prevLink() prevLink()
? prevClickZone({ href: prevLink() }, [ ? prevClickZone({ href: prevLink() }, [
Icon({ Icon({
name: 'chevron_left', name: 'chevron_left',
size: 0.75 size: 0.75,
fill: 'white'
}) })
]) ])
: null, : null,
@ -190,7 +178,8 @@ export function FocusView(vm, params, key, { appbar, appbarView }) {
? nextClickZone({ href: nextLink() }, [ ? nextClickZone({ href: nextLink() }, [
Icon({ Icon({
name: 'chevron_right', name: 'chevron_right',
size: 0.75 size: 0.75,
fill: 'white'
}) })
]) ])
: null : null
@ -230,7 +219,8 @@ const focusContainer = styled(
display: 'flex', display: 'flex',
overflow: 'hidden', overflow: 'hidden',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center' alignItems: 'center',
backgroundColor: 'black'
}, },
FILL_STYLE FILL_STYLE
); );

View File

@ -13,19 +13,15 @@ import { AllImagesView, uploadImages } from './allImages.js';
import { FocusView } from './focus.js'; import { FocusView } from './focus.js';
import { Dropzone } from './components/dropzone.js'; import { Dropzone } from './components/dropzone.js';
import { Overlay } from './components/overlay.js'; import { Overlay } from './components/overlay.js';
import { AppBarView } from './components/appbar.js';
import { Icon } from './components/icon.js'; import { Icon } from './components/icon.js';
import { routeChanged } from '../services/router.js'; import { routeChanged } from '../services/router.js';
import { injectStyle, styled } from '../services/style.js'; import { injectStyle, styled } from '../services/style.js';
import { FILL_STYLE } from './styles.js'; import { FILL_STYLE } from './styles.js';
export function GalleryView(vm) { export function GalleryView(vm) {
const context = {};
const routeName = prop(); const routeName = prop();
const routeParams = prop(); const routeParams = prop();
context.appbarView = cv(AppBarView, {}, 'appbar', context);
routeChanged.subscribe(function onRouteChange(name, params) { routeChanged.subscribe(function onRouteChange(name, params) {
routeName(name); routeName(name);
routeParams(params); routeParams(params);
@ -37,8 +33,8 @@ export function GalleryView(vm) {
content([ content([
renderSwitch( renderSwitch(
{ {
photos: [AllImagesView, {}, 'allImages', context], photos: [AllImagesView, {}, 'allImages'],
focus: [FocusView, routeParams(), `focus_${routeParams() && routeParams().id}`, context] focus: [FocusView, routeParams(), `focus_${routeParams() && routeParams().id}`]
}, },
routeName() routeName()
) )

View File

@ -22,8 +22,7 @@ const CONTENT_MARGIN_WIDTH = 2 * CONTENT_MARGIN;
const aspectRatio = img => img.width / img.height; const aspectRatio = img => img.width / img.height;
export function SectionView(vm, params, key, context) { export function SectionView(vm, params, key) {
const { appbar } = context;
const { title, photos } = params; const { title, photos } = params;
const sectionSelectButtonRef = `secSel${key}`; const sectionSelectButtonRef = `secSel${key}`;
@ -72,8 +71,7 @@ export function SectionView(vm, params, key, context) {
width, width,
height height
}, },
photo._hash(), photo._hash()
context
); );
} }