sets the delay before the shadow is removed and the target
// state is restored (default: 200).
//
//
// NOTE: if a previous shadow if the same image exists this will recycle
// the existing shadow and cancel it's removal.
// ...this is useful for when a second consecutive action with
// the same image is issued before the previous has time to
// complete, recycling the shadow will enable a single flowing
// animation for such series of commands.
// Example: several fast consecutive horizontal shifts will result
// in a single shadow "flowing" through the ribbon.
// NOTE: multiple shadows of different images are supported...
// NOTE: the .shadow element is essentially a ribbon.
//
// XXX add duration configuration...
// XXX should we also have a ribbon shadow???
// XXX when this cant find a target it will return an empty function,
// not sure if this is correct...
// XXX should we use transforms instead of css positions???
makeShadow: function(target, animate, delay, start_delay){
delay = delay || 200
start_delay = start_delay || 10
var img = this.getImage(target)
if(img.length == 0){
// XXX is this correct???
return function(){}
}
var gid = this.elemGID(img)
var s = this.scale()
var vo = this.viewer.offset()
var io = img.offset()
// get the shadow if it exists...
var shadow = this.viewer.find('.shadow[gid="'+gid+'"]')
// recycle the shadow...
if(shadow.length > 0){
// cancel previous shadow removal ticket...
var ticket = shadow.attr('ticket') + 1
shadow
// reset ticket...
// NOTE: this is a possible race condition... (XXX)
.attr('ticket', ticket)
// place it over the current image...
.css({
top: io.top - vo.top,
left: io.left - vo.left,
})
// create a new shadow...
} else {
// removal ticket...
var ticket = 0
// make a shadow element...
// ...we need to scale it to the current scale...
var shadow = $('')
.addClass('shadow ribbon clone')
.attr({
gid: gid,
ticket: ticket,
})
.append(
// clone the target into the shadow..
img
.clone()
.addClass('clone')
.removeClass('current')
.attr('gid', null))
.scale(s)
// place it over the current image...
.css({
top: io.top - vo.top,
left: io.left - vo.left,
})
.append(this.getImageMarks(img)
.clone()
.attr('gid', null))
// place in the viewer...
// NOTE: placing the shadow in the viewer is a compromise that
// lets us do simpler positioning
.appendTo(this.viewer)
}
img.addClass('moving')
var that = this
// function to clear the shadow...
return function(){
// remove only the item with the correct ticket...
if(ticket == shadow.attr('ticket')){
var s = that.scale()
var img = that.getImage(gid)
var vo = that.viewer.offset()
var io = img.offset()
if(animate){
if(start_delay){
setTimeout(function(){
shadow.css({
top: io.top - vo.top,
left: io.left - vo.left,
})
}, start_delay)
} else {
shadow.css({
top: io.top - vo.top,
left: io.left - vo.left,
})
}
}
setTimeout(function(){
// remove only the item with the correct ticket...
if(ticket == shadow.attr('ticket')){
img.removeClass('moving')
shadow.remove()
}
}, delay)
}
return img
}
},
// Basic manipulation...
// Place a ribbon...
//
// Append target ribbon:
// .placeRibbon(target)
// -> ribbon
//
// Place target ribbon at position:
// .placeRibbon(target, index)
// .placeRibbon(target, ribbon-gid)
// .placeRibbon(target, ribbon)
// -> ribbon
//
// The ribbon will be placed at the new position shifting the next
// ribbon(s), if present, by one.
//
// Indexes if used, can be negative. Negative indexes are relative
// to the end, e.g. -1 is the same as length-1.
// Placing an element at a negative index will place it AFTER the
// target element, this is in contrast to positive indexes where an
// element is placed before the target. In both of the above cases
// (positive and negative indexes) the resulting target position
// will AT the passed position.
//
// NOTE: negative and positive indexes overflow to 0 and length
// respectively.
// NOTE: both target and position must be .getRibbon(..) compatible.
// NOTE: if target ribbon does not exist a new ribbon will be created.
// NOTE: if position ribbon (gid,ribbon) does not exist or is not
// attached then the target will be appended to the end.
// NOTE: this uses the DOM data for placement, this may differ from
// the actual data.
placeRibbon: function(target, position){
// get create the ribbon...
var ribbon = this.getRibbon(target)
var i = this.getRibbonOrder(ribbon)
ribbon = ribbon.length == 0 ? this.createRibbon(target) : ribbon
var ribbon_set = this.getRibbonLocator(true)
var ribbons = this.viewer.find(RIBBON)
// normalize the position...
if(typeof(position) == typeof(123)){
position = position < 0 ? ribbons.length + position + 1 : position
position = position < 0 ? 0 : position
} else {
position = this.getRibbonOrder(position)
// XXX what do we do if the target does not exist, i.e. p == -1 ????
}
if(i == position){
return ribbon
}
// place the ribbon...
if(ribbons.length == 0 || ribbons.length <= position){
ribbon_set.append(ribbon)
} else if(i == -1 || i > position) {
// XXX need to compensate for offset???
ribbons.eq(position).before(ribbon)
// for placing after need to account for target ribbon removal...
} else if(i < position) {
ribbons.eq(position).after(ribbon)
}
// XXX do we need to update the ribbon here???
return ribbon
},
// Place an image...
//
// Place target at at offset from current position:
// .placeImage(target, offset)
// -> image
//
// Place target at image position:
// .placeImage(target, image)
// .placeImage(target, image, 'before')
// .placeImage(target, image, 'after')
// -> image
//
// Place target at ribbon start/end:
// .placeImage(target, ribbon)
// .placeImage(target, ribbon, 'before')
// .placeImage(target, ribbon, 'after')
// -> image
//
// NOTE: mode defaults to 'before'.
// NOTE: if image gid does not exist it will be created.
// NOTE: if target is a list of gids, this will place the gids in
// the same order as given, not as they were before placing...
//
// XXX is this too complicated???
placeImage: function(target, to, mode){
mode = mode == null ? 'before' : mode
if(this.getRibbonSet().length == 0){
return
}
target = target == null || target.constructor !== Array ? [target] : target
// get or make images...
var that = this
var img = $($(target)
.map(function(_, e){
var i = that.getImage(e)
return (i.length == 0 ? that.createImage(e) : i)[0]
}))
var i = this.getImage(to)
var r = this.getRibbon(to)
// offset on same ribbon...
if(typeof(to) == typeof(123)){
// moving the image to itself...
if(to == 0){
return img
}
var i = to
var images = img[i > 0 ? 'last' : 'first']()
[i > 0 ? 'nextAll' : 'prevAll'](IMAGE)
to = images.length > 0
? images.eq(Math.min(Math.abs(i), images.length)-1)
: img
// append/prepend to ribbon...
} else if(i.length == 0 && r.length > 0 && r.hasClass('ribbon')){
if(mode == 'before'){
r.append(img)
} else {
r.prepend(img)
}
return this.updateImage(img)
// relative to image...
} else {
var i = mode == 'before' ? -1 : 1
to = this.getImage(to)
// moving the image to itself...
if(to[0] == img[0]){
return img
}
var images = to[mode](IMAGE)
}
// place the image...
if(images.length <= i){
to.parents('.ribbon')
.append(img)
// after...
} else if(i > 0){
// XXX this stumbles on non-images...
//to.next(IMAGE)
// XXX is this fast enough??
to.nextAll(IMAGE).first()
.before(img)
// before...
} else {
to
.before(img)
}
// cleanup source ribbons...
this.clearEmptyRibbons()
return this.updateImage(img)
},
// Loading and updating...
// Replace image gid...
//
// XXX should this work for ribbon gids???
replaceGid: function(from, to){
var img = this.getImage(from)
img && img.length > 0
&& this.elemGID(img, to)
return this
},
// XXX is .__image_updaters the right way to go???
updateImageIndicators: function(gid, image){
gid = gid == null ? this.elemGID() : gid
image = image == null ? this.getImage() : $(image)
// collect marks...
image.after(this.getImageMarks(gid))
if(this.__image_updaters != null){
this.__image_updaters.forEach(function(update){
update(gid, image)
})
}
return image
},
_loadImagePreviewURL: function(image, url){
url = util.path2url(url)
// pre-cache and load image...
// NOTE: this will make images load without a blackout...
var img = new Image()
var i = image instanceof jQuery ? image[0] : image
img.onload = function(){
i.style.backgroundImage = 'url("'+ url +'")',
// NOTE: these do not account for rotation...
i.setAttribute('preview-width', img.width)
i.setAttribute('preview-height', img.height)
}
img.src = url
return img
},
// Update image(s)...
//
// Update current image:
// .updateImage()
// -> image
//
// Update specific image:
// .updateImage(gid)
// .updateImage(image)
// -> image
//
// Update all image:
// .updateImage('*')
// -> image
//
// NOTE: this can update collections of images by passing either a
// list of gids, images or a jQuery collection...
//
// If this is set to true image previews will be loaded synchronously...
load_img_sync: false,
//
// XXX this depends on .images...
// ...a good candidate to move to images, but not yet sure...
// XXX things to inline:
// .gid
// .style.backgroundImage
//
// .getImageMarks(..)
//
// .rotateImage(..)
// .flipImage(..)
// ._loadImagePreviewURL(..)
// .correctImageProportionsForRotation(..)
//
// .updateImageIndicators(..)
_updateImage: function(image, gid, size, sync){
image = (image == '*' ? this.viewer.find(IMAGE)
: image == null
|| typeof(image) == typeof('str') ? this.getImage(image)
: $(image))
sync = sync == null ? this.load_img_sync : sync
size = size == null ? this.getVisibleImageSize('max') : size
var that = this
return $(image.map(function(){
var image = this instanceof String
|| typeof(this) == typeof('str')
? that.getImage(this+'')
: $(this)
if(image.length == 0){
return
}
var old_gid = that.elemGID(image)
// same image -- update...
if(old_gid == gid || gid == null){
var gid = old_gid
// reuse for different image -- reconstruct...
} else {
// remove old marks...
if(typeof(old_gid) == typeof('str')){
that.getImageMarks(old_gid).remove()
}
// reset gid...
image
.attr('gid', JSON.stringify(gid)
// this removes the extra quots...
.replace(/^"(.*)"$/g, '$1'))
.css({
// clear the old preview...
'background-image': '',
})
}
// if no images data defined drop out...
if(that.images == null){
return image[0]
}
// get the image data...
var img_data = that.images[gid]
if(img_data == null){
img_data = images.IMAGE_DATA
}
// if we are a group, get the cover...
// NOTE: groups can be nested...
var seen = []
while(img_data.type == 'group'){
// error, recursive group...
if(seen.indexOf(img_data.id) >= 0){
img_data = images.IMAGE_DATA
console.error('Recursive group:', gid)
break
}
seen.push(img_data.id)
img_data = that.images[img_data.cover]
}
// image state...
//that.rotateImage(image, img_data.orientation == null ? 0 : img_data.orientation)
//that.flipImage(image, img_data.flipped == null ? [] : img_data.flipped)
image.attr({
orientation: img_data.orientation == null ? '' : img_data.orientation*1,
flipped: (img_data.flipped == null ? [] : img_data.flipped).join(', '),
})
// preview...
var p_url = that.images.getBestPreview(img_data.id, size, img_data, true).url
// update the preview if it's a new image or...
// XXX this should be pushed as far back as possible...
if(old_gid != gid
// the new preview (p_url) is different to current...
// NOTE: this may not work correctly for relative urls...
|| image.css('background-image').indexOf(util.path2url(p_url)) < 0){
// sync load...
if(sync){
that._loadImagePreviewURL(image, p_url)
// async load...
} else {
// NOTE: storing the url in .data() makes the image load the
// last requested preview and in a case when we manage to
// call updateImage(...) on the same element multiple times
// before the previews get loaded...
// ...setting the data().loading is sync while loading an
// image is not, and if several loads are done in sequence
// there is no guarantee that they will happen in the same
// order as requested...
image.data().loading = p_url
setTimeout(function(){
that._loadImagePreviewURL(image, image.data().loading)
}, 0)
}
}
// NOTE: this only has effect on non-square image blocks...
// XXX this needs the loaded image, thus should be done right
// after preview loading...
// XXX preview loading is async, is this the right
// place for this??
// ...this is also done in .rotateImage(..) above...
that.correctImageProportionsForRotation(image)
// marks and other indicators...
that.updateImageIndicators(gid, image)
return image[0]
}))
},
// XXX add options for images to preload and only then do the update...
updateImage: function(image, gid, size, sync, callback){
var that = this
image = (image == '*' ? this.viewer.find(IMAGE)
: image == null
|| typeof(image) == typeof('str') ? this.getImage(image)
: $(image))
sync = sync == null ? this.load_img_sync : sync
size = size == null ? this.getVisibleImageSize('max') : size
var update = {}
//var marks = {}
// build update data...
image.map(function(_, image){
image = typeof(image) == typeof('str') ?
that.getImage(image)
: $(image)
if(image.length == 0){
return
}
var old_gid = that.elemGID(image)
var data = update[old_gid] = {
image: image,
attrs: {},
style: {},
}
var reset_preview = false
//var will_change = []
// same image -- update...
if(old_gid == gid || gid == null){
// XXX BUG: we are actually ignoring gid...
var gid = old_gid
// reuse for different image -- reconstruct...
} else {
// remove old marks...
if(typeof(old_gid) == typeof('str')){
// XXX
//marks[old_gid] = that.getImageMarks(old_gid)
that.getImageMarks(old_gid).remove()
}
// reset gid...
data.attrs = {
gid: JSON.stringify(gid)
// this removes the extra quots...
.replace(/^"(.*)"$/g, '$1'),
}
reset_preview = true
}
data.gid = gid
// if no images data defined drop out...
if(that.images == null){
return
}
// image data...
var img_data = that.images[gid] || images.IMAGE_DATA
// if we are a group, get the cover...
// NOTE: groups can be nested...
var seen = []
while(img_data.type == 'group'){
// error, recursive group...
if(seen.indexOf(img_data.id) >= 0){
img_data = images.IMAGE_DATA
console.error('Recursive group:', gid)
break
}
seen.push(img_data.id)
img_data = that.images[img_data.cover]
}
// image state...
data.attrs.orientation = img_data.orientation == null ? '' : img_data.orientation*1
data.attrs.flipped = (img_data.flipped == null ? [] : img_data.flipped).join(', ')
//will_change.push('transform')
// stage background image update...
var p_url = that.images.getBestPreview(img_data.id, size, img_data, true).url
if(old_gid != gid
// the new preview (p_url) is different to current...
// NOTE: this may not work correctly for relative urls...
|| image.css('background-image').indexOf(util.path2url(p_url)) < 0){
//will_change.push('background-image')
reset_preview
&& (image[0].style.backgroundImage = '')
// sync...
if(sync){
that._loadImagePreviewURL(image, p_url)
// async...
// NOTE: storing the url in .data() makes the image load the
// last requested preview and in a case when we manage to
// call updateImage(...) on the same element multiple times
// before the previews get loaded...
// ...setting the data().loading is sync while loading an
// image is not, and if several loads are done in sequence
// there is no guarantee that they will happen in the same
// order as requested...
} else {
image.data().loading = p_url
setTimeout(function(){
that._loadImagePreviewURL(image, image.data().loading)
}, 0)
}
}
callback
&& callback.call(that, image, data)
//image[0].style.willChange = will_change.join(', ')
})
// clear marks...
//Object.values(marks)
// .forEach(function(m){ m.remove() })
var W = this.viewer.innerWidth()
var H = this.viewer.innerHeight()
// do the update...
return $(Object.keys(update).map(function(gid){
var data = update[gid]
var img = data.image
var _img = img[0]
var attrs = data.attrs
var css = data.style
attrs && img.attr(attrs)
css && img.css(css)
that.correctImageProportionsForRotation(img, W, H)
that.updateImageIndicators(data.gid, img)
//_img.style.willChange = ''
return _img
}))
},
// Update ribbon content...
//
// This will reuse the images that already exist, thus if updating or
// adding images to an already loaded set this should be very fast.
//
// If reference is given then this will compensate ribbon offset to
// keep the reference image in the same position (XXX ???)
//
// gids must be a list of gids.
//
// ribbons must be .getRibbon(..) compatible.
//
// reference must be .getImage(..) compatible or null to disable
// offset compensation.
//
// NOTE: this will change ribbon size and compensate for it, but this
// will not disable transitions, which at this point is the
// responsibility of the caller...
// NOTE: offset calculation depends on image blocks being square...
// NOTE: the argument force is currently ignored, it serves as a
// place holder for overloading...
//
// XXX this depends on image size being fixed for compensating
// position shift...
// ...a simpler way to go is to check .position().left of the
// reference image before and after the chage and add the delta
// to the offset...
// XXX make this add images in chunks of adjacent images...
// XXX might be a good idea to do the actual adding in requestAnimationFrame(..)
updateRibbon: function(gids, ribbon, reference, force){
var that = this
var place = false
// get/create the ribbon...
var r = this.getRibbon(ribbon)
if(r.length == 0){
place = true
// no such ribbon exists, then create and append it in the end...
// NOTE: this effectively makes the update offline and pushes
// the new ribbon on the dom in one go...
r = this.createRibbon(ribbon)
}
var loaded = r.find(IMAGE)
// compensate for new/removed images...
if(reference != null){
var ref = this.getImage(reference)
// align only if ref is loaded...
if(ref.length > 0){
var gid = this.elemGID(ref)
var W = Math.min(document.body.offsetWidth, document.body.offsetHeight)
var w = this.getVisibleImageSize('width', 1, ref) / W * 100
// calculate offset...
// NOTE: this will not work for non-square images...
var dl = loaded.index(ref) - gids.indexOf(gid)
if(dl != 0){
var x = parseFloat((r.transform('translate3d') || [0])[0]) + w * dl
r.transform({x: x + 'vmin', y: 0, z: 0})
}
}
}
// remove all images that we do not need...
var unloaded = []
var unload_marks = []
loaded = loaded
.filter(function(i, img){
var g = that.elemGID(img)
if(gids.indexOf(g) >= 0){
return true
}
unloaded.push(img)
unload_marks = unload_marks.concat(that.getImageMarks(g).toArray())
return false
})
// detach/remove everything in one go...
$(unloaded)
.detach()
.removeClass('moving current')
// blank out images to prevent wrong image flashing...
.css('background-image', 'none')
// clear marks...
$(unload_marks)
.remove()
var images = []
$(gids).each(function(i, gid){
// support for sparse ribbons...
if(gid == null){
return
}
// get/create image...
// NOTE: as this will get a loaded image if it's loaded in
// a different ribbon this WILL affect that ribbon...
var img = that.getImage(gid)
if(img.length == 0){
img = unloaded.length > 0
// reuse an image we just detached...
? that.elemGID(unloaded.pop(), gid)
// create a new image...
: that.createImage(gid)
}
// see of we are loaded in the right position...
// NOTE: loaded is maintained current later, thus it always
// contains a set of images representative of the ribbon...
var g = loaded.length > i ? that.elemGID(loaded.eq(i)) : null
// check if we need to reattach the image...
if(gid != g){
// append the image to set...
if(loaded.length == 0 || loaded.length <= i){
r.append(img.detach())
// attach the image at i...
} else {
// update the DOM...
loaded.eq(i).before(img.detach())
// update the loaded list...
var l = loaded.index(img)
if(l >= 0){
loaded.splice(l, 1)
}
loaded.splice(i, 0, img)
}
}
images.push(img[0])
})
// XXX this appears to be the bottleneck on large numbers of images...
this.updateImage($(images))
if(place){
this.placeRibbon(r, this.viewer.find(RIBBON).length)
}
return this
},
// NOTE: reference must be both present in the loaded ribbon and in
// the given gids...
updateRibbonInPlace: function(gids, ribbon, reference){
var that = this
var r = this.getRibbon(ribbon)
var loaded = r.find(IMAGE)
gids = gids.slice(0, loaded.length)
// update offset...
if(reference != null){
var ref = this.getImage(reference)
// align only if ref is loaded...
if(ref.length > 0){
var gid = this.elemGID(ref)
var W = Math.min(document.body.offsetWidth, document.body.offsetHeight)
var w = this.getVisibleImageSize('width', 1, ref) / W * 100
// calculate offset...
// NOTE: this will not work for non-square images...
var dl = loaded.index(ref) - gids.indexOf(gid)
if(dl != 0){
var x = parseFloat((r.transform('translate3d') || [0])[0]) + w * dl
r.transform({x: x + 'vmin', y: 0, z: 0})
}
}
}
// update gids...
var unload_marks = []
for(var i = 0; i < gids.length; i++){
var gid = gids[i]
//gids
// .forEach(function(gid, i){
if(gid !== undefined){
var img = loaded.eq(i)
// cleanup marks...
var g = that.elemGID(img)
unload_marks = gids.indexOf(g) < 0 ?
unload_marks.concat(that.getImageMarks(g).toArray())
: unload_marks
// XXX for some reason this is smoother than:
// gid && that.updateImage(img, gid)
gid && that.updateImage(that.elemGID(img, gid))
}
// })
}
$(unload_marks)
.remove()
return this
},
// Resize ribbon...
//
// .resizeRibbon(ribbon, left, right)
// -> ribbons
//
// left/right can be:
// - negative number - the number of images to trim
// - list of gids - the images to add
//
// NOTE: this is a less general but simpler/faster alternative to
// .updateRibbon(..)
// NOTE: this needs the ribbon to exist...
//
// XXX revize offset compensation + cleanup...
// ...at this point offset compensation animates...
resizeRibbon: function(ribbon, left, right, transitions, reference){
ribbon = this.getRibbon(ribbon)
left = left || 0
right = right || 0
reference = this.getImage(reference)
var W = Math.min(document.body.offsetWidth, document.body.offsetHeight)
var w = this.getVisibleImageSize('width', 1, reference)
var that = this
var images = ribbon.find(IMAGE)
var unloaded = $()
// trim right...
if(right < 0){
var marks = []
var unloaded = images.slice(images.length + right)
// remove marks...
.each(function(_, img){
marks = marks.concat(
that.getImageMarks(that.elemGID(img)).toArray())
})
// clear stuff...
$(marks)
.remove()
unloaded
.detach()
.removeClass('moving current')
// blank out images to prevent wrong image flashing
// when reusing...
.css('background-image', 'none')
}
// trim left...
// NOTE: this affects ribbon placement, thus we'll need to compensate...
if(left < 0){
var marks = []
// NOTE: we do not need to append or conserve previous unloaded
// images as we will need them only if we are trimming from
// one side and growing the other...
var unloaded = images.slice(0, -left)
// remove marks...
.each(function(_, img){
marks = marks.concat(
that.getImageMarks(that.elemGID(img)).toArray())
})
// calculate the compensation...
// XXX this assumes that all widths are equal...
// ...we can't calculate image width unless it is attached...
//var l = -left * (reference.outerWidth() / scale)
//var l = -left * w
// clear stuff...
$(marks)
.remove()
requestAnimationFrame(function(){
transitions || that.preventTransitions(ribbon)
var a = images[-left].offsetLeft
unloaded
.detach()
.removeClass('moving current')
// blank out images to prevent wrong image flashing
// when reusing...
.css('background-image', 'none')
// compensate for the offset...
var b = images[-left].offsetLeft
var d = ((a - b) / W) * 100
var x = parseFloat((ribbon.transform('translate3d') || [0])[0]) + d
ribbon.transform({x: x + 'vmin', y: 0, z: 0})
transitions || that.restoreTransitions(ribbon, true)
})
}
// grow right...
if(right.length > 0 || right > 0){
var c = right.length || right
// build set of empty images...
var loading = unloaded.slice(0, c)
while(loading.length < c){
loading.push(that.createImage([''])[0])
}
// update images...
right instanceof Array && right.forEach(function(gid, i){
var img = loading.eq(i)
that.elemGID(img, gid)
// XXX for some reason this does not add indicators...
that.updateImage(img)
})
ribbon.append(loading)
// XXX this is here to update the indicators...
// ...indicators seem to not be attached above...
loading.each(function(_, img){
that.updateImage(img)
})
}
// grow left...
// NOTE: this affects ribbon placement, thus we'll need to compensate...
if(left.length > 0 || left > 0){
var c = left.length || left
// build set of empty images...
var loading = unloaded.slice(0, c)
while(loading.length < c){
loading.push(that.createImage([''])[0])
}
// update images...
left instanceof Array && left.forEach(function(gid, i){
var img = loading.eq(i)
that.elemGID(img, gid)
// XXX for some reason this does not add indicators...
that.updateImage(img)
})
// calculate the compensation...
// XXX this assumes that all widths are equal...
// ...we can't calculate image with unless it is attached...
//var l = c * (reference.outerWidth() / scale)
//var l = c * w
requestAnimationFrame(function(){
transitions || that.preventTransitions(ribbon)
// XXX is this the correct reference item -- can it be deleted above???
var a = images[0].offsetLeft
ribbon.prepend(loading)
// XXX this is here to update the indicators...
// ...indicators seem to not be attached above...
loading.each(function(_, img){
that.updateImage(img)
})
// compensate for the offset...
// XXX is this the correct reference item -- can it be deleted above???
var b = images[0].offsetLeft
var d = ((a - b) / W) * 100
var x = parseFloat((ribbon.transform('translate3d') || [0])[0]) + d
ribbon.transform({x: x + 'vmin', y: 0, z: 0})
transitions || that.restoreTransitions(ribbon, true)
})
}
return this
},
// Update the data in ribbons...
//
// .updateData(data, settings)
// -> ribbons
//
//
// This uses .updateRibbon(..) to load individual ribbons, for
// more info see docs for that.
//
// This uses data.ribbon_order to place the ribbons and data.ribbons
// to place the images.
//
// This uses data.base and data.current to set the base ribbon and
// current image respectively.
//
// All the data fields are optional, but for this to make a change
// at least one must be present.
//
//
// Settings format:
// {
// // if true keep the unchanged ribbons (default: false)
// // NOTE: untouched ribbons are the ones loaded into DOM but
// // not included in any of:
// // - data.ribbon_order
// // - data.ribbons
// // - data.base
// keep_ribbons: bool,
//
// // if true do not update the base ribbon (default: false)
// keep_base: bool,
//
// // if true do not update the current image (default: false)
// keep_current: bool,
//
//
// // a shorthand setting all the above to true (default: false).
// // NOTE: if this is set to true all other settings will be
// // ignored...
// keep_all: bool,
// }
//
// NOTE: this will not clear the ribbons object explicitly.
// NOTE: this will never remove the ribbons included in any of the
// data.base, data.ribbon_order or data.ribbons...
updateData: function(data, settings){
settings = settings == null ? {} : settings
// load the data...
var that = this
// update ribbons -- place images...
if(data.ribbons != null){
// see if we've got a custom ribbon updater...
var updateRibbon = settings.updateRibbon || this.updateRibbon.bind(this)
Object.keys(data.ribbons).forEach(function(gid){
updateRibbon(data.ribbons[gid], gid)
})
}
// place ribbons...
if(data.ribbon_order != null){
data.ribbon_order.forEach(function(gid, i){
that.placeRibbon(gid, i)
})
}
if(!settings.keep_all){
// set base ribbon...
if(!settings.keep_base && data.base != null){
this.setBaseRibbon(data.base)
}
// set base ribbon...
if(!settings.keep_current && data.current != null){
this.focusImage(data.current)
}
// clear the ribbons that did not get updated...
if(!settings.keep_ribbons
&& (data.ribbon_order != null || data.ribbons != null)){
var ribbons = data.ribbon_order != null ? data.ribbon_order.slice()
: data.ribbons != null ? Object.keys(data.ribbons)
: []
that.viewer.find(RIBBON).each(function(){
var r = $(this)
if(ribbons.indexOf(that.elemGID(r)) < 0){
r.remove()
}
})
}
}
return this
},
clearEmptyRibbons: function(){
this.viewer.find(RIBBON).filter(function(_, e){
return $(e).children().length == 0
}).remove()
return this
},
// Clear elements...
//
// Clear all elements:
// .clear()
// .clear('*')
// -> Ribbons
//
// Clear an image or a ribbon by gid:
// .clear(gid)
// -> Ribbons
//
// Clear a set of elements:
// .clear([gid, ...])
// -> Ribbons
//
// Clear ribbon-set -- full rest:
// .clear('full')
// -> Ribbons
// NOTE: this will lose any state stored in the ribbon set, this
// includes vertical align and scaling...
//
//
// NOTE: another way to remove a ribbon or an image just to use
// .getRibbon(..).remove() and .getImage(...).remove() respectivly.
clear: function(gids){
// clear all...
if(gids == 'full' || gids == '*' || gids == null){
this.getRibbonSet().remove()
// clear one or more gids...
} else {
gids = gids.constructor !== Array ? [gids] : gids
var that = this
gids.forEach(function(g){
that.viewer.find('[gid='+JSON.stringify(g)+']').detach()
})
}
return this
},
// Focus image...
//
// Focus image by gid:
// .focusImage(gid)
// -> image
//
// Focus next/prev image relative to current:
// .focusImage('next')
// .focusImage('prev')
// -> image
//
// Focus image at offset from current:
// .focusImage(offset)
// -> image
//
// NOTE: gid must be a .getImage(..) compatible object.
// NOTE: for keyword and offset to work an image must be focused.
// NOTE: overflowing offset will focus first/last image.
focusImage: function(target){
var cur = this.viewer
.find('.current.image')
var next = this.getImage(target)
cur.removeClass('current')
return next.addClass('current')
},
// Set base ribbon...
//
// XXX is this really needed here???
// XXX should this support keywords a-la .focusImage(..)???
setBaseRibbon: function(gid){
this.viewer.find('.base.ribbon').removeClass('base')
return this.getRibbon(gid).addClass('base')
},
// Image manipulation...
// Toggle image mark...
//
// Toggle current image cls mark:
// .toggleImageMark(cls)
// .toggleImageMark(cls, 'toggle')
// -> mark
//
// Set current image cls mark on or off explicitly:
// .toggleImageMark(cls, 'on')
// .toggleImageMark(cls, 'off')
// -> mark
//
// Toggle image cls mark:
// .toggleImageMark(image, cls)
// .toggleImageMark(image, cls, 'toggle')
// -> mark
//
// Set image cls mark on or off explicitly:
// .toggleImageMark(image, cls, 'on')
// .toggleImageMark(image, cls, 'off')
// -> mark
//
// Get image cls mark state:
// .toggleImageMark(cls, '?')
// .toggleImageMark(image, cls, '?')
// -> 'on'
// -> 'off'
// NOTE: this will only test the first image.
//
//
// NOTE: cls can be a list...
// NOTE: this can operate on multiple images...
// NOTE: this will reuse existing marks...
toggleImageMark: function(image, cls, action){
var that = this
if(cls == null || ['toggle', 'on', 'off', '?'].indexOf(cls) >= 0 ){
action = cls
cls = image
image = null
}
image = this.getImage(image)
cls = cls.constructor !== Array ? [cls] : cls
action = action == null ? 'toggle' : action
// no image is loaded...
if(image.length == 0){
return
}
// get marked state...
if(action == '?'){
var gid = this.elemGID(image)
var res = 0
cls.forEach(function(cls){
res += that.getImageMarks(gid, cls).length != 0 ? 1 : 0
})
return res == cls.length ? 'on' : 'off'
}
// set the marks...
image.each(function(){
var image = $(this)
var gid = that.elemGID(image)
cls.forEach(function(cls){
var mark = that.getImageMarks(gid, cls)
// set the mark...
if(mark.length == 0
&& (action == 'toggle'
|| action == 'on')){
that.createMark(cls, gid)
.insertAfter(image)
// clear the mark...
} else if(action != 'on') {
mark.remove()
}
})
})
return image
},
// Rotate an image...
//
// Rotate image clockwise:
// .rotateImage(target, 'cw')
// -> image
//
// Rotate image counterclockwise:
// .rotateImage(target, 'ccw')
// -> image
//
// Set explicit image rotation angle:
// .rotateImage(target, 0|90|180|270)
// .rotateImage(target, -90|-180|-270)
// -> image
//
// NOTE: target must be .getImage(..) compatible.
// NOTE: this can be applied in bulk, e.g.
// this.rotateImage($('.image'), 'cw') will rotate all the
// loaded images clockwise.
rotateImage: function(target, direction){
target = target == null || target.constructor !== Array ? [target] : target
// validate direction...
if(images.calcRelativeRotation(direction) == null){
return target
}
var that = this
$(target).each(function(i, e){
var img = that.getImage(e)
var o = (direction == 'cw' || direction == 'ccw')
? images.calcRelativeRotation(img.attr('orientation'), direction)
: direction*1
if(o == 0){
img.removeAttr('orientation')
} else {
img.attr('orientation', o)
}
// account for proportions...
that.correctImageProportionsForRotation(img)
// XXX this is a bit of an overkill but it will update the
// preview if needed...
//that.updateImage(img)
})
return this
},
// Flip an image...
//
// Flip image relative to view:
// .flipImage(target, 'horizontal')
// .flipImage(target, 'vertical')
// .flipImage(target, 'horizontal', 'view')
// .flipImage(target, 'vertical', 'view')
// -> image
//
// Flip image relative to image:
// .flipImage(target, 'horizontal', 'image')
// .flipImage(target, 'vertical', 'image')
// -> image
//
// Set an explicit state:
// .flipImage(target, [ .. ])
// -> image
//
// NOTE: target must be .getImage(..) compatible.
// NOTE: this can be applied in bulk, e.g.
// this.flipImage($('.image'), 'vertical') will rotate all the
// loaded images vertically.
// NOTE: this is relative to how the image is viewed and not to
// it's original orientation by default...
// ...this makes things consistent both visually and internally
flipImage: function(target, direction, reference){
reference = reference || 'view'
target = target == null || target.constructor !== Array ? [target] : target
var set_state = direction.constructor === Array ? direction : null
var that = this
$(target).each(function(i, e){
var img = that.getImage(e)
// update existing state...
if(set_state == null){
var d = direction
if(reference == 'view' && [90, 270].indexOf(that.getImageRotation(img)) > -1){
d = direction == 'vertical' ? 'horizontal' : 'vertical'
}
var state = img.attr('flipped')
state = (state == null ? '' : state)
.split(',')
.map(function(e){ return e.trim() })
.filter(function(e){ return e != '' })
// toggle the specific state...
var i = state.indexOf(d)
if(i >= 0){
state.splice(i, 1)
} else {
state.push(d)
}
// set an explicit state...
} else {
var state = set_state.slice()
}
// write the state...
if(state.length == 0){
img.removeAttr('flipped')
} else {
img.attr('flipped', state.join(', '))
}
})
return this
},
// shorthands...
// XXX should these be here???
rotateCW: function(target){ return this.rotateImage(target, 'cw') },
rotateCCW: function(target){ return this.rotateImage(target, 'ccw') },
flipVertical: function(target, reference){
return this.flipImage(target, 'vertical', reference) },
flipHorizontal: function(target, reference){
return this.flipImage(target, 'horizontal', reference) },
}
RibbonsPrototype.__proto__ = BaseRibbonsPrototype
var Ribbons =
module.Ribbons =
object.makeConstructor('Ribbons',
RibbonsClassPrototype,
RibbonsPrototype)
/**********************************************************************
* vim:set ts=4 sw=4 : */ return module })