/********************************************************************** * * Viewer Generation III * * Split the API into the following sections: * - main control actions * do main domain tasks like image and ribbon manipulation. * - serialization and deserialization * load and save data * - UI * basic align, animation and modes * * * TODO group all actions into an object, referencing the viewer... * ...this will make this reusable multiple times. * TODO wrap the actions into an object and make all queries relative to * a single root viewer... * ...this will make the code reusable multiple times... * * **********************************************************************/ // NOTE: NAV_ALL might not be practical... var NAV_ALL = '*' var NAV_VISIBLE = ':visible' var NAV_MARKED = '.marked:visible' var NAV_DEFAULT = NAV_ALL var NAV_RIBBON_ALL = '' var NAV_RIBBON_VISIBLE = ':visible' var NAV_RIBBON_DEFAULT = NAV_RIBBON_ALL //var NAV_RIBBON_DEFAULT = NAV_RIBBON_VISIBLE var TRANSITION_MODE_DEFAULT = 'animate' var MAX_SCREEN_IMAGES = 12 var ZOOM_SCALE = 1.2 /********************************************************************** * Helpers */ // Match the results of two functions // // If the results are not the same then print a warning. // // NOTE: this is here for testing. // NOTE: this expects that none of the functions will modify the // arguments... // NOTE: this will return the result of the first function. function match2(f0, f1){ return function(){ var a = f0.apply(f0, arguments) var b = f1.apply(f1, arguments) if(a != b){ console.warn('Result mismatch: f0:'+a+' f1:'+b) } return a } } // Same as match2 but can take an arbitrary number of functions. // XXX test function matchN(){ var funcs = arguments return function(){ var res = [] var err = false var r // call everything... for(var i=0; i < funcs.lenght; i++){ r = f0.apply(f0, arguments) // match the results... if(r != res[res.length-1]){ err = false } res.push(r) } if(err){ console.warn('Not all results matched:', r) } return res[0] } } // XXX might need shift left/right indicators (later)... function flashIndicator(direction){ $({ prev: '.up-indicator', next: '.down-indicator', start: '.start-indicator', end: '.end-indicator', }[direction]) // NOTE: this needs to be visible in all cases and key press // rhythms... .show() .delay(100) .fadeOut(300) } function getImage(gid){ var res // current or first (no gid given) if(gid == null){ res = $('.current.image') return res.length == 0 ? $('.image').first() : res } // order... if(typeof(gid) == typeof(1)){ res = $('.image[order="'+ JSON.stringify(gid) +'"]') if(res.length != null){ return res } } // gid... res = $('.image[gid="'+ JSON.stringify(gid) +'"]') if(res.length != null){ return res } return null } function getImageOrder(image){ image = image == null ? getImage() : $(image) if(image.length == 0){ return } return JSON.parse(image.attr('order')) } function getImageGID(image){ image = image == null ? getImage() : $(image) if(image.length == 0){ return } return JSON.parse(image.attr('gid')) } function getRibbon(image){ image = image == null ? getImage() : $(image) return image.closest('.ribbon') } // NOTE: elem is optional and if given can be an image or a ribbon... function getRibbonIndex(elem){ if(elem == null){ var ribbon = getRibbon() } else { elem = $(elem) if(elem.hasClass('image')){ ribbon = getRibbon(elem) } else { ribbon = elem } } return $('.ribbon').index(ribbon) } // Calculate relative position between two elements // // NOTE: tried to make this as brain-dead-stupidly-simple as possible... // ...looks spectacular comparing to either gen2 or gen1 ;) // NOTE: if used during an animation/transition this will give the // position at the exact frame of the animation, this might not be // the desired "final" data... // XXX account for rotated images... // need to keep this generic but still account for rotation... function getRelativeVisualPosition(outer, inner){ outer = $(outer).offset() inner = $(inner).offset() return { top: inner.top - outer.top, left: inner.left - outer.left } } // Returns the image size (width) as viewed on screen... // // dim can be: // - 'width' (default) // - 'height' // - 'min' // - 'max' // // NOTE: we do not need to worry about rotation here as the size change is // compensated with margins... function getVisibleImageSize(dim){ dim = dim == null ? 'width' : dim var scale = getElementScale($('.ribbon-set')) if(dim == 'height'){ return $('.image').outerHeight(true) * scale } else if(dim == 'width'){ return $('.image').outerWidth(true) * scale } else if(dim == 'max'){ return Math.max($('.image').outerHeight(true), $('.image').outerWidth(true)) * scale } else if(dim == 'min'){ return Math.min($('.image').outerHeight(true), $('.image').outerWidth(true)) * scale } } // Return the number of images that can fit to viewer width... function getScreenWidthInImages(size){ size = size == null ? getVisibleImageSize() : size return $('.viewer').innerWidth() / size } // NOTE: this will return an empty jquery object if no image is before // the target... // NOTE: this might return an empty target if the ribbon is empty... // NOTE: this only "sees" the loaded images, for a full check use // getGIDBefore(...) that will check the full data... function getImageBefore(image, ribbon, mode){ mode = mode == null ? NAV_DEFAULT : mode image = image == null ? getImage() : $(image) if(ribbon == null){ ribbon = getRibbon(image) } var images = $(ribbon).find('.image').filter(mode) var order = getImageOrder(image) var prev = [] images.each(function(){ if(order < getImageOrder($(this))){ return false } prev = this }) return $(prev) } function shiftTo(image, ribbon){ var target = getImageBefore(image, ribbon, NAV_ALL) var cur_ribbon = getRibbon(image) // insert before the first image if nothing is before the target... if(target.length == 0){ image.prependTo($(ribbon)) } else { image.insertAfter(target) } $('.viewer').trigger('shiftedImage', [image, cur_ribbon, ribbon]) // if removing last image out of a ribbon, remove the ribbon.... if(cur_ribbon.find('.image').length == 0){ // XXX check if the ribbon outside the loaded area is empty... // ...do we need this check? it might be interesting to // "collapse" disjoint, empty areas... // ......if so, will also need to do this in DATA... removeRibbon(cur_ribbon) } return image } function shiftImage(direction, image, force_create_ribbon){ image = image == null ? getImage() : $(image) var old_ribbon = getRibbon(image) var ribbon = old_ribbon[direction]('.ribbon') // need to create a new ribbon... if(ribbon.length == 0 || force_create_ribbon == true){ var index = getRibbonIndex(old_ribbon) index = direction == 'next' ? index + 1 : index ribbon = createRibbon(index) shiftTo(image, ribbon) } else { shiftTo(image, ribbon) } return image } // Update an info element // // align can be: // - top // - bottom // // If target is an existing info container (class: overlay-info) then // just fill that. function updateInfo(elem, data, target){ var viewer = $('.viewer') target = target == null ? viewer : $(target) elem = elem == null ? $('.overlay-info') : $(elem) if(elem.length == 0){ elem = $('
') } elem .addClass('overlay-info') .html('') .off() if(typeof(data) == typeof('abc')){ elem.html(data) } else { elem.append(data) } return elem .appendTo(target) } function showInfo(elem, data, target){ elem = elem == null ? $('.overlay-info') : elem elem = data == null ? elem : updateInfo(elem, data, traget) return elem.fadeIn() } function hideInfo(elem){ elem = elem == null ? $('.overlay-info') : elem return elem.fadeOut() } // Update status message // // NOTE: this will update message content and return it as-is, things // like showing the message are to be done manually... // see: showStatus(...) and showErrorStatus(...) for a higher level // API... // NOTE: in addition to showing user status, this will also log the // satus to browser console... // NOTE: the message will be logged to console via either console.log(...) // or console.error(...), if the message starts with "Error". // NOTE: if message is null, then just return the status element... // // XXX add abbility to append and clear status... function updateStatus(message){ var elem = $('.global-status') if(elem.length == 0){ elem = $('') } if(message == null){ return elem } if(typeof(message) == typeof('s') && /^error.*/i.test(message)){ console.error.apply(console, arguments) } else { console.log.apply(console, arguments) } if(arguments.length > 1){ message = Array.apply(Array, arguments).join(' ') } return updateInfo(elem, message) } // Same as updateInfo(...) but will aslo show and animate-close the message function showStatus(message){ return updateStatus.apply(null, arguments) .stop() .show() .delay(500) .fadeOut(800) } // Same as showStatus(...) but will always add 'Error: ' to the start // of the message // // NOTE: this will show the message but will not hide it. function showErrorStatus(message){ message = Array.apply(Array, arguments) message.splice(0, 0, 'Error:') return updateStatus.apply(null, message) .one('click', function(){ $(this).fadeOut() }) .stop() .show() } // shorthand methods... function hideStatus(){ // yes, this indeed looks funny -- to hide a status you need to show // it without any arguments... ;) return showStatus() } function getStatus(){ return updateStatus() } function makeIndicator(text){ return $('') } function showGlobalIndicator(cls, text){ var c = $('.global-mode-indicators') if(c.length == 0){ c = $('