ImageGrid/ui (gen4)/features/ui-virtual-dom.js
2018-11-08 21:10:58 +03:00

515 lines
13 KiB
JavaScript
Executable File

/**********************************************************************
*
*
*
**********************************************************************/
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
(function(require){ var module={} // make module AMD/node compatible...
/*********************************************************************/
if(typeof(window) == 'undefined'){
return
}
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')
var ribbons = require('imagegrid/ribbons')
/*********************************************************************/
// TODO:
// - "dry run test" -- empty render feature that if use would not break
// anything but not draw anything either... (use this as template)
// - virtual-dom feature
// - preact / react feature
//
//
//
// - take care of DOM construction and update...
// - alignment is done via .centerRibbon(..) / .centerImage(..)
// - preview updates (XXX)
// - update onload (a-la .ribbons._loadImagePreviewURL(..))
//
var VirtualDOMRibbonsClassPrototype = {
// XXX
}
VirtualDOMRibbonsClassPrototype.__proto__ = ribbons.BaseRibbons.prototype.__proto__
// XXX make this ribbons.BaseRibbons compatible....
var VirtualDOMRibbonsPrototype = {
// XXX this is a circular ref -- I do not like it...
parent: null,
dom: null,
vdom: null,
// Format:
// {
// count: <count>,
//
// scale: <scale>,
//
// top: <offset>,
//
// ribbons: {
// <gid>: <offset>,
// ...
// },
// }
state: null,
// constructors...
makeView: function(state, initial){
state = state || {}
var that = this
var ig = this.parent
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 data = ig.data
var images = ig.images
var ribbons = data.ribbon_order
.map(function(gid){
return that.makeRibbon(gid, target, count, state, initial) })
return vdom.h('div.ribbon-set',
{
//key: 'ribbon-set',
style: {
transform: 'scale('+ s +', '+ s +')',
}
}, [
// current image indicator...
vdom.h('div.current-marker'),
// ribbon locator...
vdom.h('div.ribbon-locator',
{
//key: 'ribbon-locator',
},
ribbons),
])
},
// XXX setup handlers (???)
// XXX STUB: make aligning more extensible... (???)
makeRibbon: function(gid, target, count, state, initial){
state = state || {}
var that = this
var ig = this.parent
var current = ig.current
target = target || state.target || current
var size = this.state.tile_size = state.tile_size
|| this.state.tile_size
|| this.getVisibleImageSize('max')
var scale = state.scale = state.scale
|| ig.scale
var data = ig.data
var images = ig.images
// XXX
var ribbons = ig.ribbons
var base = data.base == gid ? '.base' : ''
var imgs = []
this.state = this.state || {}
//this.state.ribbons = this.state.ribbons || {}
// XXX
var size = this.state.tile_size =
this.state.tile_size
|| this.getVisibleImageSize('max')
// calculate offset...
// XXX this accounts for only one offset mode...
// ...make this extensible...
// XXX
var vsize = this.px2vmin(size / scale)
var ref = data.getImage(target, 'before', gid)
var offset = ref == target ? vsize / 2
: ref != null ? vsize
: 0
ref = ref || data.getImage(target, 'after', gid)
// build the images...
//var gids = data.getImages(gid, count, 'total')
var gids = data.getImages(ref, count, 'total')
gids
.forEach(function(gid){
// image...
imgs.push(that.makeImage(gid, size))
// marks...
that.makeImageMarks(gid)
.forEach(function(mark){ imgs.push(mark) })
})
// XXX not sure about this...
var style = initial ? { transform: 'translate3d(120vw, 0, 0)' } : {}
return vdom.h('div.ribbon'+base, {
//key: 'ribbon-'+gid,
// XXX events, hammer, ...???
attributes: {
gid: JSON.stringify(gid)
.replace(/^"(.*)"$/g, '$1'),
},
style: style,
},
imgs)
},
// XXX setup image handlers...
// XXX update image previews...
// XXX update image proportions for rotated images... (???)
makeImage: function(gid, size){
var ig = this.parent
//size = this.state.tile_size = size
size = size
|| this.state.tile_size
|| this.getVisibleImageSize('max')
var data = this.parent.data
var images = this.parent.images || {}
var current = data.current == gid ? '.current' : ''
// resolve group preview cover...
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, {
// XXX BUG:
// - setting this makes the images some times not change previews...
// - removing this breaks .current class setting...
key: 'image-'+gid,
attributes: {
gid: JSON.stringify(gid)
.replace(/^"(.*)"$/g, '$1'),
orientation: image.orientation,
flipped: image.flipped,
// XXX preview size -- get this onload from image...
//'preview-width': ..,
//'preview-height': ..,
},
style: {
// XXX need to update this onload if changing preview
// of same image...
backgroundImage: 'url("'+ url +'")',
}
})
},
// XXX STUB: make marks handling extensible... (???)
makeImageMarks: function(gid){
var that = this
var marks = []
var tags = this.parent.data.getTags(gid)
// XXX STUB: make this extensible...
tags.indexOf('bookmark') >= 0
&& marks.push('bookmark')
tags.indexOf('marked') >= 0
&& marks.push('marked')
return marks
.map(function(type){
return vdom.h('div.mark.'+(type || ''), {
key: 'mark-'+type+'-'+gid,
attributes: {
gid: JSON.stringify(gid)
.replace(/^"(.*)"$/g, '$1'),
},
})
})
},
// XXX add ability to hook in things like current image marker...
// XXX these need .getImage(..) / .getRibbon(..) / .getRibbonLocator(..)
centerRibbon: function(target){
var ribbon = this.getRibbon(target)
var locator = this.getRibbonLocator()
if(locator.length != 0 && ribbon.length != 0){
var t = ribbon[0].offsetTop
var h = ribbon[0].offsetHeight
locator.transform({ x: 0, y: this.px2vh(-(t + h/2)) + 'vh', z: 0 })
}
return this
},
centerImage: function(target, mode){
target = this.getImage(target)
var ribbon = this.getRibbon(target)
if(ribbon.length != 0){
var l = target[0].offsetLeft
var w = target[0].offsetWidth
var image_offset = mode == 'before' ? 0
: mode == 'after' ? w
: w/2
ribbon.transform({x: -this.px2vmin(l + image_offset) + 'vmin', y: 0, z: 0})
}
return this
},
scale: function(scale){
if(scale){
this.state.scale = scale
this.sync()
} else {
return this.parent.scale
}
},
// XXX not sure how to proceed with these...
setImageHandler: function(evt, handler){
},
setRibbonHandler: function(evt, handler){
},
clear: function(){
this.dom
&& this.dom.remove()
delete this.state
delete this.dom
delete this.vdom
return this
},
// NOTE: virtual-dom architecture is designed around a fast-render-on-demand
// concept, so we build the state on demand...
// XXX get scale from config on initial load...
sync: function(target, size){
var dom = this.dom
var state = this.state ? Object.create(this.state) : {}
target && (state.target = target)
size && (state.count = size)
// build initial state...
if(this.vdom == null){
var n = this.vdom = this.makeView(state, true)
var v = vdom.create(n)
this.parent.dom.append(v)
this.dom = v
// patch state...
} else {
var n = this.makeView(state)
var diff = vdom.diff(this.vdom, n)
vdom.patch(dom, diff)
this.vdom = n
}
return this
},
// XXX should this do a full or partial .clear()???
// XXX BUG: current image indicator resets but does not get shown...
reset: function(){
delete this.dom
delete this.vdom
if(this.state){
delete this.state.tile_size
}
return this
.sync()
},
__init__: function(parent){
this.parent = parent || this.parent
},
}
VirtualDOMRibbonsPrototype.__proto__ = ribbons.BaseRibbons.prototype
var VirtualDOMRibbons =
module.VirtualDOMRibbons =
object.makeConstructor('VirtualDOMRibbons',
VirtualDOMRibbonsClassPrototype,
VirtualDOMRibbonsPrototype)
//---------------------------------------------------------------------
var VirtualDomActions = actions.Actions({
get dom(){
return this.__dom },
set dom(value){
this.__dom = value},
// XXX setup .ribbons...
load: [
function(data){
return function(){
// recycle the viewer if one is not given specifically...
var viewer = data.viewer
viewer = viewer == null ? this.dom : viewer
if(this.dom == null){
this.dom = viewer
this.ribbons = new VirtualDOMRibbons()
} else {
this.ribbons.clear()
}
this.reload()
}
}],
reload: ['Interface/Reload viewer',
function(){
this.ribbons.reset()
this.focusImage()
}],
// XXX this ignores it's args...
refresh: ['Interface/Refresh images without reloading',
function(gids, scale){
this.ribbons.sync()
this.focusImage()
}],
// XXX
updateRibbon: ['- Interface/Update partial ribbon size',
function(target, w, size, threshold){
target = target instanceof jQuery
? this.ribbons.elemGID(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
// XXX DEBUG
//size = 5
// XXX for some reason this does not set the .current class
// on the right image...
this.ribbons.sync(target, size)
// XXX HACK: this fixes a bug in virtual-dom where .current
// is not synced correctly...
// ...one theory I have is that we change the class
// manually, dom gets diffed and no change is detected
// then the object gets recycled and the .current class
// ends up on a different element...
this.ribbons.focusImage(target)
this.centerViewer(target)
}],
})
var VirtualDom =
module.VirtualDom = core.ImageGridFeatures.Feature({
title: '',
doc: '',
tag: 'ui-vdom-render',
exclusive: ['ui-render'],
depends: [
// XXX
],
actions: VirtualDomActions,
handlers: [
['clear',
function(){ this.ribbons.clear() }],
['fitImage toggleSingleImage',
function(){ delete this.ribbons.state.tile_size }],
// XXX account for fast navigation...
['focusImage.pre',
function(target){
var img = this.ribbons.getImage(target)
// in-place update...
if(img.length > 0){
// XXX need to account for running out of images and
// not only on the current ribbon...
if(!this.__partial_ribbon_update){
this.__partial_ribbon_update = setTimeout((function(){
delete this.__partial_ribbon_update
this.ribbons.preventTransitions()
this
.updateRibbon(this.current)
// NOTE: we are doing this manually because we
// are running after the handler is done
// thus missing the base call...
.alignRibbons(null, null, true)
this.ribbons.restoreTransitions()
}).bind(this), 150)
}
// long-jump...
} else {
if(this.__partial_ribbon_update){
clearTimeout(this.__partial_ribbon_update)
delete this.__partial_ribbon_update
}
this.updateRibbon(target)
}
}],
// marks...
[[
'toggleMark',
'toggleBookmark',
//], function(){ this.updateRibbon() }],
], function(){ this.ribbons.sync() }],
],
})
/**********************************************************************
* vim:set ts=4 sw=4 : */ return module })