Add image selection

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

View File

@ -16,7 +16,7 @@
"domvm": "~3.2.1",
"exif-parser": "~0.1.9",
"extract-text-webpack-plugin": "^3.0.2",
"frptools": "2.1.0",
"frptools": "2.2.0",
"pica": "~2.0.8",
"pouchdb-adapter-http": "~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 { pouchDocArrayHash, pouchDocHash } from '../utils/conversion.js';
import { pouchDocArrayHash, pouchDocHash, hashSet, extractID } from '../utils/conversion.js';
import { AlbumTemplate } from './components/albumTemplate.js';
import { injectStyle, styled } from '../services/style.js';
@ -17,7 +23,11 @@ export function uploadImages(evt, files) {
export function AllImagesView(vm, params, key, opts) {
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(
{
@ -26,7 +36,7 @@ export function AllImagesView(vm, params, key, opts) {
true
).then(la => {
opts.appbar.renderButtons(renderAppBarButtons);
subscribeToRender(vm, [images], [la.subscribe(images)]);
subscribeToRender(vm, [images], [la.subscribe(res => images.splice(0, images.length, ...res))]);
});
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) {
ImageType.delete(i._id);
function nodeParentWithType(node, type) {
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() {
const albumName = prompt('Album Name');
if (albumName && albumName.trim()) {
const a = new AlbumType({
title: albumName.trim(),
count: 0
});
a.save();
function toggleSelect(evt, node, vm) {
const imageNode = nodeParentWithType(node, 'image');
const id = imageNode.data._id;
if (selectedIds.has(id)) {
selectedIds.delete(id);
} else {
selectedIds.add(id);
}
}
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 AlbumTemplate({
return el(
'.eventSnarfer',
{
onclick: {
'.photoSelect .icon svg path': toggleSelect,
'.photoSelect .icon': toggleSelect,
'.albumSelectButton .icon': toggleAll,
'.albumSelectButton .icon svg path': toggleAll
}
},
[
AlbumTemplate({
title: 'Test',
id: 1,
photos: images()
});
photos: images,
selectedIds,
mode: mode()
})
]
);
};
}

View File

@ -1,67 +1,78 @@
import { prop } from 'frptools';
import {
defineView as vw,
defineElement as el,
patchRefStyleMap,
patchNodeStyle
patchNodeStyle,
subscribeToRender
} from '../../utils/domvm.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 { 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 photoBackgroundRef = `pBkd${doc._id}`;
const hover = prop(false);
const hoverSelectButton = prop(false);
subscribeToRender(vm, [hover, hoverSelectButton]);
return function render(vm, { isSelected, selectMode }) {
return photoContainer(
{
onmouseenter: [
patchRefStyleMap,
{ [photoSelectButtonRef]: 'opacity: 0.7;', [photoBackgroundRef]: 'opacity: 0.7;' }
],
onmouseleave: [
patchRefStyleMap,
{ [photoSelectButtonRef]: 'opacity: 0;', [photoBackgroundRef]: 'opacity: 0;' }
]
class: 'photoContainer',
onmouseenter: [hover, true],
onmouseleave: [hover, false],
css: {
cursor: selectMode ? 'pointer' : 'zoom-in'
}
},
[
vw(AttachmentImageView, doc, doc._hash()),
AttachmentImageView(doc, {
css: {
transform: isSelected ? 'translateZ(-50px)' : null
}
}),
photoSelectButton(
{
_ref: photoSelectButtonRef,
_data: doc,
class: 'photoSelect',
css: {
// backgroundColor: isSelected ? 'white' : 'transparent',
// opacity: isSelected ? 1 : selectMode || _imageHover ? 0.7 : 0,
backgroundColor: isSelected ? 'white' : 'transparent',
opacity: isSelected || hoverSelectButton() ? 1 : selectMode || hover() ? 0.7 : 0
},
onmouseenter: [patchNodeStyle, 'opacity: 1;'],
onmouseleave: [patchNodeStyle, 'opacity: 0.7;']
onmouseenter: [hoverSelectButton, true],
onmouseleave: [hoverSelectButton, false]
},
[
Icon({
name: 'check_circle',
name: selectMode && !isSelected ? 'circle_o' : 'check_circle',
size: 0.75,
fill: '#fff' // isSelected ? '#00C800' : '#fff'
fill: isSelected ? '#00C800' : '#fff'
})
]
),
photoBackdrop({
_ref: photoBackgroundRef,
css: {
// transform: isSelected ? 'translateZ(-50px)' : null,
// opacity: selectMode || _imageHover ? 0.7 : 0,
transform: isSelected ? 'translateZ(-50px)' : null,
opacity: selectMode || hover() ? 0.7 : 0
}
})
]
);
};
}
const IMAGE_MARGIN = 2;
const CSS_FULL_SIZE = {
width: '100%',
height: '100%'
};
const photoContainer = styled({
position: 'relative',
perspective: '1000px',
@ -70,21 +81,18 @@ const photoContainer = styled({
cursor: 'zoom-in'
});
const image = styled('img', CSS_FULL_SIZE, {
const image = styled('img', CSS_FULL_SIZE, DEFAULT_TRANSITION, {
position: 'absolute',
top: 0,
left: 0,
transition: 'transform .135s cubic-bezier(0.0,0.0,0.2,1)'
left: 0
});
const photoSelectButton = styled({
const photoSelectButton = styled(DEFAULT_TRANSITION, {
position: 'absolute',
top: '4%',
left: '4%',
zIndex: 2,
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%',
padding: '2px',
backgroundColor: 'transparent',
@ -92,11 +100,10 @@ const photoSelectButton = styled({
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',
left: '0px',
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)'
});

View File

@ -5,13 +5,26 @@ import {
patchNodeStyle
} from '../../utils/domvm.js';
import { injectStyle, styled } from '../../services/style.js';
import { DEFAULT_TRANSITION } from '../styles.js';
import { Icon } from './icon.js';
import { AlbumPhotoTemplate } from './albumPhotoTemplate.js';
export function AlbumTemplate(params) {
const { id, title, photos } = params;
const { id, title, photos, selectedIds, mode } = params;
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(
{
@ -25,12 +38,16 @@ export function AlbumTemplate(params) {
{
_ref: albumSelectButtonRef,
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 })]
)
]),
albumContent(photos.map(AlbumPhotoTemplate))
albumContent(photos.map(photoMap))
]
);
}
@ -50,10 +67,8 @@ const albumContent = styled({
userSelect: 'none'
});
const albumSelectButton = styled({
const albumSelectButton = styled(DEFAULT_TRANSITION, {
paddingLeft: '0.5em',
cursor: 'pointer',
opacity: 0, // TODO onhover 0.7
transition:
'transform 0.135s cubic-bezier(0, 0, 0.2, 1), opacity 0.135s cubic-bezier(0, 0, 0.2, 1)'
opacity: 0
});

View File

@ -4,55 +4,58 @@ import { defineElement as el } from '../../utils/domvm.js';
import { ImageType } from '../../data/image.js';
import { FileType } from '../../data/file.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 model = prop(params, pouchDocHash);
const id = computed(pouchDocHash, [model]);
const sizes = computed(d => d.sizes, [model]); // always update
const srcMap = new Map();
const blobURL = prop('');
const imageURL = computed((sizes, bURL) => bURL || sizes.thumbnail || sizes.full, [
sizes,
blobURL
]);
model.subscribe(() => {
if (blobURL()) {
URL.revokeObjectURL(blobURL());
blobURL('');
}
});
async function loadImageFromBlob() {
const options = ['thumbnail', 'preview', 'full'].filter(o => sizes().hasOwnProperty(o));
async function loadImageFromBlob(doc, evt, node, vm) {
const { sizes, _id } = doc;
const options = ['thumbnail', 'preview', 'full'].filter(o => sizes.hasOwnProperty(o));
for (let attempt of options) {
try {
const data = await FileType.getFromURL(sizes()[attempt]);
if (blobURL()) {
URL.revokeObjectURL(blobURL());
const data = await FileType.getFromURL(sizes[attempt]);
let src = evt.target.src;
if (src.startsWith('blob:')) {
URL.revokeObjectURL(src);
}
blobURL(URL.createObjectURL(data));
return;
src = URL.createObjectURL(data);
node.patch({ src });
srcMap.set(_id, src);
// node.data = attempt;
break;
} catch (err) {
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] }
},
[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);
}
export function isString(str) {
export function isString(obj) {
return typeof obj === 'string';
}

View File

@ -49,3 +49,13 @@ export const pick = id => doc => doc[id];
export const extractID = pick('_id');
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/mini/domvm.mini.js';
import { deepAssign } from './conversion.js';
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);
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 });
}
export function patchRefStyleMap(refStylemap, ...args) {
Object.entries(refStylemap).forEach(([r, s]) => patchRefStyle(r, s, ...args));
export function patchRefStyleMap(refStyleMap, ...args) {
Object.entries(refStyleMap).forEach(([r, s]) => patchRefStyle(r, s, ...args));
}
export function patchNodeStyle(style, evt, node) {
node.patch({ style });
}
export function changeElementStateMap(refStateMap, evt, node, vm) {
Object.entries(refStateMap).forEach(([r, state]) => {
deepAssign(vm.refs[ref]._data, state);
});
}