Add image selection

This commit is contained in:
Timothy Farrell 2017-12-08 08:06:03 -06:00
parent cfdc78ab4c
commit 213d2b1559
10 changed files with 246 additions and 136 deletions

View File

@ -16,7 +16,7 @@
"domvm": "~3.2.1", "domvm": "~3.2.1",
"exif-parser": "~0.1.9", "exif-parser": "~0.1.9",
"extract-text-webpack-plugin": "^3.0.2", "extract-text-webpack-plugin": "^3.0.2",
"frptools": "2.1.0", "frptools": "2.2.0",
"pica": "~2.0.8", "pica": "~2.0.8",
"pouchdb-adapter-http": "~6.3.4", "pouchdb-adapter-http": "~6.3.4",
"pouchdb-adapter-idb": "~6.3.4", "pouchdb-adapter-idb": "~6.3.4",

View File

@ -1,9 +1,15 @@
import { prop, computed } from 'frptools'; import { prop, computed, container } from 'frptools';
import { subscribeToRender, defineView, defineElement as el } from '../utils/domvm.js'; import {
subscribeToRender,
defineView,
subscribeToRender,
defineElement as el
} from '../utils/domvm.js';
import { error } from '../services/console.js';
import { ImageType } from '../data/image.js'; import { ImageType } from '../data/image.js';
import { pouchDocArrayHash, pouchDocHash } from '../utils/conversion.js'; import { pouchDocArrayHash, pouchDocHash, hashSet, extractID } from '../utils/conversion.js';
import { AlbumTemplate } from './components/albumTemplate.js'; import { AlbumTemplate } from './components/albumTemplate.js';
import { injectStyle, styled } from '../services/style.js'; import { injectStyle, styled } from '../services/style.js';
@ -17,7 +23,11 @@ export function uploadImages(evt, files) {
export function AllImagesView(vm, params, key, opts) { export function AllImagesView(vm, params, key, opts) {
const model = prop({}, pouchDocHash); const model = prop({}, pouchDocHash);
const images = prop([], pouchDocArrayHash); const images = container([], pouchDocArrayHash);
const visibleIds = computed(arr => arr.map(extractID), [images]);
const hoverId = prop(null);
const selectedIds = container(new Set(), hashSet);
const mode = computed(sIds => (sIds.size > 0 ? 'select' : 'view'), [selectedIds]);
ImageType.find( ImageType.find(
{ {
@ -26,7 +36,7 @@ export function AllImagesView(vm, params, key, opts) {
true true
).then(la => { ).then(la => {
opts.appbar.renderButtons(renderAppBarButtons); opts.appbar.renderButtons(renderAppBarButtons);
subscribeToRender(vm, [images], [la.subscribe(images)]); subscribeToRender(vm, [images], [la.subscribe(res => images.splice(0, images.length, ...res))]);
}); });
function renderAppBarButtons() { function renderAppBarButtons() {
@ -43,27 +53,74 @@ export function AllImagesView(vm, params, key, opts) {
}) })
]; ];
} }
//
// function deleteImage(i) {
// ImageType.delete(i._id);
// }
//
// function addAlbum() {
// const albumName = prompt("Album Name");
// if (albumName && albumName.trim()) {
// const a = new AlbumType({
// title: albumName.trim(),
// count: 0
// });
// a.save();
// }
// }
function deleteImage(i) { function nodeParentWithType(node, type) {
ImageType.delete(i._id); let parentNode = node;
while (parentNode && (!parentNode.data || parentNode.data.type !== type)) {
parentNode = parentNode.parent;
}
if (!parentNode) {
error(`Could not find {"type": "${type}"} parent.`);
return;
}
return parentNode;
} }
function addAlbum() { function toggleSelect(evt, node, vm) {
const albumName = prompt('Album Name'); const imageNode = nodeParentWithType(node, 'image');
if (albumName && albumName.trim()) { const id = imageNode.data._id;
const a = new AlbumType({ if (selectedIds.has(id)) {
title: albumName.trim(), selectedIds.delete(id);
count: 0 } else {
}); selectedIds.add(id);
a.save();
} }
} }
function toggleAll(evt, node, vm) {
if (images.length === selectedIds.size) {
selectedIds.clear();
} else {
images.map(extractID).forEach(i => selectedIds.add(i));
}
}
subscribeToRender(vm, [selectedIds, images, hoverId, mode]);
return function() { return function() {
return AlbumTemplate({ return el(
'.eventSnarfer',
{
onclick: {
'.photoSelect .icon svg path': toggleSelect,
'.photoSelect .icon': toggleSelect,
'.albumSelectButton .icon': toggleAll,
'.albumSelectButton .icon svg path': toggleAll
}
},
[
AlbumTemplate({
title: 'Test', title: 'Test',
id: 1, id: 1,
photos: images() photos: images,
}); selectedIds,
mode: mode()
})
]
);
}; };
} }

View File

@ -1,67 +1,78 @@
import { prop } from 'frptools';
import { import {
defineView as vw, defineView as vw,
defineElement as el, defineElement as el,
patchRefStyleMap, patchRefStyleMap,
patchNodeStyle patchNodeStyle,
subscribeToRender
} from '../../utils/domvm.js'; } from '../../utils/domvm.js';
import { injectStyle, styled } from '../../services/style.js'; import { injectStyle, styled } from '../../services/style.js';
import { DEFAULT_TRANSITION, CSS_FULL_SIZE, IMAGE_MARGIN } from '../styles.js';
import { Icon } from './icon.js'; import { Icon } from './icon.js';
import { AttachmentImageView } from './attachmentImage.js'; import { AttachmentImageView } from './attachmentImage.js';
export function AlbumPhotoTemplate(doc, isSelected, selectMode) { const _imageHover = false;
const dim = 'opacity: 0.7;';
const off = 'opacity: 0;';
const full = 'opacity: 1;';
export function AlbumPhotoTemplate(vm, { doc }) {
const photoSelectButtonRef = `pSB${doc._id}`; const photoSelectButtonRef = `pSB${doc._id}`;
const photoBackgroundRef = `pBkd${doc._id}`; const photoBackgroundRef = `pBkd${doc._id}`;
const hover = prop(false);
const hoverSelectButton = prop(false);
subscribeToRender(vm, [hover, hoverSelectButton]);
return function render(vm, { isSelected, selectMode }) {
return photoContainer( return photoContainer(
{ {
onmouseenter: [ class: 'photoContainer',
patchRefStyleMap, onmouseenter: [hover, true],
{ [photoSelectButtonRef]: 'opacity: 0.7;', [photoBackgroundRef]: 'opacity: 0.7;' } onmouseleave: [hover, false],
], css: {
onmouseleave: [ cursor: selectMode ? 'pointer' : 'zoom-in'
patchRefStyleMap, }
{ [photoSelectButtonRef]: 'opacity: 0;', [photoBackgroundRef]: 'opacity: 0;' }
]
}, },
[ [
vw(AttachmentImageView, doc, doc._hash()), AttachmentImageView(doc, {
css: {
transform: isSelected ? 'translateZ(-50px)' : null
}
}),
photoSelectButton( photoSelectButton(
{ {
_ref: photoSelectButtonRef, _ref: photoSelectButtonRef,
_data: doc,
class: 'photoSelect',
css: { css: {
// backgroundColor: isSelected ? 'white' : 'transparent', backgroundColor: isSelected ? 'white' : 'transparent',
// opacity: isSelected ? 1 : selectMode || _imageHover ? 0.7 : 0, opacity: isSelected || hoverSelectButton() ? 1 : selectMode || hover() ? 0.7 : 0
}, },
onmouseenter: [patchNodeStyle, 'opacity: 1;'], onmouseenter: [hoverSelectButton, true],
onmouseleave: [patchNodeStyle, 'opacity: 0.7;'] onmouseleave: [hoverSelectButton, false]
}, },
[ [
Icon({ Icon({
name: 'check_circle', name: selectMode && !isSelected ? 'circle_o' : 'check_circle',
size: 0.75, size: 0.75,
fill: '#fff' // isSelected ? '#00C800' : '#fff' fill: isSelected ? '#00C800' : '#fff'
}) })
] ]
), ),
photoBackdrop({ photoBackdrop({
_ref: photoBackgroundRef, _ref: photoBackgroundRef,
css: { css: {
// transform: isSelected ? 'translateZ(-50px)' : null, transform: isSelected ? 'translateZ(-50px)' : null,
// opacity: selectMode || _imageHover ? 0.7 : 0, opacity: selectMode || hover() ? 0.7 : 0
} }
}) })
] ]
); );
};
} }
const IMAGE_MARGIN = 2;
const CSS_FULL_SIZE = {
width: '100%',
height: '100%'
};
const photoContainer = styled({ const photoContainer = styled({
position: 'relative', position: 'relative',
perspective: '1000px', perspective: '1000px',
@ -70,21 +81,18 @@ const photoContainer = styled({
cursor: 'zoom-in' cursor: 'zoom-in'
}); });
const image = styled('img', CSS_FULL_SIZE, { const image = styled('img', CSS_FULL_SIZE, DEFAULT_TRANSITION, {
position: 'absolute', position: 'absolute',
top: 0, top: 0,
left: 0, left: 0
transition: 'transform .135s cubic-bezier(0.0,0.0,0.2,1)'
}); });
const photoSelectButton = styled({ const photoSelectButton = styled(DEFAULT_TRANSITION, {
position: 'absolute', position: 'absolute',
top: '4%', top: '4%',
left: '4%', left: '4%',
zIndex: 2, zIndex: 2,
display: 'flex', display: 'flex',
transition:
'transform .135s cubic-bezier(0.0,0.0,0.2,1), opacity .135s cubic-bezier(0.0,0.0,0.2,1)',
borderRadius: '50%', borderRadius: '50%',
padding: '2px', padding: '2px',
backgroundColor: 'transparent', backgroundColor: 'transparent',
@ -92,11 +100,10 @@ const photoSelectButton = styled({
cursor: 'pointer' cursor: 'pointer'
}); });
const photoBackdrop = styled(CSS_FULL_SIZE, { const photoBackdrop = styled(CSS_FULL_SIZE, DEFAULT_TRANSITION, {
position: 'absolute', // Unnecessary but helps with a rendering bug in Chrome. https://gitlab.com/explorigin/gallery/issues/1
top: '0px', top: '0px',
left: '0px', left: '0px',
zIndex: 1, zIndex: 1,
transition:
'transform .135s cubic-bezier(0.0,0.0,0.2,1), opacity .135s cubic-bezier(0.0,0.0,0.2,1)',
backgroundImage: 'linear-gradient(to bottom,rgba(0,0,0,0.26),transparent 56px,transparent)' backgroundImage: 'linear-gradient(to bottom,rgba(0,0,0,0.26),transparent 56px,transparent)'
}); });

View File

@ -5,13 +5,26 @@ import {
patchNodeStyle patchNodeStyle
} from '../../utils/domvm.js'; } from '../../utils/domvm.js';
import { injectStyle, styled } from '../../services/style.js'; import { injectStyle, styled } from '../../services/style.js';
import { DEFAULT_TRANSITION } from '../styles.js';
import { Icon } from './icon.js'; import { Icon } from './icon.js';
import { AlbumPhotoTemplate } from './albumPhotoTemplate.js'; import { AlbumPhotoTemplate } from './albumPhotoTemplate.js';
export function AlbumTemplate(params) { export function AlbumTemplate(params) {
const { id, title, photos } = params; const { id, title, photos, selectedIds, mode } = params;
const albumSelectButtonRef = `albSel${id}`; const albumSelectButtonRef = `albSel${id}`;
const selectMode = mode === 'select';
function photoMap(doc) {
return vw(
AlbumPhotoTemplate,
{
doc,
isSelected: selectedIds.has(doc._id),
selectMode
},
doc._hash()
);
}
return Album( return Album(
{ {
@ -25,12 +38,16 @@ export function AlbumTemplate(params) {
{ {
_ref: albumSelectButtonRef, _ref: albumSelectButtonRef,
onmouseenter: [patchNodeStyle, 'opacity: 1;'], onmouseenter: [patchNodeStyle, 'opacity: 1;'],
onmouseleave: [patchNodeStyle, 'opacity: 0.7;'] onmouseleave: [patchNodeStyle, 'opacity: 0.7;'],
css: {
opacity: selectMode ? 0.7 : 0
},
class: 'albumSelectButton'
}, },
[Icon({ name: 'check_circle', size: 0.25 })] [Icon({ name: 'check_circle', size: 0.25 })]
) )
]), ]),
albumContent(photos.map(AlbumPhotoTemplate)) albumContent(photos.map(photoMap))
] ]
); );
} }
@ -50,10 +67,8 @@ const albumContent = styled({
userSelect: 'none' userSelect: 'none'
}); });
const albumSelectButton = styled({ const albumSelectButton = styled(DEFAULT_TRANSITION, {
paddingLeft: '0.5em', paddingLeft: '0.5em',
cursor: 'pointer', cursor: 'pointer',
opacity: 0, // TODO onhover 0.7 opacity: 0
transition:
'transform 0.135s cubic-bezier(0, 0, 0.2, 1), opacity 0.135s cubic-bezier(0, 0, 0.2, 1)'
}); });

View File

@ -4,55 +4,58 @@ import { defineElement as el } from '../../utils/domvm.js';
import { ImageType } from '../../data/image.js'; import { ImageType } from '../../data/image.js';
import { FileType } from '../../data/file.js'; import { FileType } from '../../data/file.js';
import { pouchDocHash } from '../../utils/conversion.js'; import { pouchDocHash } from '../../utils/conversion.js';
import { styled } from '../../services/style.js';
import { DEFAULT_TRANSITION } from '../styles.js';
export function AttachmentImageView(vm, params) { const srcMap = new Map();
const model = prop(params, pouchDocHash);
const id = computed(pouchDocHash, [model]);
const sizes = computed(d => d.sizes, [model]); // always update
const blobURL = prop(''); async function loadImageFromBlob(doc, evt, node, vm) {
const imageURL = computed((sizes, bURL) => bURL || sizes.thumbnail || sizes.full, [ const { sizes, _id } = doc;
sizes, const options = ['thumbnail', 'preview', 'full'].filter(o => sizes.hasOwnProperty(o));
blobURL
]);
model.subscribe(() => {
if (blobURL()) {
URL.revokeObjectURL(blobURL());
blobURL('');
}
});
async function loadImageFromBlob() {
const options = ['thumbnail', 'preview', 'full'].filter(o => sizes().hasOwnProperty(o));
for (let attempt of options) { for (let attempt of options) {
try { try {
const data = await FileType.getFromURL(sizes()[attempt]); const data = await FileType.getFromURL(sizes[attempt]);
let src = evt.target.src;
if (blobURL()) { if (src.startsWith('blob:')) {
URL.revokeObjectURL(blobURL()); URL.revokeObjectURL(src);
} }
blobURL(URL.createObjectURL(data)); src = URL.createObjectURL(data);
return; node.patch({ src });
srcMap.set(_id, src);
// node.data = attempt;
break;
} catch (err) { } catch (err) {
continue; continue;
} }
} }
}
function cleanup() {
URL.revokeObjectURL(blobURL());
}
return function render() {
return el('img', {
src: imageURL,
onerror: loadImageFromBlob,
_key: id(),
_hooks: {
didRemove: cleanup
}
});
};
} }
function cleanup(id, evt) {
const { src } = evt.target;
if (src.startsWith('blob:')) {
URL.revokeObjectURL(s);
srcMap.remove(id);
}
}
export function AttachmentImageView(doc, props) {
const { sizes, _id } = doc;
const src = srcMap.get(_id) || sizes.thumbnail || sizes.preview || sizes.full;
return image(
Object.assign(
{
src,
onerror: [loadImageFromBlob, doc],
_key: _id,
_hooks: {
didRemove: [cleanup, _id]
}
},
props || {}
)
);
}
const image = styled('img', DEFAULT_TRANSITION);

View File

@ -8,7 +8,7 @@ export function ThumbnailTemplate(doc, remove, key) {
{ {
onclick: { img: [remove, doc] } onclick: { img: [remove, doc] }
}, },
[vw(AttachmentImageView, doc, key)] [AttachmentImageView(doc)]
) )
]); ]);
} }

View File

@ -0,0 +1,11 @@
export const IMAGE_MARGIN = 2;
export const CSS_FULL_SIZE = {
width: '100%',
height: '100%'
};
export const DEFAULT_TRANSITION = {
transition:
'transform 0.135s cubic-bezier(0, 0, 0.2, 1), opacity 0.135s cubic-bezier(0, 0, 0.2, 1)'
};

View File

@ -16,6 +16,6 @@ export function isObject(obj) {
return typeof obj === 'object' && !Array.isArray(obj); return typeof obj === 'object' && !Array.isArray(obj);
} }
export function isString(str) { export function isString(obj) {
return typeof obj === 'string'; return typeof obj === 'string';
} }

View File

@ -49,3 +49,13 @@ export const pick = id => doc => doc[id];
export const extractID = pick('_id'); export const extractID = pick('_id');
export const extractREV = pick('_rev'); export const extractREV = pick('_rev');
export function hashSet(_a) {
if (_a instanceof Set) {
return Array.from(_a.keys())
.sort()
.map(k => `${(typeof k).substr(0, 1)}:${encodeURIComponent(k)}/`)
.join('?');
}
return _a;
}

View File

@ -1,8 +1,9 @@
// export * from 'domvm/dist/dev/domvm.dev.js'; // export * from 'domvm/dist/dev/domvm.dev.js';
export * from 'domvm/dist/mini/domvm.mini.js'; export * from 'domvm/dist/mini/domvm.mini.js';
import { deepAssign } from './conversion.js';
export function subscribeToRender(vm, subscribables, subscriptions) { export function subscribeToRender(vm, subscribables, subscriptions) {
const redraw = () => vm.redraw(); const redraw = (...args) => vm.redraw();
const subList = subscribables.map(s => s.subscribe(redraw)).concat(subscriptions); const subList = subscribables.map(s => s.subscribe(redraw)).concat(subscriptions);
vm.config({ hooks: { willUnmount: () => subList.forEach(s => s()) } }); vm.config({ hooks: { willUnmount: () => subList.forEach(s => s()) } });
@ -12,10 +13,16 @@ export function patchRefStyle(ref, style, evt, node, vm) {
vm.refs[ref].patch({ style }); vm.refs[ref].patch({ style });
} }
export function patchRefStyleMap(refStylemap, ...args) { export function patchRefStyleMap(refStyleMap, ...args) {
Object.entries(refStylemap).forEach(([r, s]) => patchRefStyle(r, s, ...args)); Object.entries(refStyleMap).forEach(([r, s]) => patchRefStyle(r, s, ...args));
} }
export function patchNodeStyle(style, evt, node) { export function patchNodeStyle(style, evt, node) {
node.patch({ style }); node.patch({ style });
} }
export function changeElementStateMap(refStateMap, evt, node, vm) {
Object.entries(refStateMap).forEach(([r, state]) => {
deepAssign(vm.refs[ref]._data, state);
});
}