diff --git a/ui (gen4)/viewer.js b/ui (gen4)/viewer.js index daf37654..f2283552 100755 --- a/ui (gen4)/viewer.js +++ b/ui (gen4)/viewer.js @@ -894,23 +894,92 @@ actions.Actions(Client, { // ...need something like: // Features(['feature_a', 'feature_b'], action).setup() +// XXX might be a good idea to automate .setup(..)/.remove(..) and split +// it into components: +// - actions sets +// - event handlers +// - will need a standard way to reference both the action-set +// and feature from the handler... (???) +// - .config merging +// - custom setup stuff var FeatureProto = module.FeatureProto = { tag: null, + setup: function(actions){ + var that = this + + // mixin actions... + if(this.actions != null){ + actions.mixin(this.actions) + } + + // install handlers... + if(this.handlers != null){ + this.handlers.forEach(function(h){ + actions.on(h[0], that.tag, h[1]) + }) + } + + // merge config... + // XXX should this use inheritance??? + if(this.config != null || (this.actions != null && this.actions.config != null)){ + var config = this.config || this.actions.config + + if(actions.config == null){ + actions.config = {} + } + Object.keys(config).forEach(function(n){ + if(actions.config[n] === undefined){ + actions.config[n] = config[n] + } + }) + } + + // custom setup... + // XXX is this the correct way??? + if(this.hasOwnProperty('setup') && this.setup !== FeatureProto.setup){ + this.setup(actions) + } + + return this + }, remove: function(actions){ - return actions.off('*', this.tag) + if(this.actions != null){ + actions.mixout(this.actions) + } + + if(this.handlers != null){ + actions.off('*', this.tag) + } + + if(this.hasOwnProperty('remove') && this.setup !== FeatureProto.remove){ + this.remove(actions) + } + + // remove feature DOM elements... + actions.ribbons.viewer.find('.' + this.tag).remove() + + return this }, } +// XXX is hard-coded default feature-set a good way to go??? var Feature = module.Feature = -function Feature(obj){ +function Feature(feature_set, obj){ + if(obj == null){ + obj = feature_set + // XXX is this a good default??? + feature_set = Features + } + obj.__proto__ = FeatureProto - // XXX not sure about this... - Features[obj.tag] = obj + if(feature_set){ + feature_set[obj.tag] = obj + } return obj } @@ -919,22 +988,27 @@ function Feature(obj){ // XXX experimental... // ...not sure if the global feature set is a good idea... // XXX might be good to track and automate: -// - priority/precedence -// - exclusivity groups -- i.e. only one from a group can be on. -// - dependency and dependency precedence // - documentation and control ui // - title // - doc // - ... +// - exclusivity groups -- i.e. only one from a group can be on. +// the last one wins + output error +// - priority/precedence (sort) +// - dependency and dependency precedence (sort) +// XXX if this works out might be a good idea to organize everything as +// a feature... including the Client and Viewer +// ...needs more thought... var FeatureSet = module.FeatureSet = { setup: function(obj, lst){ lst = lst.constructor !== Array ? [lst] : lst var that = this + var setup = FeatureProto.setup lst.forEach(function(n){ if(that[n] != null){ console.log('Setting up feature:', n) - that[n].setup(obj) + setup.call(that[n], obj) } }) }, @@ -1045,33 +1119,36 @@ actions.Actions({ // XXX need to test and tweak with actual images... var PartialRibbons = module.PartialRibbons = Feature({ + title: 'Partial Ribbons', + doc: 'Maintains partially loaded ribbons, this enables very lage ' + +'image sets to be hadled eficiently.', + + priority: 'high', + tag: 'ui-partial-ribbons', - // number of screen widths to load... - size: 7, - // number of screen widths to edge to trigger reload... - threshold: 1.5, + actions: PartialRibbonsActions, - setup: function(actions){ - var feature = this + config: { + // number of screen widths to load... + 'ribbon-size-screens': 7, - if(!('ribbon-size-screens' in actions.config)){ - actions.config['ribbon-size-screens'] = this.size - } - if(!('ribbon-resize-threshold' in actions.config)){ - actions.config['ribbon-resize-threshold'] = this.threshold - } + // number of screen widths to edge to trigger reload... + 'ribbon-resize-threshold': 1.5, + }, - return actions - .mixin(PartialRibbonsActions) - .on('focusImage.pre centerImage.pre', this.tag, function(target){ + handlers: [ + ['focusImage.pre centerImage.pre', + function(target){ this.updateRibbon(target) - }) - .on('fitImage.pre', this.tag, function(n){ + }], + ['fitImage.pre', + function(n){ this.updateRibbon('current', n || 1) - }) - .on('fitRibbon.pre', this.tag, function(n){ + }], + ['fitRibbon.pre', + function(n){ n = n || 1 // convert target height in ribbons to width in images... @@ -1085,12 +1162,8 @@ module.PartialRibbons = Feature({ var nw = w / (h/n) this.updateRibbon('current', nw) - }) - }, - remove: function(actions){ - actions.mixout(PartialRibbonsActions) - return actions.off('*', this.tag) - }, + }], + ], }) @@ -1106,6 +1179,11 @@ actions.Actions({ 'single-image-mode') ], }) +// XXX should this be an action??? +function updateImageProportions(){ + // XXX +} + // XXX an ideal case would be: // @@ -1176,25 +1254,21 @@ var SingleImageView = module.SingleImageView = Feature({ tag: 'ui-single-image-view', - // XXX should this be an action??? - updateImageProportions: function(actions){ - // XXX - }, + actions: SingleImageActions, - setup: function(actions){ - var that = this - return actions - .mixin(SingleImageActions) - .on('fitImgae.post', this.tag, function(){ + handlers:[ + ['fitImgae.post', + function(){ // singe image mode -- set image proportions... if(this.toggleSingleImage('?') == 'on'){ - that.updateImageProportions(this) + updateImageProportions.call(this) } - }) - .on('toggleSingleImage.post', this.tag, function(){ + }], + ['toggleSingleImage.post', + function(){ // singe image mode -- set image proportions... if(this.toggleSingleImage('?') == 'on'){ - that.updateImageProportions(this) + updateImageProportions.call(this) // ribbon mode -- restore original image size... } else { @@ -1203,12 +1277,8 @@ module.SingleImageView = Feature({ height: '' }) } - }) - }, - remove: function(actions){ - actions.mixout(SingleImageActions) - return actions.off('*', this.tag) - }, + }], + ], }) @@ -1216,15 +1286,15 @@ module.SingleImageView = Feature({ //--------------------------------------------------------------------- // XXX this should also define up/down navigation behavior e.g. what to // focus on next/prev ribbon... +// XXX should .alignByOrder(..) be a feature-specific action or global +// as it is now??? var AlignRibbonsToImageOrder = module.AlignRibbonsToImageOrder = Feature({ tag: 'ui-ribbon-align-to-order', - setup: function(actions){ - return actions - .on('focusImage.post', this.tag, - function(){ this.alignByOrder() }) - }, + handlers: [ + ['focusImage.post', function(){ this.alignByOrder() }] + ], }) @@ -1234,11 +1304,9 @@ var AlignRibbonsToFirstImage = module.AlignRibbonsToFirstImage = Feature({ tag: 'ui-ribbon-align-to-first', - setup: function(actions){ - return actions - .on('focusImage.post', this.tag, - function(){ this.alignByFirst() }) - }, + handlers: [ + ['focusImage.post', function(){ this.alignByFirst() }], + ], }) @@ -1249,295 +1317,294 @@ var ShiftAnimation = module.ShiftAnimation = Feature({ tag: 'ui-animation', - setup: function(actions){ - var animate = function(target){ - // XXX do not do target lists... - if(target != null && target.constructor === Array){ - return - } - var s = this.ribbons.makeShadow(target, true) - return function(){ s() } - } + handlers: [ + ['shiftImageUp.pre shiftImageDown.pre', + function(target){ + // XXX do not do target lists... + if(target != null && target.constructor === Array){ + return + } + var s = this.ribbons.makeShadow(target, true) + return function(){ s() } + }], // NOTE: this will keep the shadow in place -- the shadow will not // go to the mountain, the mountain will come to the shadow ;) - var noanimate = function(target){ - // XXX do not do target lists... - if(target != null && target.constructor === Array){ - return - } - var s = this.ribbons.makeShadow(target) - return function(){ s() } - } - var tag = this.tag - return actions - .on('shiftImageUp.pre', tag, animate) - .on('shiftImageDown.pre', tag, animate) - .on('shiftImageLeft.pre', tag, noanimate) - .on('shiftImageRight.pre', tag, noanimate) - }, + ['shiftImageLeft.pre shiftImageRight.pre', + function(target){ + // XXX do not do target lists... + if(target != null && target.constructor === Array){ + return + } + var s = this.ribbons.makeShadow(target) + return function(){ s() } + }], + ], }) //--------------------------------------------------------------------- +var BoundsIndicatorsActions = +module.BoundsIndicatorsActions = +actions.Actions({ + flashIndicator: ['Flash an indicator', + function(direction){ + var cls = { + // shift up/down... + up: '.up-indicator', + down: '.down-indicator', + // hit start/end/top/bottom of view... + start: '.start-indicator', + end: '.end-indicator', + top: '.top-indicator', + bottom: '.bottom-indicator', + }[direction] + + var indicator = this.ribbons.viewer.find(cls) + + if(indicator.length == 0){ + indicator = $('
') + .addClass(cls.replace('.', '') +' '+ this.tag) + .appendTo(this.ribbons.viewer) + } + + return indicator + // NOTE: this needs to be visible in all cases and key press + // rhythms... + .show() + .delay(100) + .fadeOut(300) + }], +}) + +function didAdvance(indicator){ + return function(){ + var img = this.data.current + return function(){ + if(img == this.data.current){ + this.flashIndicator(indicator) + } + } + } +} + var BoundsIndicators = module.BoundsIndicators = Feature({ tag: 'ui-bounds-indicators', - flashIndicator: function(viewer, direction){ - var cls = { - // shift up/down... - up: '.up-indicator', - down: '.down-indicator', - // hit start/end/top/bottom of view... - start: '.start-indicator', - end: '.end-indicator', - top: '.top-indicator', - bottom: '.bottom-indicator', - }[direction] + actions: BoundsIndicatorsActions, - var indicator = viewer.find(cls) + handlers: [ + // basic navigation... + ['nextImage.pre lastImage.pre', didAdvance('end')], + ['prevImage.pre firstImage.pre', didAdvance('start')], + ['nextRibbon.pre lastRibbon.pre', didAdvance('bottom')], + ['prevRibbon.pre firstRibbon.pre', didAdvance('top')], - if(indicator.length == 0){ - indicator = $('
') - .addClass(cls.replace('.', '') +' '+ this.tag) - .appendTo(viewer) - } + // vertical shifting... + ['shiftImageUp.pre', + function(target){ + target = target || this.current + var r = this.data.getRibbonOrder(target) - return indicator - // NOTE: this needs to be visible in all cases and key press - // rhythms... - .show() - .delay(100) - .fadeOut(300) - }, + var l = this.data.getImages(r).length + var l0 = this.data.getImages(0).length - setup: function(actions){ - var that = this - - var didAdvance = function(indicator){ - return function(){ - var img = this.data.current return function(){ - if(img == this.data.current){ - that.flashIndicator(actions.ribbons.viewer, indicator) + // when shifting last image of top ribbon (i.e. length == 1) + // up the state essentially will not change... + if((r == 0 && l == 1) + // we are shifting to a new empty ribbon... + || (r == 1 && l == 1 && l0 == 0)){ + this.flashIndicator('top') + } else { + this.flashIndicator('up') } } - } - } + }], + ['shiftImageDown.pre', + function(target){ + target = target || this.current + var r0 = this.data.getRibbonOrder(target) + var l = this.data.getImages(r0).length - var tag = this.tag - return actions - // basic navigation... - .on('nextImage.pre lastImage.pre', tag, didAdvance('end')) - .on('prevImage.pre firstImage.pre', tag, didAdvance('start')) - .on('nextRibbon.pre lastRibbon.pre', tag, didAdvance('bottom')) - .on('prevRibbon.pre firstRibbon.pre', tag, didAdvance('top')) - - // vertical shifting... - .on('shiftImageUp.pre', tag, - function(target){ - target = target || this.current - var r = this.data.getRibbonOrder(target) - - var l = this.data.getImages(r).length - var l0 = this.data.getImages(0).length - - return function(){ - // when shifting last image of top ribbon (i.e. length == 1) - // up the state essentially will not change... - if((r == 0 && l == 1) - // we are shifting to a new empty ribbon... - || (r == 1 && l == 1 && l0 == 0)){ - that.flashIndicator(this.ribbons.viewer, 'top') - } else { - that.flashIndicator(this.ribbons.viewer, 'up') - } + return function(){ + var r1 = this.data.getRibbonOrder(target) + if(r0 == r1 && r0 == this.data.ribbon_order.length-1 && l == 1){ + this.flashIndicator('bottom') + } else { + this.flashIndicator('down') } - }) - .on('shiftImageDown.pre', tag, - function(target){ - target = target || this.current - var r0 = this.data.getRibbonOrder(target) - var l = this.data.getImages(r0).length + } + }], - return function(){ - var r1 = this.data.getRibbonOrder(target) - if(r0 == r1 && r0 == this.data.ribbon_order.length-1 && l == 1){ - that.flashIndicator(this.ribbons.viewer, 'bottom') - } else { - that.flashIndicator(this.ribbons.viewer, 'down') - } - } - }) - - // horizontal shifting... - .on('shiftImageLeft.pre', tag, - function(target){ - if(target == null - //&& actions.data.getImageOrder('ribbon') == 0){ - && this.data.getImage('prev') == null){ - that.flashIndicator(this.ribbons.viewer, 'start') - } - }) - .on('shiftImageRight.pre', tag, - function(target){ - if(target == null - && this.data.getImage('next') == null){ - that.flashIndicator(this.ribbons.viewer, 'end') - } - }) - }, - remove: function(actions){ - actions.ribbons.viewer.find('.' + this.tag).remove() - return actions.off('*', this.tag) - }, + // horizontal shifting... + ['shiftImageLeft.pre', + function(target){ + if(target == null + //&& actions.data.getImageOrder('ribbon') == 0){ + && this.data.getImage('prev') == null){ + this.flashIndicator('start') + } + }], + ['shiftImageRight.pre', + function(target){ + if(target == null + && this.data.getImage('next') == null){ + this.flashIndicator('end') + } + }], + ], }) //--------------------------------------------------------------------- +var CurrentImageIndicatorActions = +module.CurrentImageIndicatorActions = +actions.Actions({ + updateCurrentImageIndicator: ['Update current image indicator', + function(target, update_border){ + var scale = this.ribbons.getScale() + var cur = this.ribbons.getImage(target) + var ribbon = this.ribbons.getRibbon(target) + var ribbon_set = this.ribbons.viewer.find('.ribbon-set') + + var marker = ribbon.find('.current-marker') + + // get config... + var border = this.config['current-image-border'] + var min_border = this.config['current-image-min-border'] + var border_timeout = this.config['current-image-border-timeout'] + var fadein = this.config['current-image-indicator-fadein'] + + // no marker found -- either in different ribbon or not created yet... + if(marker.length == 0){ + // get marker globally... + marker = this.ribbons.viewer.find('.current-marker') + + // create a marker... + if(marker.length == 0){ + var marker = $('
') + .addClass('current-marker '+ this.tag) + .css({ + opacity: '0', + top: '0px', + left: '0px', + }) + .appendTo(ribbon) + .animate({ + 'opacity': 1 + }, fadein) + + // add marker to current ribbon... + } else { + marker.appendTo(ribbon) + } + } + + // NOTE: we will update only the attrs that need to be updated... + var css = {} + + var w = cur.outerWidth(true) + var h = cur.outerHeight(true) + + // keep size same as the image... + if(marker.outerWidth() != w || marker.outerHeight() != h){ + css.width = w + css.height = h + } + + // update border... + if(update_border !== false){ + var border = Math.max(min_border, border / scale) + + // set border right away... + if(update_border == 'before'){ + css.borderWidth = border + + // set border with a delay... + } else { + setTimeout(function(){ + marker.css({ borderWidth: border }) + }, border_timeout) + } + } + + css.left = cur[0].offsetLeft + + marker.css(css) + }], +}) + var CurrentImageIndicator = module.CurrentImageIndicator = Feature({ tag: 'ui-current-image-indicator', - border: 3, - min_border: 2, + actions: CurrentImageIndicatorActions, - border_timeout: 200, - shift_timeout: 200, + config: { + 'current-image-border': 3, + 'current-image-min-border': 2, - fadein: 500, + 'current-image-border-timeout': 200, + 'current-image-shift-timeout': 200, - animate: true, - - updateMarker: function(actions, target, update_border){ - var scale = actions.ribbons.getScale() - var cur = actions.ribbons.getImage(target) - var ribbon = actions.ribbons.getRibbon(target) - var ribbon_set = actions.ribbons.viewer.find('.ribbon-set') - - var marker = ribbon.find('.current-marker') - - // no marker found -- either in different ribbon or not created yet... - if(marker.length == 0){ - // get marker globally... - marker = actions.ribbons.viewer.find('.current-marker') - - // create a marker... - if(marker.length == 0){ - var marker = $('
') - .addClass('current-marker '+ this.tag) - .css({ - opacity: '0', - top: '0px', - left: '0px', - }) - .appendTo(ribbon) - .animate({ - 'opacity': 1 - }, this.fadein) - - // add marker to current ribbon... - } else { - marker.appendTo(ribbon) - } - } - - // NOTE: we will update only the attrs that need to be updated... - var css = {} - - var w = cur.outerWidth(true) - var h = cur.outerHeight(true) - - // keep size same as the image... - if(marker.outerWidth() != w || marker.outerHeight() != h){ - css.width = w - css.height = h - } - - // update border... - if(update_border !== false){ - var border = Math.max(this.min_border, this.border / scale) - - // set border right away... - if(update_border == 'before'){ - css.borderWidth = border - - // set border with a delay... - } else { - setTimeout(function(){ - marker.css({ borderWidth: border }) - }, this.border_timeout) - } - } - - css.left = cur[0].offsetLeft - - return marker.css(css) + 'current-image-indicator-fadein': 500, }, - setup: function(actions){ - var timeout - var that = this - return actions - // move marker to current image... - .on( 'focusImage.post', this.tag, - function(){ that.updateMarker(this) }) - // prevent animations when focusing ribbons... - .on('focusRibbon.pre', this.tag, - function(){ - var m = this.ribbons.viewer.find('.current-marker') - this.ribbons.preventTransitions(m) - return function(){ - this.ribbons.restoreTransitions(m) - } - }) - // this is here to compensate for position change on ribbon - // resize... - .on('resizeRibbon.post', this.tag, - function(target, s){ - var m = this.ribbons.viewer.find('.current-marker') - if(m.length != 0){ - this.ribbons.preventTransitions(m) - that.updateMarker(this, target, false) - this.ribbons.restoreTransitions(m, true) - } - }) - // Change border size in the appropriate spot in the animation: - // - before animation when scaling up - // - after when scaling down - // This is done to make the visuals consistent... - .on( 'fitImage.pre fitRibbon.pre', this.tag, function(w1){ + handlers: [ + // move marker to current image... + [ 'focusImage.post', + function(){ this.updateCurrentImageIndicator() }], + // prevent animations when focusing ribbons... + ['focusRibbon.pre', + function(){ + var m = this.ribbons.viewer.find('.current-marker') + this.ribbons.preventTransitions(m) + return function(){ + this.ribbons.restoreTransitions(m) + } + }], + // this is here to compensate for position change on ribbon + // resize... + ['resizeRibbon.post', + function(target, s){ + var m = this.ribbons.viewer.find('.current-marker') + if(m.length != 0){ + this.ribbons.preventTransitions(m) + this.updateCurrentImageIndicator(target, false) + this.ribbons.restoreTransitions(m, true) + } + }], + // Change border size in the appropriate spot in the animation: + // - before animation when scaling up + // - after when scaling down + // This is done to make the visuals consistent... + [ 'fitImage.pre fitRibbon.pre', + function(w1){ var w0 = this.screenwidth w1 = w1 || 1 return function(){ - that.updateMarker(this, null, w0 > w1 ? 'before' : 'after') + this.updateCurrentImageIndicator(null, w0 > w1 ? 'before' : 'after') } - }) - // hide marker on shift left/right and show it after done shifting... - .on('shiftImageLeft.pre shiftImageRight.pre', this.tag, function(){ - this.ribbons.viewer.find('.current-marker').hide() - if(timeout != null){ - clearTimeout(timeout) - timeout == null - } - return function(){ - var ribbons = this.ribbons - var fadein = that.fadein - timeout = setTimeout(function(){ - ribbons.viewer.find('.current-marker').fadeIn(fadein) - }, that.shift_timeout) - } - }) - // turn the marker on... - // XXX not sure about this... - //.focusImage() - }, - remove: function(actions){ - actions.ribbons.viewer.find('.' + this.tag).remove() - return actions.off('*', this.tag) - }, + }], + ['shiftImageLeft.pre shiftImageRight.pre', + function(){ + this.ribbons.viewer.find('.current-marker').hide() + if(this._current_image_indicator_timeout != null){ + clearTimeout(this._current_image_indicator_timeout) + delete this._current_image_indicator_timeout + } + return function(){ + var ribbons = this.ribbons + var fadein = this.config['current-image-indicator-fadein'] + this._current_image_indicator_timeout = setTimeout(function(){ + ribbons.viewer.find('.current-marker').fadeIn(fadein) + }, this.config['current-image-shift-timeout']) + } + }], + ], }) @@ -1547,13 +1614,6 @@ module.CurrentImageIndicator = Feature({ var ImageStateIndicator = module.ImageStateIndicator = Feature({ tag: 'ui-image-state-indicator', - - setup: function(actions){ - }, - remove: function(actions){ - actions.ribbons.viewer.find('.' + this.tag).remove() - return actions.off('*', this.tag) - }, }) @@ -1563,13 +1623,6 @@ module.ImageStateIndicator = Feature({ var GlobalStateIndicator = module.GlobalStateIndicator = Feature({ tag: 'ui-global-state-indicator', - - setup: function(actions){ - }, - remove: function(actions){ - actions.ribbons.viewer.find('.' + this.tag).remove() - return actions.off('*', this.tag) - }, })