themeApp.on = function( event, callback ) { if ( _.contains( [ 'screen:leave', 'screen:showed', 'screen:before-transition', 'menu:rendered', 'header:rendered' ], event ) ) { //Proxy RegionManager events : RegionManager.on( event, callback ); } else if ( _.contains( [ 'refresh:start', 'refresh:end' ], event ) ) { //Proxy App events : App.on( event, callback ); } else { vent.on( event, callback ); } };
define( function( require, exports ) { "use strict"; var _ = require( 'underscore' ), Backbone = require( 'backbone' ), RegionManager = require( 'core/region-manager' ), Utils = require( 'core/app-utils' ), Config = require( 'root/config' ), Messages = require( 'core/messages' ), App = require( 'core/app' ), Hooks = require( 'core/lib/hooks' ), TemplateTags = require( 'core/theme-tpl-tags' ), PhoneGap = require( 'core/phonegap-utils' ); var themeApp = { }; /************************************************ * Events management */ /** * Theme event aggregator */ var vent = _.extend( { }, Backbone.Events ); /** * Aggregate App and RegionManager events */ themeApp.on = function( event, callback ) { if ( _.contains( [ 'screen:leave', 'screen:showed', 'screen:before-transition', 'menu:refresh', 'header:render', 'waiting:start', 'waiting:stop' ], event ) ) { //Proxy RegionManager events : RegionManager.on( event, callback ); } else { vent.on( event, callback ); } }; /** * Proxy App events to theme events * * @param {string} event App event id * @param {object} data App event data * @returns {event} Triggers theme event based on App core event */ App.on( 'all', function( event, data ) { var theme_event_data = format_theme_event_data( event, data ); if ( theme_event_data.type == 'error' || theme_event_data.type == 'info' || theme_event_data.type == 'network' ) { //2 ways of binding to error and info events : vent.trigger( event, theme_event_data ); //Ex: bind directly to 'info:no-content' vent.trigger( theme_event_data.type, theme_event_data ); //Ex: bind to general 'info', then filter with if( info.event == 'no-content' ) } } ); /** * Formats App core events feedbacks in a themes friendly way. * * @param {string} event App event id (example "error:synchro:ajax") * @param {object} data * @returns {object} { * event: string : theme event id (example : "synchro:ajax"), * type: string : 'error' | 'info', * message: string : error or info message * data: object : original core event data : { * type: string : 'ajax' | 'ws-data' | 'not-found' | 'wrong-data', * where: string : core function where the event occured * message: string : message associated to the event * data: object : data associated to the core event * } * } */ var format_theme_event_data = function( event, data ) { var theme_event_data = { event: event, type: '', message: '', data: data }; if ( event.indexOf( 'error:' ) === 0 ) { theme_event_data.type = 'error'; theme_event_data.event = event.replace( 'error:', '' ); if ( data.type == 'ajax' ) { theme_event_data.message = Messages.get('error_remote_connexion_failed'); } else { theme_event_data.message = Messages.get('error_occured_undefined'); } } else if ( event.indexOf( 'info:' ) === 0 ) { theme_event_data.type = 'info'; theme_event_data.event = event.replace( 'info:', '' ); if ( event == 'info:no-content' ) { theme_event_data.message = Messages.get('info_no_content'); } } else if ( event.indexOf( 'network:' ) === 0 ) { theme_event_data.type = 'network'; theme_event_data.event = event.replace( 'network:', '' ); if( event == 'network:online' ) { theme_event_data.message = Messages.get('info_network_online'); }else if( event == 'network:offline' ) { theme_event_data.message = Messages.get('info_network_offline'); } } return theme_event_data; }; /************************************************ * Themes actions results */ /** * Formats data that is used in themes as the result of an event or * treatment. * * @param {boolean} ok * @param {string} message * @param {object} data * @returns object { * ok: boolean, * message: string, * data: object * } */ var format_result_data = function( ok, message, data ) { ok = ok === true || ok === 1 || ok === '1'; message = !_.isUndefined(message) && _.isString(message) ? message: ''; data = !_.isUndefined(data) ? data : {}; return { ok: ok, message: message, data: data }; }; /************************************************ * Filters, actions and Params management */ themeApp.filter = function( filter, callback ) { Hooks.addFilter( filter, callback ); } themeApp.action = function( action, callback ) { Hooks.addAction( action, callback ); } themeApp.setParam = function( param, value ) { App.setParam( param, value ); }; /************************************************ * App contents refresh */ var refreshing = 0; /** * Launches app content refresh * * @param {callback} cb_ok Treatment to apply on success * @param {callback} cb_error Treatment to apply on error * @returns {event|callback} : when refresh is finished : * - "refresh:end" event is triggered with a "result" object param * - callback cb_ok is called if success, with a "result" object param * - callback cb_error is called if error, with a "result" object param * * "result" object : { * ok: boolean : true if refresh is successful, * message: string : empty if success, error message if refresh fails, * data: object : empty if success, error object if refresh fails : * Use this result.data if you need specific info about the error. * See format_theme_event_data() for error object details. * } */ themeApp.refresh = function( cb_ok, cb_error ) { refreshing++; vent.trigger( 'refresh:start' ); App.sync( function() { RegionManager.buildMenu( function() { App.resetDefaultRoute(); /** * Use the 'go-to-default-route-after-refresh' to control whether * the default route should be automatically triggered after refresh. */ var go_to_default_route = App.getParam('go-to-default-route-after-refresh'); if( go_to_default_route ){ App.router.default_route(); } Backbone.history.stop(); Backbone.history.start({silent:false}); refreshing--; vent.trigger( 'refresh:end', format_result_data(true) ); if ( cb_ok ) { cb_ok(); } }, true ); }, function( error ) { refreshing--; var formated_error = format_theme_event_data( error.event, error ); if ( cb_error ) { cb_error( formated_error ); } var result = format_result_data(false,formated_error.message,formated_error); vent.trigger( 'refresh:end', result ); }, true ); }; themeApp.isRefreshing = function() { return refreshing > 0; }; /************************************************ * App navigation */ themeApp.navigate = function( navigate_to_fragment ) { App.router.navigate( navigate_to_fragment, { trigger: true } ); }; themeApp.navigateToDefaultRoute = function() { App.router.default_route(); }; /************************************************ * Back button */ /** * Automatically shows and hide Back button according to current screen (list, single, page, comments, etc...) * Use only if back button is not refreshed at each screen load! (otherwhise $go_back_btn will not be set correctly). * @param $go_back_btn Back button jQuery DOM element */ themeApp.setAutoBackButton = function( $go_back_btn, do_before_auto_action ) { RegionManager.on( 'screen:showed', function( current_screen, view ) { var display = themeApp.getBackButtonDisplay(); if ( display == 'show' ) { if ( do_before_auto_action != undefined ) { do_before_auto_action( true ); } $go_back_btn.show(); themeApp.updateBackButtonEvents( $go_back_btn ); } else if ( display == 'hide' ) { if ( do_before_auto_action != undefined ) { do_before_auto_action( false ); } themeApp.updateBackButtonEvents( $go_back_btn ); $go_back_btn.hide(); } } ); }; /** * To know if the back button can be displayed on the current screen, * according to app history. Use this to configure back button * manually if you don't use themeApp.setAutoBackButton(). */ themeApp.getBackButtonDisplay = function() { var display = ''; var previous_screen = App.getPreviousScreenData(); if ( !_.isEmpty( previous_screen ) ) { display = 'show'; } else { display = 'hide'; } return display; }; /** * Sets back buton click event. Use this to configure back button * manually if you don't use themeApp.setAutoBackButton(). * @param $go_back_btn Back button jQuery DOM element */ themeApp.updateBackButtonEvents = function( $go_back_btn ) { if ( $go_back_btn.length ) { var display = themeApp.getBackButtonDisplay(); if ( display == 'show' ) { $go_back_btn.unbind( 'click' ).click( function( e ) { e.preventDefault(); var prev_screen_link = App.getPreviousScreenLink(); themeApp.navigate( prev_screen_link ); } ); } else if ( display == 'hide' ) { $go_back_btn.unbind( 'click' ); } } }; /************************************************ * "Get more" link */ themeApp.getGetMoreLinkDisplay = function() { var get_more_link_data = { display: false, nb_left: 0 }; var current_screen = App.getCurrentScreenData(); if ( current_screen.screen_type == 'list' ) { var component = App.components.get( current_screen.component_id ); if ( component ) { var component_data = component.get( 'data' ); if ( component_data.hasOwnProperty( 'ids' ) ) { var nb_left = component_data.total - component_data.ids.length; get_more_link_data.nb_left = nb_left; get_more_link_data.display = nb_left > 0; } } } return get_more_link_data; }; themeApp.getMoreComponentItems = function( do_after ) { var current_screen = App.getCurrentScreenData(); if ( current_screen.screen_type == 'list' ) { App.getMoreOfComponent( current_screen.component_id, function( new_items, is_last, data ) { var current_archive_view = RegionManager.getCurrentView(); current_archive_view.addPosts( new_items ); current_archive_view.render(); do_after( is_last, new_items, data.nb_left ); } ); } }; /************************************************ * DOM element auto class */ /** * Sets class to the given DOM element according to the given current screen. * If element is not provided, defaults to <body>. */ var setContextClass = function( current_screen, element_id ) { if ( !_.isEmpty( current_screen ) ) { var $element = element_id == undefined ? $( 'body' ) : $( '#' + element_id ); $element.removeClass( function( index, css ) { return ( css.match( /\app-\S+/g ) || [ ] ).join( ' ' ); } ); $element.addClass( 'app-' + current_screen.screen_type ); $element.addClass( 'app-' + current_screen.fragment ); } }; /** * Adds class on given DOM element according to the current screen. * If element is not provided, defaults to <body>. * @param activate Set to true to activate */ themeApp.setAutoContextClass = function( activate, element_id ) { if ( activate ) { RegionManager.on( 'screen:showed', function( current_screen ) { setContextClass( current_screen, element_id ); } ); setContextClass( App.getCurrentScreenData(), element_id ); } //TODO : handle deactivation! }; /************************************************ * Screen transitions */ themeApp.getTransitionDirection = function( current_screen, previous_screen ) { var transition = 'replace'; if ( current_screen.screen_type == 'list' || current_screen.screen_type == 'custom-component' ) { if ( previous_screen.screen_type == 'single' ) { transition = 'right'; } else { transition = 'replace'; } } else if ( current_screen.screen_type == 'single' ) { if ( previous_screen.screen_type == 'list' || previous_screen.screen_type == 'custom-component' ) { transition = 'left'; } else if ( previous_screen.screen_type == 'comments' ) { transition = 'right'; } else { transition = 'replace'; } } else if ( current_screen.screen_type == 'comments' ) { transition = 'left'; } else { transition = 'replace'; } return transition; }; themeApp.setAutoScreenTransitions = function( transition_replace, transition_left, transition_right ) { themeApp.setParam( 'custom-screen-rendering', true ); themeApp.action( 'screen-transition', function( $deferred, $wrapper, $current, $next, current_screen, previous_screen ) { var direction = themeApp.getTransitionDirection( current_screen, previous_screen ); switch ( direction ) { case 'left': transition_left( $wrapper, $current, $next, $deferred ); break; case 'right': transition_right( $wrapper, $current, $next, $deferred ); break; case 'replace': transition_replace( $wrapper, $current, $next, $deferred ); break; default: transition_replace( $wrapper, $current, $next, $deferred ); break; } ; } ); }; /************************************************ * App network management */ /** * Retrieve network state : "online", "offline" or "unknown" * If full_info is passed and set to true, detailed connexion info is * returned (Wifi, 3G etc...). * * @param boolean full_info Set to true to get detailed connexion info * @returns string "online", "offline" or "unknown" */ themeApp.getNetworkState = function(full_info) { return PhoneGap.getNetworkState(full_info); }; /************************************************ * App infos management */ themeApp.showCustomPage = function( template, data ) { if ( template === undefined ) { template = 'custom'; } App.showCustomPage( template, data ); }; //Use exports so that theme-tpl-tags and theme-app (which depend on each other, creating //a circular dependency for requirejs) can both be required at the same time //(in theme functions.js for example) : _.extend( exports, themeApp ); } );
define( function( require, exports ) { "use strict"; var _ = require( 'underscore' ), Backbone = require( 'backbone' ), RegionManager = require( 'core/region-manager' ), Utils = require( 'core/app-utils' ), Config = require( 'root/config' ), Messages = require( 'core/messages' ), App = require( 'core/app' ), Hooks = require( 'core/lib/hooks' ), TemplateTags = require( 'core/theme-tpl-tags' ), PhoneGap = require( 'core/phonegap/utils' ); var themeApp = { }; /************************************************ * Events management */ /** * Theme event aggregator */ var vent = _.extend( { }, Backbone.Events ); /** * Allows themes (and addons) to trigger events * @param string event Event id * @param JSON object data */ themeApp.trigger = function( event, data ) { vent.trigger( event, data ); }; /** * Aggregate App and RegionManager events */ themeApp.on = function( event, callback ) { if ( _.contains( [ 'screen:leave', 'screen:showed', 'screen:before-transition', 'menu:refresh', 'header:render', 'waiting:start', 'waiting:stop' ], event ) ) { //Proxy RegionManager events : RegionManager.on( event, callback ); } else { vent.on( event, callback ); } }; /** * Proxy App events to theme events * * @param {string} event App event id * @param {object} data App event data * @returns {event} Triggers theme event based on App core event */ App.on( 'all', function( event, data ) { var theme_event_data = format_theme_event_data( event, data ); /** * "stop-theme-event" filter : use this filter to avoid an event from triggering in the theme. * Useful to deactivate some error events display for exemple. * * @param {boolean} Whether to stop the event or not. Default false. * @param {JSON Object} theme_event_data : Theme event data object * @param {String} event : Original (internal) event name */ var stop_theme_event = Hooks.applyFilters( 'stop-theme-event', false, [theme_event_data, event] ); if ( !stop_theme_event ) { if ( theme_event_data.type == 'error' || theme_event_data.type == 'info' || theme_event_data.type == 'network' ) { //2 ways of binding to error and info events : vent.trigger( event, theme_event_data ); //Ex: bind directly to 'info:no-content' vent.trigger( theme_event_data.type, theme_event_data ); //Ex: bind to general 'info', then filter with if( info.event == 'no-content' ) } } } ); /** * Formats App core events feedbacks in a themes friendly way. * * @param {string} event App event id (example "error:synchro:ajax") * @param {object} data * @returns {object} { * event: string : theme event id (example : "synchro:ajax"), * type: string : 'error' | 'info', * message: string : error or info message * data: object : original core event data : { * type: string : 'ajax' | 'ws-data' | 'not-found' | 'wrong-data', * where: string : core function where the event occured * message: string : message associated to the event * data: object : data associated to the core event * } * } */ var format_theme_event_data = function( event, data ) { var theme_event_data = { event: event, type: '', subtype: data !== undefined && data.hasOwnProperty( 'type' ) ? data.type : '', message: '', data: data }; if ( event.indexOf( 'error:' ) === 0 ) { theme_event_data.type = 'error'; theme_event_data.event = event.replace( 'error:', '' ); if ( data.type == 'ajax' ) { theme_event_data.message = Messages.get('error_remote_connexion_failed'); } else { theme_event_data.message = Messages.get('error_occured_undefined'); } } else if ( event.indexOf( 'info:' ) === 0 ) { theme_event_data.type = 'info'; theme_event_data.event = event.replace( 'info:', '' ); if ( event == 'info:no-content' ) { theme_event_data.message = Messages.get('info_no_content'); } } else if ( event.indexOf( 'network:' ) === 0 ) { theme_event_data.type = 'network'; theme_event_data.event = event.replace( 'network:', '' ); if( event == 'network:online' ) { theme_event_data.message = Messages.get('info_network_online'); }else if( event == 'network:offline' ) { theme_event_data.message = Messages.get('info_network_offline'); } } /** * "theme-event-message" filter : use this hook to customize event messages * * @param {String} Event message to customize * @param {JSON Object} theme_event_data : Theme event data object * @param {String} event : Original (internal) event name */ theme_event_data.message = Hooks.applyFilters( 'theme-event-message', theme_event_data.message, [theme_event_data, event] ); return theme_event_data; }; /************************************************ * Themes actions results */ /** * Formats data that is used in themes as the result of an event or * treatment. * * @param {boolean} ok * @param {string} message * @param {object} data * @returns object { * ok: boolean, * message: string, * data: object * } */ var format_result_data = function( ok, message, data ) { ok = ok === true || ok === 1 || ok === '1'; message = !_.isUndefined(message) && _.isString(message) ? message: ''; data = !_.isUndefined(data) ? data : {}; return { ok: ok, message: message, data: data }; }; /************************************************ * Filters, actions and Params management */ themeApp.filter = function( filter, callback, priority ) { Hooks.addFilter( filter, callback, priority ); } themeApp.action = function( action, callback, priority ) { Hooks.addAction( action, callback, priority ); } themeApp.setParam = function( param, value ) { App.setParam( param, value ); }; /************************************************ * App contents refresh */ var refreshing = 0; /** * Launches app content refresh * * @param {callback} cb_ok Treatment to apply on success * @param {callback} cb_error Treatment to apply on error * @returns {event|callback} : when refresh is finished : * - "refresh:end" event is triggered with a "result" object param * - callback cb_ok is called if success, with a "result" object param * - callback cb_error is called if error, with a "result" object param * * "result" object : { * ok: boolean : true if refresh is successful, * message: string : empty if success, error message if refresh fails, * data: object : empty if success, error object if refresh fails : * Use this result.data if you need specific info about the error. * See format_theme_event_data() for error object details. * } */ themeApp.refresh = function( cb_ok, cb_error ) { refreshing++; vent.trigger( 'refresh:start' ); App.sync( function() { RegionManager.buildMenu( function() { App.resetDefaultRoute(); /** * Use the 'go-to-default-route-after-refresh' to control whether * the default route should be automatically triggered after refresh. */ var go_to_default_route = App.getParam('go-to-default-route-after-refresh'); if( go_to_default_route ){ App.router.default_route(); } Backbone.history.stop(); Backbone.history.start({silent:false}); refreshing--; vent.trigger( 'refresh:end', format_result_data(true) ); if ( cb_ok ) { cb_ok(); } }, true ); }, function( error ) { refreshing--; var formated_error = format_theme_event_data( error.event, error ); if ( cb_error ) { cb_error( formated_error ); } var result = format_result_data(false,formated_error.message,formated_error); vent.trigger( 'refresh:end', result ); }, true ); }; themeApp.isRefreshing = function() { return refreshing > 0; }; /************************************************ * App navigation */ themeApp.navigate = function( navigate_to_fragment ) { App.router.navigate( navigate_to_fragment, { trigger: true } ); }; themeApp.navigateToDefaultRoute = function() { App.router.default_route(); }; /** * Reload current screen : re-trigger current route. */ themeApp.reloadCurrentScreen = function() { //Directly navigate to current fragment doesn't work (Backbone sees that //it is the same and doesn't re-trigger it!) : we have to navigate to a //false dummy route (without triggering the navigation, so it is invisible) //and then renavigate to original current route : var current_fragment = Backbone.history.getFragment(); App.router.navigate( 'WpakDummyRoute' ); //Route that does not exist App.router.navigate( current_fragment, { trigger: true } ); }; /** * Re-render current view WITHOUT re-triggering any route. */ themeApp.rerenderCurrentScreen = function() { var current_view = RegionManager.getCurrentView(); current_view.render(); }; /************************************************ * Back button */ /** * Automatically shows and hide Back button according to current screen (list, single, page, comments, etc...) * Use only if back button is not refreshed at each screen load! (otherwhise $go_back_btn will not be set correctly). * @param $go_back_btn Back button jQuery DOM element */ themeApp.setAutoBackButton = function( $go_back_btn, do_before_auto_action ) { RegionManager.on( 'screen:showed', function( current_screen, view ) { var display = themeApp.getBackButtonDisplay(); if ( display == 'show' ) { if ( do_before_auto_action != undefined ) { do_before_auto_action( true ); } $go_back_btn.show(); themeApp.updateBackButtonEvents( $go_back_btn ); } else if ( display == 'hide' ) { if ( do_before_auto_action != undefined ) { do_before_auto_action( false ); } themeApp.updateBackButtonEvents( $go_back_btn ); $go_back_btn.hide(); } } ); }; /** * To know if the back button can be displayed on the current screen, * according to app history. Use this to configure back button * manually if you don't use themeApp.setAutoBackButton(). */ themeApp.getBackButtonDisplay = function() { var display = ''; var previous_screen = App.getPreviousScreenData(); if ( !_.isEmpty( previous_screen ) ) { display = 'show'; } else { display = 'hide'; } return display; }; /** * Sets back buton click event. Use this to configure back button * manually if you don't use themeApp.setAutoBackButton(). * @param $go_back_btn Back button jQuery DOM element */ themeApp.updateBackButtonEvents = function( $go_back_btn ) { if ( $go_back_btn.length ) { var display = themeApp.getBackButtonDisplay(); if ( display == 'show' ) { $go_back_btn.unbind( 'click' ).click( function( e ) { e.preventDefault(); var prev_screen_link = App.getPreviousScreenLink(); themeApp.navigate( prev_screen_link ); } ); } else if ( display == 'hide' ) { $go_back_btn.unbind( 'click' ); } } }; /************************************************ * "Get more" link */ themeApp.getGetMoreLinkDisplay = function() { var get_more_link_data = { display: false, nb_left: 0 }; var current_screen = App.getCurrentScreenData(); if ( current_screen.screen_type == 'list' ) { var component = App.components.get( current_screen.component_id ); if ( component ) { var component_data = component.get( 'data' ); if ( component_data.hasOwnProperty( 'ids' ) ) { var nb_left = component_data.total - component_data.ids.length; get_more_link_data.nb_left = nb_left; get_more_link_data.display = nb_left > 0; } } } get_more_link_data = Hooks.applyFilters( 'get-more-link-display', get_more_link_data, [ current_screen ] ); return get_more_link_data; }; themeApp.getMoreComponentItems = function( do_after ) { var current_screen = App.getCurrentScreenData(); if ( current_screen.screen_type == 'list' ) { App.getMoreOfComponent( current_screen.component_id, function( new_items, is_last, data ) { var current_archive_view = RegionManager.getCurrentView(); current_archive_view.addPosts( new_items ); current_archive_view.render(); do_after( is_last, new_items, data.nb_left ); } ); } else { Hooks.doActions( 'get-more-component-items', [ current_screen, do_after ] ); } }; /************************************************ * "Live Query" Web Service */ /** * Call live query web service * * @param JSON Object web_service_params Any params that you want to send to the server. * The following params are automatically recognised and interpreted on server side : * - wpak_component_slug : { string | Array of string } components to make query on * - wpak_query_action : { string } 'get-component' to retrieve the full component, or 'get-items' to retrieve choosen component items * - wpak_items_ids : { int | array of int } If wpak_query_action = 'get-items' : component items ids to retrieve * @param options JSON Object : allowed settings : * - success Function Callback called on success * - error Function Callback called on error * - auto_interpret_result Boolean (default true). If false, web service answer must be interpreted in the cb_ok callback. * - type String : can be one of : * -- "update" : merge new with existing component data, * -- "replace" : delete current component data and replace with new * -- "replace-keep-global-items" (default) : for list components : replace component ids and merge global items * - persistent Boolean (default false). If true, new data is stored in local storage. */ themeApp.liveQuery = function( web_service_params, options ){ var cb_ok = null; if( options.success ) { cb_ok = options.success; delete options.success; } var cb_error = null; if( options.error ) { cb_error = options.error; delete options.error; } App.liveQuery( web_service_params, cb_ok, cb_error, options ); }; /** * Refresh component items from server. * * @param {int} component_id * @param {int | array of int} items_ids. If none provided, will refresh all component items. * @param {JSON Object} options : * - success {callback} * - error {callback} * - autoformat_answer {boolean} If true (default), the answer returned to the success * callback is automatically formated to return significant data. If false, the full * liveQuery answer is returned no matter what. */ themeApp.refreshComponentItems = function ( component_id, items_ids, options ) { var existing_component = App.components.get( component_id ); if( existing_component ) { //If no item id provided, refresh all component items : if ( items_ids === undefined || items_ids === '' || items_ids === 0 || ( _.isArray( items_ids ) && items_ids.length === 0 ) ) { var component_data = existing_component.get('data'); if ( component_data && component_data.ids ) { items_ids = component_data.ids; } else { items_ids = null; } } if ( items_ids !== null ) { themeApp.liveQuery( { wpak_component_slug : component_id, wpak_query_action : 'get-items', wpak_items_ids : items_ids }, { //Those are default liveQuery options values, but we set //them explicitly for more clarity : type : 'update', auto_interpret_result : true, persistent : false, //Callbacks : success : function ( answer ) { if ( options !== undefined && options.success ) { //If no globals in answer, return full answer var refreshed_items = answer; if ( !options.hasOwnProperty( 'autoformat_answer' ) || options.autoformat_answer === true ) { if ( answer.globals ) { //If globals in answer, return items indexed on globals : refreshed_items = answer.globals; var globals = []; _.each( answer.globals, function ( items, global ) { globals.push( global ); } ); if ( globals.length === 1 ) { //If only one global returned, return directly the corresponding items if ( _.isArray( items_ids ) ) { refreshed_items = answer.globals[globals[0]]; } else { //If only one item asked, return only this item : refreshed_items = _.first( _.toArray( answer.globals[globals[0]] ) ); } } } } options.success( refreshed_items ); } }, error : function ( answer_error ) { if ( options !== undefined && options.error ) { options.error( answer_error ); } } } ); } } }; /************************************************ * DOM element auto class */ /** * Sets class to the given DOM element according to the given current screen. * If element is not provided, defaults to <body>. */ var setContextClass = function( current_screen, element_id ) { if ( !_.isEmpty( current_screen ) ) { var $element = element_id == undefined ? $( 'body' ) : $( '#' + element_id ); $element.removeClass( function( index, css ) { return ( css.match( /\app-\S+/g ) || [ ] ).join( ' ' ); } ); $element.addClass( 'app-' + current_screen.screen_type ); $element.addClass( 'app-' + current_screen.fragment ); } }; /** * Adds class on given DOM element according to the current screen. * If element is not provided, defaults to <body>. * @param activate Set to true to activate */ themeApp.setAutoContextClass = function( activate, element_id ) { if ( activate ) { RegionManager.on( 'screen:showed', function( current_screen ) { setContextClass( current_screen, element_id ); } ); setContextClass( App.getCurrentScreenData(), element_id ); } //TODO : handle deactivation! }; /************************************************ * Screen transitions */ themeApp.getTransitionDirection = function( current_screen, previous_screen ) { var transition = 'replace'; if ( current_screen.screen_type == 'list' || current_screen.screen_type == 'custom-component' ) { if ( previous_screen.screen_type == 'single' ) { transition = 'right'; } else { transition = 'replace'; } } else if ( current_screen.screen_type == 'single' ) { if ( previous_screen.screen_type == 'list' || previous_screen.screen_type == 'custom-component' ) { transition = 'left'; } else if ( previous_screen.screen_type == 'comments' ) { transition = 'right'; } else { transition = 'replace'; } } else if ( current_screen.screen_type == 'comments' ) { transition = 'left'; } else { transition = 'replace'; } return transition; }; themeApp.setAutoScreenTransitions = function( transition_replace, transition_left, transition_right ) { themeApp.setParam( 'custom-screen-rendering', true ); themeApp.action( 'screen-transition', function( $deferred, $wrapper, $current, $next, current_screen, previous_screen ) { var direction = themeApp.getTransitionDirection( current_screen, previous_screen ); switch ( direction ) { case 'left': transition_left( $wrapper, $current, $next, $deferred ); break; case 'right': transition_right( $wrapper, $current, $next, $deferred ); break; case 'replace': transition_replace( $wrapper, $current, $next, $deferred ); break; default: transition_replace( $wrapper, $current, $next, $deferred ); break; } ; } ); }; /************************************************ * App network management */ /** * Retrieve network state : "online", "offline" or "unknown" * If full_info is passed and set to true, detailed connexion info is * returned (Wifi, 3G etc...). * * @param boolean full_info Set to true to get detailed connexion info * @returns string "online", "offline" or "unknown" */ themeApp.getNetworkState = function(full_info) { return PhoneGap.getNetworkState(full_info); }; /************************************************ * App custom pages and custom routes management */ themeApp.showCustomPage = function( template, data, id ) { if ( template === undefined ) { template = 'custom'; } if ( data === undefined ) { data = {}; } if ( id === undefined ) { id = 'auto-custom-page'; } App.showCustomPage( template, data, id ); }; themeApp.addCustomRoute = function( fragment, template, data ) { fragment = fragment.replace('#',''); if ( template === undefined ) { template = 'custom'; } if ( data === undefined ) { data = {}; } App.addCustomRoute( fragment, template, data ); }; themeApp.removeCustomRoute = function( fragment ) { fragment = fragment.replace('#',''); App.removeCustomRoute( fragment ); }; /************************************************** * Retrieve internal app data that can be useful in themes */ themeApp.getGlobalItems = function( global_key, items_ids, result_type ) { var items = null; if( result_type === undefined ) { result_type = 'slice'; } switch( result_type ) { case 'slice' : items = App.getGlobalItemsSlice( global_key, items_ids ); break; case 'array' : items = App.getGlobalItems( global_key, items_ids ); break; } return items; }; //Use exports so that theme-tpl-tags and theme-app (which depend on each other, creating //a circular dependency for requirejs) can both be required at the same time //(in theme functions.js for example) : _.extend( exports, themeApp ); } );
define( function( require, exports ) { "use strict"; var _ = require( 'underscore' ), Backbone = require( 'backbone' ), RegionManager = require( 'core/region-manager' ), Messages = require( 'core/messages' ), App = require( 'core/app' ), Hooks = require( 'core/lib/hooks' ), DynamicData = require( 'core/app-dynamic-data' ), TemplateTags = require( 'core/theme-tpl-tags' ), PhoneGap = require( 'core/phonegap/utils' ); var themeApp = { }; /************************************************ * Events management */ /** * Theme event aggregator */ var vent = _.extend( { }, Backbone.Events ); /** * Allows themes (and addons) to trigger events * @param string event Event id * @param JSON object data */ themeApp.trigger = function( event, data ) { vent.trigger( event, data ); }; /** * Aggregate ThemeApp and RegionManager events */ themeApp.on = function( event, callback ) { if ( _.contains( [ 'screen:leave', 'screen:showed', 'screen:before-transition', 'menu:rendered', 'header:rendered' ], event ) ) { //Proxy RegionManager events : RegionManager.on( event, callback ); } else if ( _.contains( [ 'refresh:start', 'refresh:end' ], event ) ) { //Proxy App events : App.on( event, callback ); } else { vent.on( event, callback ); } }; /** * Proxy App events to theme events * * @param {string} event App event id * @param {object} data App event data * @returns {event} Triggers theme event based on App core event */ App.on( 'all', function( event, data ) { var theme_event_data = format_theme_event_data( event, data ); /** * "stop-theme-event" filter : use this filter to avoid an event from triggering in the theme. * Useful to deactivate some error events display for exemple. * * @param {boolean} Whether to stop the event or not. Default false. * @param {JSON Object} theme_event_data : Theme event data object * @param {String} event : Original (internal) event name */ var stop_theme_event = Hooks.applyFilters( 'stop-theme-event', false, [theme_event_data, event] ); if ( !stop_theme_event ) { if ( theme_event_data.type == 'error' || theme_event_data.type == 'info' || theme_event_data.type == 'network' ) { //2 ways of binding to error and info events : vent.trigger( event, theme_event_data ); //Ex: bind directly to 'info:no-content' vent.trigger( theme_event_data.type, theme_event_data ); //Ex: bind to general 'info', then filter with if( info.event == 'no-content' ) } } } ); /** * Formats App core events feedbacks in a themes friendly way. * * @param {string} event App event id (example "error:synchro:ajax") * @param {object} data * @returns {object} { * event: string : theme event id (example : "synchro:ajax"), * type: string : 'error' | 'info', * message: string : error or info message * data: object : original core event data : { * type: string : 'ajax' | 'web-service' | 'not-found' | 'wrong-data', * where: string : core function where the event occured * message: string : message associated to the event * data: object : data associated to the core event * } * } */ var format_theme_event_data = function( event, data ) { var theme_event_data = { event: event, type: '', subtype: data !== undefined && data.hasOwnProperty( 'type' ) ? data.type : '', message: '', core_data: data }; if ( event.indexOf( 'error:' ) === 0 ) { theme_event_data.type = 'error'; theme_event_data.event = event.replace( 'error:', '' ); if ( data.type == 'ajax' ) { theme_event_data.message = Messages.get('error_remote_connexion_failed'); } else { theme_event_data.message = Messages.get('error_occured_undefined'); } } else if ( event.indexOf( 'info:' ) === 0 ) { theme_event_data.type = 'info'; theme_event_data.event = event.replace( 'info:', '' ); if ( event == 'info:no-content' ) { theme_event_data.message = Messages.get('info_no_content'); } } else if ( event.indexOf( 'network:' ) === 0 ) { theme_event_data.type = 'network'; theme_event_data.event = event.replace( 'network:', '' ); if( event == 'network:online' ) { theme_event_data.message = Messages.get('info_network_online'); }else if( event == 'network:offline' ) { theme_event_data.message = Messages.get('info_network_offline'); } } /** * "theme-event-message" filter : use this hook to customize event messages * * @param {String} Event message to customize * @param {JSON Object} theme_event_data : Theme event data object * @param {String} event : Original (internal) event name */ theme_event_data.message = Hooks.applyFilters( 'theme-event-message', theme_event_data.message, [theme_event_data, event] ); return theme_event_data; }; /************************************************ * Themes actions results */ /** * Formats data that is used in themes as the result of an event or * treatment. * * @param {boolean} ok * @param {string} message * @param {object} data * @returns object { * ok: boolean, * message: string, * data: object * } */ var format_result_data = function( ok, message, data ) { ok = ok === true || ok === 1 || ok === '1'; message = !_.isUndefined(message) && _.isString(message) ? message: ''; data = !_.isUndefined(data) ? data : {}; return { ok: ok, message: message, data: data }; }; /************************************************ * Filters, actions and Params management */ themeApp.filter = function( filter, callback, priority ) { Hooks.addFilter( filter, callback, priority ); }; themeApp.action = function( action, callback, priority ) { Hooks.addAction( action, callback, priority ); }; themeApp.setParam = function( param, value ) { App.setParam( param, value ); }; /************************************************ * App contents refresh */ var refreshing = 0; /** * Launches app content refresh * * @param {callback} cb_ok Treatment to apply on success * @param {callback} cb_error Treatment to apply on error * @returns {event|callback} : when refresh is finished : * - "refresh:end" event is triggered with a "result" object param * - callback cb_ok is called if success, with a "result" object param * - callback cb_error is called if error, with a "result" object param * * "result" object : { * ok: boolean : true if refresh is successful, * message: string : empty if success, error message if refresh fails, * data: object : empty if success, error object if refresh fails : * Use this result.data if you need specific info about the error. * See format_theme_event_data() for error object details. * } */ themeApp.refresh = function( cb_ok, cb_error ) { refreshing++; App.sync( function( deferred ) { RegionManager.buildMenu( function() { App.resetDefaultRoute(); /** * Use the 'go-to-default-route-after-refresh' to control whether * the default route should be automatically triggered after refresh. */ var go_to_default_route = App.getParam('go-to-default-route-after-refresh'); if( go_to_default_route ){ App.router.default_route(); } Backbone.history.stop(); Backbone.history.start({silent:false}); refreshing--; if ( deferred ) { deferred.resolve( format_result_data( true ) ); //Triggers refresh:end } if ( cb_ok ) { cb_ok(); } }, true ); }, function( error, deferred ) { refreshing--; var formated_error = format_theme_event_data( error.event, error ); if ( cb_error ) { cb_error( formated_error ); } var result = format_result_data(false,formated_error.message,formated_error); if ( deferred ) { deferred.reject( result ); //Triggers refresh:end } }, true ); }; themeApp.isRefreshing = function() { return refreshing > 0; }; /************************************************ * App navigation */ themeApp.navigate = function( navigate_to_fragment ) { if ( !App.isLaunching() ) { //Don't allow to navigate from theme if app is launching App.router.navigate( navigate_to_fragment, { trigger: true } ); } }; themeApp.navigateToDefaultRoute = function() { if ( !App.isLaunching() ) { //Don't allow to navigate from theme if app is launching App.router.default_route(); } }; themeApp.navigateToPreviousScreen = function() { var prev_screen_link = App.getPreviousScreenLink(); vent.trigger( 'navigate:previous-screen', { previous_screen: App.getPreviousScreenData(), current_screen: App.getCurrentScreenData(), previous_screen_link: prev_screen_link }); themeApp.navigate( prev_screen_link ); }; /** * Reload current screen : re-trigger current route. */ themeApp.reloadCurrentScreen = function() { //Directly navigate to current fragment doesn't work (Backbone sees that //it is the same and doesn't re-trigger it!) : we have to navigate to a //false dummy route (without triggering the navigation, so it is invisible) //and then renavigate to original current route : var current_fragment = Backbone.history.getFragment(); App.router.navigate( 'WpakDummyRoute' ); //Route that does not exist App.router.navigate( current_fragment, { trigger: true } ); }; /** * Re-render current view WITHOUT re-triggering any route. */ themeApp.rerenderCurrentScreen = function() { var current_view = RegionManager.getCurrentView(); current_view.render(); }; /** * Retrieve current Backbone view object */ themeApp.getCurrentView = function() { var current_view = RegionManager.getCurrentView(); return current_view; }; /************************************************ * Back button */ /** * To know if the back button can be displayed on the current screen, * according to app history. Use this to configure back button * manually if you don't use themeApp.setAutoBackButton(). */ themeApp.getBackButtonDisplay = function() { var display = ''; var previous_screen = App.getPreviousScreenData(); if ( !_.isEmpty( previous_screen ) ) { display = 'show'; } else { display = 'hide'; } return display; }; /************************************************ * "Get more" link */ themeApp.getGetMoreLinkDisplay = function() { var get_more_link_data = { display: false, nb_left: 0 }; var current_screen = App.getCurrentScreenData(); if ( current_screen.screen_type == 'list' ) { var component = App.components.get( current_screen.component_id ); if ( component ) { var component_data = component.get( 'data' ); if ( component_data.hasOwnProperty( 'ids' ) ) { var nb_left = component_data.total - component_data.ids.length; get_more_link_data.nb_left = nb_left; get_more_link_data.display = nb_left > 0; } } } get_more_link_data = Hooks.applyFilters( 'get-more-link-display', get_more_link_data, [ current_screen ] ); return get_more_link_data; }; themeApp.getMoreComponentItems = function( cb_after, cb_error ) { var current_screen = App.getCurrentScreenData(); if ( current_screen.screen_type === 'list' ) { App.getMoreOfComponent( current_screen.component_id, function( data ) { var current_archive_view = RegionManager.getCurrentView(); current_archive_view.addPosts( data.new_items ); current_archive_view.render(); cb_after( data.is_last, data.new_items, data.nb_left ); }, function( error ) { var get_more_link_data = themeApp.getGetMoreLinkDisplay(); cb_error( format_theme_event_data( error.event, error ), get_more_link_data ); } ); } }; /************************************************ * Comments */ /** * Displays the comments screen for a given post. * Retrieves the post comments from server or from memory if already cached, * then navigate to #comments-[post_id]. * Using this function allows to use success and error callbacks (cb_ok/cb_error), * which you can't do if you navigate directly to #comments-[post_id] in your theme. * * Note that the cb_ok() callback is called after comments are retrieved, but can't * be called after the comments view is rendered (as view rendering is done in router). * If you need to do something after the comments screen is showed, you can use * the 'screen:showed' event where you'll test if ( current_screen.screen_type === 'comments' ) ). * * @param {int} post_id Post we want to retrieve the comments for. * @param {function} cb_ok What to do if coments are retrieved ok * @param {function} cb_error What to do if an error occurs while retrieving comments */ themeApp.displayPostComments = function ( post_id, cb_ok, cb_error ) { App.getPostComments( post_id, function ( comments, post, item_global ) { cb_ok( comments.toJSON(), post.toJSON(), item_global ); themeApp.navigate( App.getScreenFragment( 'comments', { item_id: post_id } ) ); }, function ( error ) { cb_error( format_theme_event_data( error.event, error ) ); } ); }; /** * When on a comments screen, reloads the comments for the current post and * re-renders the view to display new comments. * * @param {function} cb_ok What to do when comment screen was updated successfully * @param {function} cb_error What to do if an error occurs while updating comment screen */ themeApp.updateCurrentCommentScreen = function ( cb_ok, cb_error ) { var current_screen_info = this.getCurrentScreenObject(); if ( current_screen_info.screen_type !== 'comments' ) { return; } //Retrieve post id corresponding to the current comments screen: var post_id = current_screen_info.post.id; var _this = this; //Reload post comments from server: App.getPostComments( post_id, function ( comments, post ) { //New comments loaded successfully //Update current view's comments with new comments var comments_view = RegionManager.getCurrentView(); comments_view.comments = comments; //Rerender screen: _this.rerenderCurrentScreen(); cb_ok( comments, post ); }, function ( error ) { cb_error( error ); }, true //To force post comments cache flush ); } /************************************************ * Components */ /** * Retrieve all app's components * * @returns {Array} Array of compoents as JSON objects */ themeApp.getComponents = function() { return App.getComponents(); }; /** * Retrieves the id of the component corresponding to the current screen, if any. * * @returns {string} Id of the current screen's component (usually a string slug) */ themeApp.getCurrentComponentId = function() { var current_component_id = ''; var current_screen = App.getCurrentScreenData(); if ( current_screen.component_id && App.componentExists( current_screen.component_id ) ) { current_component_id = current_screen.component_id; } return current_component_id; }; /** * Retrieves the component corresponding to the current screen, if any. * * @returns {JSON object} Current screen's component data */ themeApp.getCurrentComponent = function() { var current_component = null; var current_component_id = themeApp.getCurrentComponentId(); if ( current_component_id !== '' ) { current_component = App.getComponentData( current_component_id ); } return current_component; }; /************************************************ * "Live Query" Web Service */ /** * Call "Live Query" web service. * Allows to refresh app components (or choosen component items) dynamically from server. * * @param JSON Object web_service_params Any params that you want to send to the server. * The following params are automatically recognised and interpreted on server side : * - wpak_component_slug : { string | Array of string } components to make query on * - wpak_query_action : { string } 'get-component' to retrieve the full component, or 'get-items' to retrieve choosen component items * - wpak_items_ids : { int | array of int } If wpak_query_action = 'get-items' : component items ids to retrieve * @param options JSON Object : allowed settings : * - success Function Callback called on success * - error Function Callback called on error * - auto_interpret_result Boolean (default true). If false, web service answer must be interpreted in the cb_ok callback. * - type String : can be one of : * -- "update" : merge new with existing component data, * -- "replace" : delete current component data, empty the corresponding global, and replace with new * -- "replace-keep-global-items" (default) : for list components : replace component items ids and merge global items * - persistent Boolean (default false). If true, new data is stored in local storage. */ themeApp.liveQuery = function( web_service_params, options ){ var cb_ok = null; if( options.success ) { cb_ok = options.success; delete options.success; } var cb_error = null; if( options.error ) { cb_error = options.error; delete options.error; } App.liveQuery( web_service_params, cb_ok, cb_error, options ); }; /** * Refresh component data and items from server. * * @param {JSON Object} options : * - component_id {string} String (slug) idendifier provided when creating the component. * If none provided, will retrieve current screen component's id, if any. * - success {callback} * - error {callback} * - refresh_type {string} Can be: * -- "update" : merge new with existing component data, * -- "replace" : delete current component data, empty the corresponding global, and replace with new * -- "replace-keep-global-items" (default) : for list components : replace component items ids and merge global items * - persistent {boolean} (default false). If true, new data is stored in local storage. */ themeApp.refreshComponent = function( options ) { options = options || {}; var component_id = options.component_id ? options.component_id : themeApp.getCurrentComponentId(); var live_query_options = {}; if ( options.success ) { live_query_options.success = options.success; //Will be passed (answer, update_results) } if ( options.error ) { live_query_options.error = options.error; //Will be passed (answer_error) } if ( options.refresh_type ) { //If none, will default to 'replace-keep-global-items' live_query_options.type = options.refresh_type; } if ( options.hasOwnProperty( 'persistent' ) ) { //If none, will default to false live_query_options.persistent = options.persistent; } themeApp.liveQuery( { wpak_component_slug: component_id, wpak_query_action: 'get-component' }, live_query_options ); }; /** * Refresh given component items from server. * This only refreshes choosen items of a component, not the component itself. * Use ThemeApp.refreshComponent() to update component data in addition to its items. * * @param {string} component_id String (slug) idendifier provided when creating the component * If none provided, will retrieve current screen component's id, if any. * @param {int | array of int} items_ids If none provided, will refresh all component items. * @param {JSON Object} options : * - success {callback} * - error {callback} * - autoformat_answer {boolean} If true (default), the answer returned to the success * callback is automatically formated to return significant data (array of items). If false, the full * liveQuery answer is returned no matter what. * - refresh_type {string} Can be: * -- "update" : merge new with existing component data, * -- "replace" : delete current component data, empty the corresponding global, and replace with new * -- "replace-keep-global-items" (default) : for list components : replace component items ids and merge global items * - persistent {boolean} (default false). If true, new data is stored in local storage. */ themeApp.refreshComponentItems = function ( component_id, items_ids, options ) { if ( component_id === undefined ) { component_id = TemplateTags.getCurrentComponentId(); } var existing_component = App.components.get( component_id ); if( existing_component ) { //If no item id provided, refresh all component items : if ( items_ids === undefined || items_ids === '' || items_ids === 0 || ( _.isArray( items_ids ) && items_ids.length === 0 ) ) { var component_data = existing_component.get('data'); if ( component_data && component_data.ids ) { items_ids = component_data.ids; } else { items_ids = null; } } if ( items_ids !== null ) { themeApp.liveQuery( { wpak_component_slug : component_id, wpak_query_action : 'get-items', wpak_items_ids : items_ids }, { type : options.hasOwnProperty( 'refresh_type' ) ? options.refresh_type : 'replace-keep-global-items', persistent : options.hasOwnProperty( 'persistent' ) ? options.persistent : false, auto_interpret_result : true, //Callbacks : success : function ( answer ) { if ( options !== undefined && options.success ) { //If no globals in answer, return full answer var refreshed_items = answer; if ( !options.hasOwnProperty( 'autoformat_answer' ) || options.autoformat_answer === true ) { if ( answer.globals ) { //If globals in answer, return items indexed on globals : refreshed_items = answer.globals; var globals = []; _.each( answer.globals, function ( items, global ) { globals.push( global ); } ); if ( globals.length === 1 ) { //If only one global returned, return directly the corresponding items if ( _.isArray( items_ids ) ) { refreshed_items = answer.globals[globals[0]]; } else { //If only one item asked, return only this item : refreshed_items = _.first( _.toArray( answer.globals[globals[0]] ) ); } } } } options.success( refreshed_items ); } }, error : function ( answer_error ) { if ( options !== undefined && options.error ) { options.error( answer_error ); } } } ); } } }; /************************************************ * Dynamic data from synchronization web service */ themeApp.getDynamicData = function ( field ) { return DynamicData.getDynamicData( field ); }; /************************************************ * DOM element auto class */ /** * Sets class to the given DOM element according to the given current screen. * If element is not provided, defaults to <body>. */ var setContextClass = function( current_screen, element_id ) { if ( !_.isEmpty( current_screen ) ) { var $element = element_id == undefined ? $( 'body' ) : $( '#' + element_id ); $element.removeClass( function( index, css ) { return ( css.match( /\app-\S+/g ) || [ ] ).join( ' ' ); } ); $element.addClass( 'app-' + current_screen.screen_type ); $element.addClass( 'app-' + current_screen.fragment ); } }; /** * Adds class on given DOM element according to the current screen. * If element is not provided, defaults to <body>. * @param activate Set to true to activate */ themeApp.setAutoContextClass = function( activate, element_id ) { if ( activate ) { RegionManager.on( 'screen:showed', function( current_screen ) { setContextClass( current_screen, element_id ); } ); setContextClass( App.getCurrentScreenData(), element_id ); } //TODO : handle deactivation! }; /************************************************ * Screen transitions */ /** * Returns the transition direction ("next-screen", "previous-screen", "default" or customized) according * to current and previous screen. * Use this to know the current transition direction when hooking on 'screen-transition' action hook. * And use the following 'transition-direction' hook to define your own kind of transition directions. * * @param {Object} current_screen : The screen we're leaving. * @param {Object} next_screen : The screen that is going to be displayed after transition * @returns {String} Transition direction (default 'default', 'next-screen' and 'previous-screen' but * can be customized with the "transition-direction" filter). */ themeApp.getTransitionDirection = function( current_screen, next_screen ) { var transition = 'default'; if ( next_screen.screen_type == 'list' || next_screen.screen_type == 'custom-component' ) { if ( current_screen.screen_type == 'single' ) { transition = 'previous-screen'; } else { transition = 'default'; } } else if ( next_screen.screen_type == 'single' ) { if ( current_screen.screen_type == 'list' || current_screen.screen_type == 'custom-component' ) { transition = 'next-screen'; } else if ( current_screen.screen_type == 'comments' ) { transition = 'previous-screen'; } else { transition = 'default'; } } else if ( next_screen.screen_type == 'comments' ) { transition = 'next-screen'; } else { transition = 'default'; } /** * "transition-direction" filter : use this filter to customize transitions * directions according to what are current (ie asked) and previous screen. * * @param {string} Transition direction to override if needed. * @param {Object} current_screen : The screen we're leaving. * @param {Object} next_screen : The screen that is going to be displayed after transition. */ var transition = Hooks.applyFilters( 'transition-direction', transition, [current_screen, next_screen] ); return transition; }; /************************************************ * App network management */ /** * Retrieve network state : "online", "offline" or "unknown" * If full_info is passed and set to true, detailed connexion info is * returned (Wifi, 3G etc...). * * @param boolean full_info Set to true to get detailed connexion info * @returns string "online", "offline" or "unknown" */ themeApp.getNetworkState = function(full_info) { return PhoneGap.getNetworkState(full_info); }; /************************************************ * App custom pages and custom routes management */ themeApp.showCustomPage = function( template, data, fragment, silent ) { if ( template === undefined ) { template = 'custom'; } if ( data === undefined ) { data = {}; } if ( fragment === undefined ) { fragment = 'auto-custom-page'; } if ( silent === undefined ) { silent = true; } App.showCustomPage( template, data, fragment, silent ); }; themeApp.addCustomRoute = function( fragment, template, data ) { fragment = fragment.replace('#',''); if ( template === undefined ) { template = 'custom'; } if ( data === undefined ) { data = {}; } App.addCustomRoute( fragment, template, data ); }; themeApp.removeCustomRoute = function( fragment ) { fragment = fragment.replace('#',''); App.removeCustomRoute( fragment ); }; /************************************************** * Retrieve internal app data that can be useful in themes */ /** * Retrieves a list of items (posts for example) from local storage * * @param {array} items_ids IDs of the items to retrieve * @param {string} global_key (Optional) global to retrieve the items from: 'posts' (default) or 'pages'. * @param {string} result_type 'slice' to retrieve a Backbone Collection (default), 'array' to retrieve an array. * @returns {Backbone Collection | Array | null} items list */ themeApp.getItems = function( items_ids, global_key, result_type ) { var items = null; global_key = global_key || 'posts'; result_type = result_type || 'slice'; switch( result_type ) { case 'slice' : items = App.getGlobalItemsSlice( global_key, items_ids ); break; case 'array' : items = App.getGlobalItems( global_key, items_ids ); break; } return items; }; /** * Retrieves an item (post for example) from local storage * * @param {int} item_id Post ID of the post to retrieve * @param {string} global_key (Optional) global to retrieve the item from: 'posts' (default) or 'pages'. * @returns {JSON Object | null} item (post or page) object if found, null if no post found with the given item_id. */ themeApp.getItem = function( item_id, global_key ) { global_key = global_key || 'posts'; return App.getGlobalItem( global_key, item_id ); }; /** * Retrieve items (posts/pages etc) from remote server and merge them into existing app's items. * * @param Array items array of ids of pages/posts to retrieve. * @param JSON Object options: * - component_id: Int (optional) Slug of the component we want to retrieve items for. * If not provided, the first component of "component_type" found * will be used. * - component_type: String (optional) Type of component ("posts-list", "pages") we want to * retrieve items for. Only useful if component_id is not provided. * If not provided, defaults to "posts-list". * - persistent: Boolean (optional) Whether to persist retrieved items to local storage. * Defaults to true. * - success: Callback (optional) Called if items are retrieved successfully * - error: Callback (optional) Called if an error occured while retrieving items from server. * App error events are also triggered in that case. */ themeApp.getItemsFromRemote = function( items_ids, options ) { App.getItemsFromRemote( items_ids, options ); }; /** * Retrieves current screen infos * * (Alias of theme-tpl-tags::getCurrentScreen(): because getting current * screen is needed very often, we need it both sides) * * @return JSON object containing : * - screen_type : list, single, comments, page * - fragment : unique screen url id (what's after # in url) * - component_id : component slug id, if displaying a component screen (list, page) * - item_id : current item id, if displaying single content (post,page) * - label : current item label (title of component, title of post) * - data : contains more specific data depending on which screen type is displayed * > total : total number of posts for lists * > query : query vars used to retrieve contents (taxonomy, terms...) * > ids : id of posts displayed in lists * > any other specific data depending on currently displayed component */ themeApp.getCurrentScreen = function() { return App.getCurrentScreenData(); }; /** * Retrieves the screen that is before current screen in app's history. * For example if your history is "Screen A > Screen B > Screen C then back button to Screen B", * getPreviousScreenInHistory() returns Screen A, which is where we will go from Screen B * if we press back button. * To retrieve Screen C in this case, you would use getPreviousScreen(). * * @returns {screen_type:string} */ themeApp.getPreviousScreenInHistory = function() { return App.getPreviousScreenData(); }; /** * Retrieves the screen that was displayed just before current screen. * For example if your history is "Screen A > Screen B > Screen C then back button to Screen B", * Screen C has been poped from history stack but you can still access it via * this getPreviousScreen() function. * * @returns {Screen object} Previous screen * (see themeApp.getCurrentScreen() for screen object structure) */ themeApp.getPreviousScreen = function() { return App.getPreviousScreenMemoryData(); }; /** * Returns app's current history stack. * For example if your history is "Screen A > Screen B > Screen C", * the history stack is [Screen object A, Screen object B, Screen object C]. * Then if you press back button, the history stack becomes * [Screen object A, Screen object B] (Screen object C has been poped). * * @returns {Array} App's history stack: array of screen objects * (see themeApp.getCurrentScreen() for screen object structure) */ themeApp.getHistory = function() { return App.getHistory(); }; /** * Returns the last action that was made on app's history stack (push, pop, empty or empty-then-push). * For example if your history is "List > Single then go to Single's Comments screen", * the last history action is a "push" (Comments screen was pushed after Single screen in the history stack). * * Knowing last history action is particularly useful when implementing custom transitions * for example to differentiate transitions between "going from Single A to Single B" and * "going back from Single B to Single A". * * Note that the mapping between "Going from a screen to another screen" and "what * history action is triggered" is customizable using the "make-history" JS filter. * * @returns {String} Last history action : push, pop, empty or empty-then-push */ themeApp.getLastHistoryAction = function() { return App.getLastHistoryAction(); }; /** * Retrieves useful data corresponding to the object that is currently displayed. * The returned set of data is a custom selection of data that can be found in * "Screen data" (ThemeApp.getCurrentScreen()) and "View data" (RegionManager.getCurrentView()). * * @returns JSON Object depending on current screen: * - for lists: object containing: title (list title), posts (list of posts), ids (=post ids), total, component_id, query * - for single: post object: id, post_type, date, title, content, excerpt, thumbnail, author, nb_comments, slug, permalink * - for comments: object containing: post (post we retrieve the comments for) and comments (list of comments for this post) * - for pages: page object: id, post_type, date, title, content, excerpt, thumbnail, author, nb_comments, slug, permalink, tree_data, component (page's component object) * - for custom pages: object containing: id, route, title (if custom page data contains a 'title' property), data (custom page data), template. * - for custom components: object containing: component_id, title, route, data, template * - for all: field 'screen_type': can be: 'list', 'single', 'comments', 'page', 'custom-page', 'custom-component' */ themeApp.getCurrentScreenObject = function() { var screen_object = {}; var screen_data = App.getCurrentScreenData(); var current_view = RegionManager.getCurrentView(); switch( screen_data.screen_type ) { case 'list': //For lists, build a custom screen object from screen data and current view data: screen_object = { title: screen_data.label, component_id: screen_data.component_id, posts: current_view.posts.toJSON() }; _.extend( screen_object, screen_data.data ); //Adds ids, query, total break; case 'single': //For single, just return the current post object: if ( screen_data.data.post ) { screen_object = screen_data.data.post; } else if ( screen_data.data.item ) { screen_object = screen_data.data.item; } break; case 'comments': //For comments, build a custom screen object from screen data and current view data: screen_object = { post: current_view.post.toJSON(), comments: current_view.comments.toJSON() }; break; case 'page': //For page, just return the current page object: screen_object = screen_data.data.post; screen_object.component = TemplateTags.getComponent( screen_data.component_id ); break; case 'custom-page': //For custom pages, return page id, page route, page custom data, and page template: //(id and route are the same thing, because a custom page is identified by its route) screen_object = { id: screen_data.item_id, route: screen_data.item_id, title: current_view.custom_page_data && current_view.custom_page_data.hasOwnProperty( 'title' ) ? current_view.custom_page_data.title : '', data: current_view.custom_page_data ? current_view.custom_page_data : {}, template: current_view.template_name }; break; case 'custom-component': //For custom components, return component_id, title, route, data, template: screen_object = { component_id: screen_data.component_id, title: screen_data.label, route: screen_data.fragment, data: screen_data.data, template: current_view.template_name }; break; }; screen_object.screen_type = screen_data.screen_type; return screen_object; }; /** * Re-render Menu view */ themeApp.renderMenu = function() { var menu_view = RegionManager.getMenuView(); if ( menu_view ) { menu_view.render(); } }; //Use exports so that theme-tpl-tags and theme-app (which depend on each other, creating //a circular dependency for requirejs) can both be required at the same time //(in theme functions.js for example) : _.extend( exports, themeApp ); } );