diff --git a/ui (gen4)/features/all.js b/ui (gen4)/features/all.js
index 291fd6e0..790713f4 100755
--- a/ui (gen4)/features/all.js
+++ b/ui (gen4)/features/all.js
@@ -23,6 +23,7 @@ require('features/ui')
require('features/ui-partial-ribbons-precache')
require('features/ui-partial-ribbons')
require('features/ui-partial-ribbons-2')
+require('features/ui-partial-ribbons-vdom')
require('features/ui-single-image')
require('features/ui-chrome')
require('features/ui-progress')
diff --git a/ui (gen4)/features/meta.js b/ui (gen4)/features/meta.js
index 16d9a0bc..c59c83d4 100755
--- a/ui (gen4)/features/meta.js
+++ b/ui (gen4)/features/meta.js
@@ -65,7 +65,8 @@ core.ImageGridFeatures.Feature('viewer-testing', [
'ui-single-image',
//'ui-partial-ribbons',
// XXX this still has problems...
- 'ui-partial-ribbons-2',
+ //'ui-partial-ribbons-2',
+ 'ui-partial-ribbons-vdom',
'marks',
'ui-range',
diff --git a/ui (gen4)/features/ui-partial-ribbons-vdom.js b/ui (gen4)/features/ui-partial-ribbons-vdom.js
new file mode 100755
index 00000000..d30b2d35
--- /dev/null
+++ b/ui (gen4)/features/ui-partial-ribbons-vdom.js
@@ -0,0 +1,368 @@
+/**********************************************************************
+*
+*
+*
+**********************************************************************/
+((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 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...
+//
+//
+//---------------------------------------------------------------------
+
+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) }
+
+
+
+//---------------------------------------------------------------------
+
+// XXX get vertical offset and scale...
+var makeView =
+window.makeView =
+function(data, images, count){
+ // XXX
+ var s = 1
+ var x = 0
+
+ var ribbons = data.ribbon_order
+ .map(function(gid){
+ return makeRibbon(gid, data, images, count) })
+
+ return vdom.h('div.ribbon-set', {
+ key: 'ribbon-set',
+ style: {
+ transform: 'scale('+ s +', '+ s +')',
+ }
+ }, [
+ vdom.h('div.ribbon-locator', {
+ key: 'ribbon-locator',
+ style: {
+ transform: 'translate3d(0px, '+ x +'px, 0px)',
+ },
+ },
+ ribbons)
+ ])
+}
+
+
+// XXX calc/get count...
+var makeRibbon =
+window.makeRibbon =
+function(gid, data, images, count){
+ var imgs = []
+
+ data.getImages(gid, count, 'total')
+ .forEach(function(gid){
+ imgs.push(makeImage(gid, data, images))
+ makeImageMarks(gid, data, images)
+ .forEach(function(mark){
+ imgs.push(mark) })
+ })
+ return vdom.h('div.ribbon', {
+ key: 'ribbon-'+gid,
+
+ gid: new GID(gid),
+ },
+ imgs)
+}
+
+
+// XXX handle previews -- hook???
+// NOTE: at this point this does not account for previews at all...
+var makeImage =
+window.makeImage =
+function(gid, data, images){
+ var image = (images || {})[gid] || {}
+ var current = data.current == gid ? '.current' : ''
+
+ gid = JSON.stringify(gid).slice(1, -1)
+
+ return vdom.h('div.image'+current, {
+ key: 'image-'+gid,
+
+ gid: new GID(gid),
+
+ orientation: new VALUE(image.orientation),
+ flipped: new VALUE(image.flipped),
+
+ // XXX preview stuff???
+ })
+}
+
+// XXX
+var makeImageMarks =
+window.makeImageMarks =
+function(gid, data, images){
+ gid = JSON.stringify(gid).slice(1, -1)
+ var marks = []
+
+ // XXX
+
+ return marks
+}
+
+
+
+/*********************************************************************/
+
+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,
+ },
+
+ updateRibbon: ['- Interface/Update partial ribbon size',
+ function(target, w, size, threshold, preload){
+ 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
+ preload = preload === undefined ? true : preload
+ 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...
+ // XXX this is faster than .resizeRibbon(..) but it's not
+ // used unconditionally because I can't get rid of
+ // sync up images being replaced...
+ // ...note that .resizeRibbon(..) is substantially
+ // slower (updates DOM), i.e. introduces a lag, but
+ // the results look OK...
+ // XXX approaches to try:
+ // - wait for images to preload and only then update...
+ // - preload images in part of a ribbon and when ready update...
+ // ...this is like the first but we wait for less images...
+ } 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: [
+ ['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 })
diff --git a/ui (gen4)/index.html b/ui (gen4)/index.html
index 651e588a..8cd1fd71 100755
--- a/ui (gen4)/index.html
+++ b/ui (gen4)/index.html
@@ -54,6 +54,8 @@ if(window.require && window.nw){
+
+