ImageGrid/ui (gen4)/features/ui-partial-ribbons-vdom.js
Alex A. Naanou fa1b7312ba still experementing + bugfix...
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
2017-04-28 22:38:31 +03:00

530 lines
14 KiB
JavaScript
Executable File

/**********************************************************************
*
*
*
**********************************************************************/
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
(function(require){ var module={} // make module AMD/node compatible...
/*********************************************************************/
var vdom = require('ext-lib/virtual-dom')
var object = require('lib/object')
var actions = require('lib/actions')
var features = require('lib/features')
var core = require('features/core')
/*********************************************************************/
// XXX EXPERIMENT: use virtual-dom to do ribbon updates...
// - create and maintain a full ribbon view from .ribbon-set and down...
// - sync with dom only when needed...
// - on direct edits (use .update() / .reload() ???)
// - on .updateRibbon(..) -- lazily and when needed...
// - see if we can offload the vdom logic to a worker...
// XXX using virtual-dom...
// - make the below functions into methods...
// - add .sync() to sync-up the DOM with virtual dom...
// ...this would lead to .updateRibbon(..) to only need to
// figure out when to call .sync()
// XXX Q: should this be a special imagegrid/ribbons.js implementation
// or a different level API??
// ...maybe: imagegrid/ribbons-vdom.js as a completely standalone
// module that would be mixed with imagegrid/ribbons.js -- sounds
// a bit too complicated, overkill??
// XXX Q: how should we handle "sync" stuff???
// things like toggling marks or rotating an image...
//
//
//---------------------------------------------------------------------
// XXX DEBUG: remove when not needed...
window.vdom = vdom
//---------------------------------------------------------------------
// attribute hooks...
function GID(value){
this.value = JSON.stringify(value)
.replace(/^"(.*)"$/g, '$1') }
GID.prototype.hook = function(elem, prop){
elem.setAttribute(prop, this.value) }
function VALUE(value){
this.value = value || '' }
VALUE.prototype.hook = function(elem, prop){
this.value != ''
&& elem.setAttribute(prop, this.value) }
function PREVIEW(ig, gid, url){
this.ig = ig
this.gid = gid
this.url = url
}
PREVIEW.prototype.hook = function(elem, prop){
this.ig.ribbons._loadImagePreviewURL(elem, this.url)
}
//---------------------------------------------------------------------
var VirtualDOMRibbonsClassPrototype = {
// XXX ???
}
var VirtualDOMRibbonsPrototype = {
dom: null,
vdom: null,
// XXX this is a circular ref -- I do not like it...
imagegrid: null,
// XXX ???
count: null,
scale: null,
target: null,
// Format:
// {
// count: <count>,
//
// scale: <scale>,
//
// top: <offset>,
// ribbons: {
// <gid>: <offset>,
// ...
// },
// }
state: null,
// XXX the complete set of data this needs to render a state:
// Big stuff:
// - ribbon order, content (.data.ribbons and .data.ribbon_order)
// Small stuff:
// - current (.current)
// - vertical offset (.centerRibbon(..))
// - horizontal offset per ribbon (.centerImage(..))
// - marks (.__image_updaters and API)
// constructors...
// XXX should these be here or be stateless and in VirtualDOMRibbonsClassPrototype???
makeView: function(state){
state = state || {}
var that = this
var ig = this.imagegrid
var target = state.target || ig.current
this.state = this.state || {}
var count = state.count = state.count
|| ig.screenwidth * (ig.config['ribbon-size-screens'] || 9)
var s = state.scale = state.scale
|| ig.scale
var top = state.top = state.top
|| this.state.top
|| ig.ribbons.getRibbonLocator().transform('y')
var data = ig.data
var images = ig.images
var ribbons = data.ribbon_order
.map(function(gid){
return that.makeRibbon(gid, count, state) })
return vdom.h('div.ribbon-set', {
key: 'ribbon-set',
style: {
transform: 'scale('+ s +', '+ s +')',
}
}, [
vdom.h('div.ribbon-locator', {
key: 'ribbon-locator',
style: {
// XXX should this be in vh???
transform: 'translate3d(0px, '+ top +'px, 0px)',
},
},
ribbons)
])
},
// XXX calc offset (x)...
// XXX should we setup handlers here???
makeRibbon: function(gid, count, state){
state = state || {}
var that = this
var ig = this.imagegrid
var data = ig.data
var images = ig.images
var base = data.base == gid ? '.base' : ''
var imgs = []
this.state = this.state || {}
this.state.ribbons = this.state.ribbons || {}
var x = this.state.ribbons[gid] =
(state.ribbons && state.ribbons[gid])
|| this.state.ribbons[gid]
// XXX calculate new offset...
|| parseFloat(ig.ribbons.getRibbon(gid).transform('x'))
data.getImages(gid, count, 'total')
.forEach(function(gid){
imgs.push(that.makeImage(gid))
that.makeImageMarks(gid)
.forEach(function(mark){
imgs.push(mark) })
})
return vdom.h('div.ribbon'+base, {
key: 'ribbon-'+gid,
// XXX events, hammer, ...???
attributes: {
gid: JSON.stringify(gid)
.replace(/^"(.*)"$/g, '$1'),
},
style: {
transform: 'translate3d('+ x +'vmin, 0px, 0px)',
},
},
imgs)
},
// NOTE: at this point this does not account for previews at all...
makeImage: function(gid, size){
var ig = this.imagegrid
size = this.state.tile_size = size
|| this.state.tile_size
|| ig.ribbons.getVisibleImageSize('max')
var data = this.imagegrid.data
var images = this.imagegrid.images || {}
var current = data.current == gid ? '.current' : ''
var image = images[gid] || {}
var seen = []
while(image.type == 'group'){
// error, recursive group...
if(seen.indexOf(image.id) >= 0){
image = images.IMAGE_DATA
console.error('Recursive group:', gid)
break
}
seen.push(image.id)
image = that.images[image.cover]
}
var url = ig.images.getBestPreview(gid, size, image, true).url
return vdom.h('div.image'+current, {
key: 'image-'+gid,
attributes: {
gid: JSON.stringify(gid)
.replace(/^"(.*)"$/g, '$1'),
orientation: image.orientation,
flipped: image.flipped,
//'preview-width': w,
//'preview-height': h,
},
style: {
backgroundImage: 'url("'+ url +'")',
}
})
},
// XXX get marks...
makeImageMarks: function(gid){
var that = this
var marks = []
var tags = this.imagegrid.data.getTags(gid)
// XXX STUB: make this extensible...
tags.indexOf('bookmark') >= 0
&& marks.push('bookmark')
tags.indexOf('selected') >= 0
&& marks.push('selected')
return marks
.map(function(type){
return that.makeImageMark(gid, type) })
},
makeImageMark: function(gid, type){
return vdom.h('div.mark.'+(type || ''), {
key: 'mark-'+gid,
attributes: {
gid: JSON.stringify(gid)
.replace(/^"(.*)"$/g, '$1'),
},
})
},
// XXX add ability to hook in things like current image marker...
// XXX update .state...
update: function(){
},
// NOTE: virtual-dom architecture is designed around a fast-render-on-demand
// concept, so we build the state on demand...
sync: function(){
var dom = this.dom = this.dom || this.imagegrid.ribbons.getRibbonSet()
// build initial state...
if(this.vdom == null){
var n = this.vdom = this.makeView(this.state || {})
var v = vdom.create(n)
dom.replaceWith(v)
this.dom = v
// patch state...
} else {
var n = this.makeView(this.state || {})
vdom.patch(dom, vdom.diff(this.vdom, n))
this.vdom = n
}
return this
},
__init__: function(imagegrid){
this.imagegrid = imagegrid
},
}
var VirtualDOMRibbons =
module.VirtualDOMRibbons =
object.makeConstructor('VirtualDOMRibbons',
VirtualDOMRibbonsClassPrototype,
VirtualDOMRibbonsPrototype)
/*********************************************************************/
var PartialRibbonsActions = actions.Actions({
config: {
// Number of screen widths to load...
'ribbon-size-screens': 7,
// Amount of screen widths to keep around the current image...
'ribbon-update-threshold': 1.2,
// Oversize multiplier limit when we resize the ribbon down...
'ribbon-resize-threshold': 2,
// Sets size of ribbons in single image mode...
'ribbons-resize-single-image': 21,
// can be:
// 'hybrid'
// 'resize'
'ribbons-in-place-update-mode': 'resize',
'ribbons-in-place-update-timeout': 100,
// XXX
'ribbon-update-timeout': 120,
},
get virtualdom(){
return (this.__virtual_dom = this.__virtual_dom || VirtualDOMRibbons(this)) },
// XXX
centerImage: [
function(target, align, offset, scale){
}],
centerRibbon: [
function(target){
}],
updateRibbon: ['- Interface/Update partial ribbon size',
function(target, w, size, threshold){
target = target instanceof jQuery
? this.ribbons.getElemGID(target)
// NOTE: data.getImage(..) can return null at start or end
// of ribbon, thus we need to account for this...
: (this.data.getImage(target)
|| this.data.getImage(target, 'after'))
w = w || this.screenwidth
// get config data and normalize...
size = (size
|| this.config['ribbon-size-screens']
|| 9) * w
threshold = threshold == 0 ? threshold
: (threshold
|| this.config['ribbon-resize-threshold']
|| 2)
var update_threshold = (this.config['ribbon-update-threshold'] || 2) * w
var data = this.data
var ribbons = this.ribbons
var t = Date.now()
this.__last_ribbon_update = this.__last_ribbon_update || t
var timeout = this.config['ribbons-in-place-update-timeout']
var update_timeout = this.config['ribbon-update-timeout']
// localize transition prevention...
// NOTE: we can't get ribbon via target directly here as
// the target might not be loaded...
var r_gid = data.getRibbon(target)
if(r_gid == null){
return
}
// NOTE: for the initial load this may be empty...
var r = ribbons.getRibbon(r_gid)
// next/prev loaded...
var img = this.ribbons.getImage(target)
var nl = img.nextAll('.image:not(.clone)').length
var pl = img.prevAll('.image:not(.clone)').length
var loaded = nl + pl + 1
// next/prev available...
// NOTE: we do not include target in counts...
var gids = this.data.getImages(target, size, 'total')
var na = gids.slice(gids.indexOf(target)+1).length
var pa = gids.slice(0, gids.indexOf(target)).length
// full resize...
if(threshold == 0
// ribbon not loaded...
|| img.length == 0
// ribbon shorter than we expect...
|| (loaded < size && na + pa > loaded)
// ribbon too long...
|| loaded > size * threshold
// passed hard threshold -- too close to edge...
|| (nl < w && na > nl) || (pl < w && pa > pl)){
//console.log('RESIZE (sync)')
this.resizeRibbon(target, size)
// more complex cases...
// passed threshold on the right...
} else if((nl < update_threshold && na > nl)
// passed threshold on the left...
|| (pl < update_threshold && pa > pl)
// loaded more than we need by threshold...
|| nl + pl + 1 > size + update_threshold){
// resize...
if(this.config['ribbons-in-place-update-mode'] == 'resize'
// no ribbon loaded...
|| r.length == 0
// only if we are going slow...
|| (timeout != null
&& (t - this.__last_ribbon_update > timeout))
// full screen...
|| (this.toggleSingleImage
&& this.toggleSingleImage('?') == 'on')){
return function(){
var that = this
// sync update...
if(update_timeout == null){
//console.log('RESIZE (post)', t-this.__last_ribbon_update)
this.resizeRibbon(target, size)
// async update...
} else {
this.__update_timeout
&& clearTimeout(this.__update_timeout)
this.__update_timeout = setTimeout(function(){
//console.log('RESIZE (timeout)', t-this.__last_ribbon_update)
delete that.__update_timeout
that.resizeRibbon(target, size)
}, update_timeout)
}
}
// in-place update...
} else {
//console.log('UPDATE', t - this.__last_ribbon_update)
var c = gids.indexOf(data.getImage('current', r_gid))
var t = gids.indexOf(target)
ribbons
.preventTransitions(r)
.updateRibbonInPlace(gids, r_gid, target)
.restoreTransitions(r, true)
}
}
this.__last_ribbon_update = t
}],
})
var PartialRibbons =
module.PartialRibbons = core.ImageGridFeatures.Feature({
title: '',
doc: '',
priority: 'high',
tag: 'ui-partial-ribbons-vdom',
exclusive: ['ui-partial-ribbons'],
depends: [
'ui',
],
suggested: [
'ui-partial-ribbons-precache',
],
actions: PartialRibbonsActions,
handlers: [
['start',
function(){
console.warn(
'EXPERIMENTAL: starting virtual-dom version of partial ribbons...') }],
['focusImage.pre centerImage.pre',
function(target, list){
// NOTE: we have to do this as we are called BEFORE the
// actual focus change happens...
// XXX is there a better way to do this???
target = list != null ? target = this.data.getImage(target, list) : target
this.updateRibbon(target)
}],
['resizing.post',
function(_, unit, size){
// keep constant size in single image...
if(this.toggleSingleImage && this.toggleSingleImage('?') == 'on'){
this.updateRibbon(
'current',
this.config['ribbons-resize-single-image'] || 13)
} else if(unit == 'scale'){
this.updateRibbon('current', this.screenwidth / size || 1)
} else if(unit == 'screenwidth'){
this.updateRibbon('current', size || 1)
} else if(unit == 'screenheight'){
size = size || 1
// convert target height in ribbons to width in images...
// NOTE: this does not account for compensation that
// .updateRibbon(..) makes for fitting whole image
// counts, this is a small enough error so as not
// to waste time on...
var s = this.ribbons.scale()
var h = this.ribbons.getScreenHeightRibbons()
var w = this.ribbons.getScreenWidthImages()
var nw = w / (h/size)
this.updateRibbon('current', nw)
}
}],
],
})
/**********************************************************************
* vim:set ts=4 sw=4 : */ return module })