From 213d2b155926efded554e7323473192e64beff63 Mon Sep 17 00:00:00 2001 From: Timothy Farrell Date: Fri, 8 Dec 2017 08:06:03 -0600 Subject: [PATCH] Add image selection --- packages/gallery/package.json | 2 +- packages/gallery/src/interface/allImages.js | 97 +++++++++++--- .../components/albumPhotoTemplate.js | 121 +++++++++--------- .../src/interface/components/albumTemplate.js | 31 +++-- .../interface/components/attachmentImage.js | 93 +++++++------- .../src/interface/components/thumbnail.js | 2 +- packages/gallery/src/interface/styles.js | 11 ++ packages/gallery/src/utils/comparators.js | 2 +- packages/gallery/src/utils/conversion.js | 10 ++ packages/gallery/src/utils/domvm.js | 13 +- 10 files changed, 246 insertions(+), 136 deletions(-) create mode 100644 packages/gallery/src/interface/styles.js diff --git a/packages/gallery/package.json b/packages/gallery/package.json index 5adddfc..9e02f55 100644 --- a/packages/gallery/package.json +++ b/packages/gallery/package.json @@ -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", diff --git a/packages/gallery/src/interface/allImages.js b/packages/gallery/src/interface/allImages.js index 48d3c9d..8a04ae6 100644 --- a/packages/gallery/src/interface/allImages.js +++ b/packages/gallery/src/interface/allImages.js @@ -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({ - title: 'Test', - id: 1, - photos: images() - }); + 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, + selectedIds, + mode: mode() + }) + ] + ); }; } diff --git a/packages/gallery/src/interface/components/albumPhotoTemplate.js b/packages/gallery/src/interface/components/albumPhotoTemplate.js index b0ee90f..fd4667b 100644 --- a/packages/gallery/src/interface/components/albumPhotoTemplate.js +++ b/packages/gallery/src/interface/components/albumPhotoTemplate.js @@ -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); - return photoContainer( - { - onmouseenter: [ - patchRefStyleMap, - { [photoSelectButtonRef]: 'opacity: 0.7;', [photoBackgroundRef]: 'opacity: 0.7;' } - ], - onmouseleave: [ - patchRefStyleMap, - { [photoSelectButtonRef]: 'opacity: 0;', [photoBackgroundRef]: 'opacity: 0;' } - ] - }, - [ - vw(AttachmentImageView, doc, doc._hash()), - photoSelectButton( - { - _ref: photoSelectButtonRef, - css: { - // backgroundColor: isSelected ? 'white' : 'transparent', - // opacity: isSelected ? 1 : selectMode || _imageHover ? 0.7 : 0, - }, - onmouseenter: [patchNodeStyle, 'opacity: 1;'], - onmouseleave: [patchNodeStyle, 'opacity: 0.7;'] - }, - [ - Icon({ - name: 'check_circle', - size: 0.75, - fill: '#fff' // isSelected ? '#00C800' : '#fff' - }) - ] - ), - photoBackdrop({ - _ref: photoBackgroundRef, + subscribeToRender(vm, [hover, hoverSelectButton]); + + return function render(vm, { isSelected, selectMode }) { + return photoContainer( + { + class: 'photoContainer', + onmouseenter: [hover, true], + onmouseleave: [hover, false], css: { - // transform: isSelected ? 'translateZ(-50px)' : null, - // opacity: selectMode || _imageHover ? 0.7 : 0, + cursor: selectMode ? 'pointer' : 'zoom-in' } - }) - ] - ); + }, + [ + AttachmentImageView(doc, { + css: { + transform: isSelected ? 'translateZ(-50px)' : null + } + }), + photoSelectButton( + { + _ref: photoSelectButtonRef, + _data: doc, + class: 'photoSelect', + css: { + backgroundColor: isSelected ? 'white' : 'transparent', + opacity: isSelected || hoverSelectButton() ? 1 : selectMode || hover() ? 0.7 : 0 + }, + onmouseenter: [hoverSelectButton, true], + onmouseleave: [hoverSelectButton, false] + }, + [ + Icon({ + name: selectMode && !isSelected ? 'circle_o' : 'check_circle', + size: 0.75, + fill: isSelected ? '#00C800' : '#fff' + }) + ] + ), + photoBackdrop({ + _ref: photoBackgroundRef, + css: { + 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)' }); diff --git a/packages/gallery/src/interface/components/albumTemplate.js b/packages/gallery/src/interface/components/albumTemplate.js index debe956..07ca01e 100644 --- a/packages/gallery/src/interface/components/albumTemplate.js +++ b/packages/gallery/src/interface/components/albumTemplate.js @@ -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 }); diff --git a/packages/gallery/src/interface/components/attachmentImage.js b/packages/gallery/src/interface/components/attachmentImage.js index e208d7c..6f52675 100644 --- a/packages/gallery/src/interface/components/attachmentImage.js +++ b/packages/gallery/src/interface/components/attachmentImage.js @@ -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 - ]); +async function loadImageFromBlob(doc, evt, node, vm) { + const { sizes, _id } = doc; + const options = ['thumbnail', 'preview', 'full'].filter(o => sizes.hasOwnProperty(o)); - 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) { - try { - const data = await FileType.getFromURL(sizes()[attempt]); - - if (blobURL()) { - URL.revokeObjectURL(blobURL()); - } - blobURL(URL.createObjectURL(data)); - return; - } catch (err) { - continue; + for (let attempt of options) { + try { + const data = await FileType.getFromURL(sizes[attempt]); + let src = evt.target.src; + if (src.startsWith('blob:')) { + URL.revokeObjectURL(src); } + 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); diff --git a/packages/gallery/src/interface/components/thumbnail.js b/packages/gallery/src/interface/components/thumbnail.js index a45491f..536d203 100644 --- a/packages/gallery/src/interface/components/thumbnail.js +++ b/packages/gallery/src/interface/components/thumbnail.js @@ -8,7 +8,7 @@ export function ThumbnailTemplate(doc, remove, key) { { onclick: { img: [remove, doc] } }, - [vw(AttachmentImageView, doc, key)] + [AttachmentImageView(doc)] ) ]); } diff --git a/packages/gallery/src/interface/styles.js b/packages/gallery/src/interface/styles.js new file mode 100644 index 0000000..9331ce2 --- /dev/null +++ b/packages/gallery/src/interface/styles.js @@ -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)' +}; diff --git a/packages/gallery/src/utils/comparators.js b/packages/gallery/src/utils/comparators.js index 7a97155..402d8a7 100644 --- a/packages/gallery/src/utils/comparators.js +++ b/packages/gallery/src/utils/comparators.js @@ -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'; } diff --git a/packages/gallery/src/utils/conversion.js b/packages/gallery/src/utils/conversion.js index 4eee9f6..4a5f65c 100644 --- a/packages/gallery/src/utils/conversion.js +++ b/packages/gallery/src/utils/conversion.js @@ -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; +} diff --git a/packages/gallery/src/utils/domvm.js b/packages/gallery/src/utils/domvm.js index 6eeeed8..471b2d6 100644 --- a/packages/gallery/src/utils/domvm.js +++ b/packages/gallery/src/utils/domvm.js @@ -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); + }); +}