/*! drupalgap 2017-10-18 */ // Initialize the drupalgap json object. var drupalgap = drupalgap || drupalgap_init(); // Do not remove this line. // Init _GET for url path query strings. var _dg_GET = _dg_GET || {}; /** * Initializes the drupalgap json object. * @return {Object} */ function drupalgap_init() { var dg = { modules: { core: [ { name: 'comment' }, { name: 'contact' }, { name: 'entity' }, { name: 'field' }, { name: 'file' }, { name: 'image' }, { name: 'menu' }, { name: 'mvc' }, { name: 'node' }, { name: 'search' }, { name: 'system' }, { name: 'taxonomy' }, { name: 'user' }, { name: 'views' } ] }, module_paths: [], includes: [ { name: 'block' }, { name: 'common' }, { name: 'form' }, { name: 'go' }, { name: 'menu' }, { name: 'page' }, { name: 'region' }, { name: 'theme' }, { name: 'title' } ], online: false, destination: '', api: {}, back: false, /* moving backwards or not */ back_path: [], /* paths to move back to */ blocks: [], connected: false, // Becomes true once DrupalGap performs a System Connect call. content_types_list: {}, /* holds info about each content type */ date_formats: { }, /* @see system_get_date_formats() in Drupal core */ date_types: { }, /* @see system_get_date_types() in Drupal core */ entity_info: {}, field_info_fields: {}, field_info_instances: {}, field_info_extra_fields: {}, form_errors: {}, form_states: [], loading: false, /* indicates if the loading message is shown or not */ loader: 'loading', /* used to determine the jQM loader mode */ locale: {}, /* holds onto language json objects, keyed by language code */ messages: [], menus: {}, menu_links: {}, menu_router: {}, /* @todo - doesn't appear to be used at all, remove it */ mvc: { models: {}, views: {}, controllers: {} }, output: '', /* hold output generated by menu_execute_active_handler() */ page: { jqm_events: [], title: '', variables: {}, process: true, options: {} /* holds the current page's options, eg. reloadPage, etc. */ }, pages: [], /* Collection of page ids that are loaded into the DOM. */ path: '', /* The current menu path. */ remote_addr: null, /* php's $_SERVER['REMOTE_ADDR'] via system connect */ router_path: '', /* The current menu router path. */ services: {}, sessid: null, settings: {}, site_settings: {}, /* holds variable settings from the Drupal site */ taxonomy_vocabularies: false, /* holds vocabs from system connect */ theme_path: '', themes: [], theme_registry: {}, toast: { shown: false }, views: { ids: [] }, views_datasource: {} }; // Extend jDrupal as needed... // Forms will expire upon install and don't have an expiration time. if (!Drupal.cache_expiration) { Drupal.cache_expiration = {}; } if (!Drupal.cache_expiration.forms) { Drupal.cache_expiration.forms = {}; } // Finally return the JSON object. return dg; } /** * This is called once the element's onload is fired. */ function drupalgap_onload() { try { // At this point, the Drupal object has been initialized by jDrupal and the // app/settings.js file was loaded in . Let's add DrupalGap's modules // onto the Drupal JSON object. Remember, all of the module source code is // included via the makefile's bin generation. However, the core modules // hook_install() implementations haven't been called yet, so we add them to // the module listing so they can be invoked later on. var modules = [ 'drupalgap', 'block', 'comment', 'contact', 'entity', 'field', 'file', 'image', 'menu', 'mvc', 'node', 'search', 'system', 'taxonomy', 'user', 'views' ]; for (var i = 0; i < modules.length; i++) { var module = modules[i]; Drupal.modules.core[module] = module_object_template(module); } // Depending on the mode, we'll move on to _drupalgap_deviceready() // accordingly. By default we'll assume the mode is for phonegap, unless // otherwise specified by the settings.js file. If it is for phonegap, we'll // attach its device ready listener, otherwise we'll just move on. if (typeof drupalgap.settings.mode === 'undefined') { drupalgap.settings.mode = 'phonegap'; } switch (drupalgap.settings.mode) { case 'phonegap': document.addEventListener('deviceready', _drupalgap_deviceready, false); break; case 'web-app': _drupalgap_deviceready(); break; default: console.log( 'drupalgap_onload - unknown mode (' + drupalgap.settings.mode + ')' ); return; break; } } catch (error) { console.log('drupalgap_onload - ' + error); } } /** * Implements PhoneGap's deviceready(). */ function _drupalgap_deviceready() { try { // Set some jQM properties to better handle the back button on iOS9. if ( typeof device !== 'undefined' && device.platform === "iOS" && parseInt(device.version) === 9 ) { $.mobile.hashListeningEnabled = false; $.mobile.pushStateEnabled = false; } // The device is now ready, it is now safe for DrupalGap to start... drupalgap_bootstrap(); // Verify site path is set. if (!Drupal.settings.site_path || Drupal.settings.site_path == '') { var msg = t('No site_path to Drupal set in the app/settings.js file!'); drupalgap_alert(msg, { title: t('Error') }); return; } // Device is ready, let's call any implementations of hook_deviceready(). If any implementation returns // false, that means they would like to take over the rest of the deviceready procedure (aka the System // Connect call) var proceed = true; var invocation_results = module_invoke_all('deviceready'); if (invocation_results && invocation_results.length > 0) { for (var i = 0; i < invocation_results.length; i++) { if (!invocation_results[i]) { proceed = false; break; } } } // If the device is offline, warn the user and then go to the offline page, unless someone implemented // hook_offline, then let them handle it. if (!drupalgap_has_connection()) { if (!module_implements('device_offline')) { if (drupalgap.settings.offline_message) { drupalgap_alert(drupalgap.settings.offline_message, { title: t('Offline'), alertCallback: function() { drupalgap_goto('offline'); } }); } else { drupalgap_goto('offline'); } } else { setTimeout(function() { module_invoke_all('device_offline'); }, 1); } } else if (proceed) { // Device is online and no one has taken over the deviceready, continue with the System Connect call. system_connect(_drupalgap_deviceready_options()); } } catch (error) { console.log('_drupalgap_deviceready - ' + error); } } /** * Builds the default system connect options. * @return {Object} */ function _drupalgap_deviceready_options() { try { var pageOptions = arguments[0] ? arguments[0] : {}; return { success: function(result) { // Set the connection and invoke hook_device_connected(). drupalgap.connected = true; module_invoke_all('device_connected'); // If there is a hash url present and it can be routed go directly to that page, // otherwise go to the app's front page. var path = ''; var hash = window.location.hash; if (hash.indexOf('#') != -1) { hash = hash.replace('#', ''); _drupalgap_goto_prepare_path(hash, true); var routedPath = drupalgap_get_path_from_page_id(hash); if (routedPath) { path = routedPath; } } drupalgap_goto(path, pageOptions); }, error: function(jqXHR, textStatus, errorThrown) { // Build an informative error message and display it. var msg = t('Failed connection to') + ' ' + Drupal.settings.site_path; if (errorThrown != '') { msg += ' - ' + errorThrown; } msg += ' - ' + t('Check your device\'s connection and check that') + ' ' + Drupal.settings.site_path + ' ' + t('is online.'); drupalgap_alert(msg, { title: t('Unable to Connect'), alertCallback: function() { drupalgap_goto('offline'); } }); } }; } catch (error) { console.log('_drupalgap_deviceready_options - ' + error); } } /** * Loads up all necessary assets to make DrupalGap ready. */ function drupalgap_bootstrap() { try { // Load up any contrib and/or custom modules (the DG core moodules have // already been loaded at this point), load the theme and all blocks. Then // build the menu router, load the menus, and build the theme registry. drupalgap_load_modules(); drupalgap_load_theme(); drupalgap_load_blocks(); drupalgap_load_locales(); menu_router_build(); drupalgap_menus_load(); drupalgap_theme_registry_build(); // Attach device back button handler (Android). document.addEventListener('backbutton', function(e) { drupalgap_back(); e.preventDefault(); }, false); } catch (error) { console.log('drupalgap_bootstrap - ' + error); } } /** * Loads any contrib or custom modules specifed in the settings.js file. Then * invoke hook_install() on all modules, including core. */ function drupalgap_load_modules() { try { var module_types = ['contrib', 'custom']; // We only need to load contrib and custom modules because core modules are // already included in the binary. for (var index in module_types) { if (!module_types.hasOwnProperty(index)) { continue; } var bundle = module_types[index]; // Let's be nice and try to load any old drupalgap.modules declarations // in developers settings.js files for a while, but throw a warning to // encourage them to update. This code can be removed after a few // releases to help developers get caught up without angering them. if ( drupalgap.modules && drupalgap.modules[bundle] && drupalgap.modules[bundle].length != 0 ) { for (var index in drupalgap.modules[bundle]) { if (!drupalgap.modules[bundle].hasOwnProperty(index)) { continue; } var module = drupalgap.modules[bundle][index]; if (module.name) { var msg = 'WARNING: The module "' + module.name + '" defined ' + 'in settings.js needs to be added to ' + 'Drupal.modules[\'' + bundle + '\'] instead! See ' + 'default.settings.js for examples on the new syntax!'; console.log(msg); Drupal.modules[bundle][module.name] = module; } } } for (var module_name in Drupal.modules[bundle]) { if (!Drupal.modules[bundle].hasOwnProperty(module_name)) { continue; } var module = Drupal.modules[bundle][module_name]; // If the module object is empty, initialize a module object. if ($.isEmptyObject(module)) { Drupal.modules[bundle][module_name] = module_object_template(module_name); module = Drupal.modules[bundle][module_name]; } // If the module's name isn't set, set it. if (!module.name) { Drupal.modules[bundle][module_name].name = module_name; module = Drupal.modules[bundle][module_name]; } // If the module is already loaded via the index.html file, just continue. if (typeof module.loaded !== 'undefined' && module.loaded) { continue; } // Determine module directory. var dir = drupalgap_modules_get_bundle_directory(bundle); module_base_path = dir + '/' + module.name; // Add module .js file to array of paths to load. var extension = module.minified ? '.min.js' : '.js'; module_path = module_base_path + '/' + module.name + extension; modules_paths = [module_path]; // If there are any includes with this module, add them to the // list of paths to include. if (module.includes != null && module.includes.length != 0) { for (var include_index in module.includes) { if (!module.includes.hasOwnProperty(include_index)) { continue; } var include_object = module.includes[include_index]; modules_paths.push( module_base_path + '/' + include_object.name + '.js' ); } } // Now load all the paths for this module. for (var modules_paths_index in modules_paths) { if (!modules_paths.hasOwnProperty(modules_paths_index)) { continue; } var modules_paths_object = modules_paths[modules_paths_index]; jQuery.ajax({ async: false, type: 'GET', url: modules_paths_object, data: null, success: function() { if (Drupal.settings.debug) { console.log(modules_paths_object); } }, dataType: 'script', error: function(xhr, textStatus, errorThrown) { console.log( t('Failed to load module!') + ' (' + module.name + ')', textStatus, errorThrown.message ); } }); } } } // Now invoke hook_install on all modules, including core. module_invoke_all('install'); } catch (error) { console.log('drupalgap_load_modules - ' + error); } } /** * Load the theme specified by drupalgap.settings.theme into drupalgap.theme * Returns true on success, false if it fails. * @return {Boolean} */ function drupalgap_load_theme() { try { if (!drupalgap.settings.theme) { var msg = 'drupalgap_load_theme - ' + t('no theme specified in settings.js'); drupalgap_alert(msg); } else { // Pull the theme name from the settings.js file. var theme_name = drupalgap.settings.theme; var theme_path = 'themes/' + theme_name + '/' + theme_name + '.js'; if (theme_name != 'easystreet3' && theme_name != 'ava') { theme_path = 'app/themes/' + theme_name + '/' + theme_name + '.js'; } if (!drupalgap_file_exists(theme_path)) { var error_msg = 'drupalgap_theme_load - ' + t('Failed to load theme!') + ' ' + t('The theme\'s JS file does not exist') + ': ' + theme_path; drupalgap_alert(error_msg); return false; } // We found the theme's js file, add it to the page. drupalgap_add_js(theme_path); // Call the theme's template_info implementation. var template_info_function = theme_name + '_info'; if (function_exists(template_info_function)) { var fn = window[template_info_function]; drupalgap.theme = fn(); // For each region in the name, set the 'name' value on the region JSON. for (var name in drupalgap.theme.regions) { if (!drupalgap.theme.regions.hasOwnProperty(name)) { continue; } var region = drupalgap.theme.regions[name]; drupalgap.theme.regions[name].name = name; } // Make sure the theme implements the required regions. var regions = system_regions_list(); for (var i = 0; i < regions.length; i++) { var region = regions[i]; if (typeof drupalgap.theme.regions[region] === 'undefined') { console.log('WARNING: drupalgap_load_theme() - The "' + theme_name + '" theme does not have the "' + region + '" region specified in "' + theme_name + '_info()."'); } } // Theme loaded successfully! Set the drupalgap.theme_path and return // true. drupalgap.theme_path = theme_path.replace('/' + theme_name + '.js', ''); return true; } else { var error_msg = 'drupalgap_load_theme() - ' + t('failed') + ' - ' + template_info_function + '() ' + t('does not exist!'); drupalgap_alert(error_msg); } } return false; } catch (error) { console.log('drupalgap_load_theme - ' + error); } } /** * Given a path to a javascript file relative to the app's www directory, * this will load the javascript file so it will be available in scope. */ function drupalgap_add_js() { try { var data; if (arguments[0]) { data = arguments[0]; } jQuery.ajax({ async: false, type: 'GET', url: data, data: null, success: function() { if (Drupal.settings.debug) { // Print the js path to the console. console.log(data); } }, dataType: 'script', error: function(xhr, textStatus, errorThrown) { console.log( 'drupalgap_add_js - error - (' + data + ' : ' + textStatus + ') ' + errorThrown ); } }); } catch (error) { console.log('drupalgap_add_js - ' + error); } } /** * Given a path to a css file relative to the app's www directory, this will * attempt to load the css file so it will be available in scope. */ function drupalgap_add_css() { try { var data; if (arguments[0]) { data = arguments[0]; } $('', {rel: 'stylesheet', href: data}).appendTo('head'); } catch (error) { console.log('drupalgap_add_css - ' + error); } } /** * Rounds up all blocks defined by hook_block_info and places them in the * drupalgap.blocks array. */ function drupalgap_load_blocks() { try { drupalgap.blocks = module_invoke_all('block_info'); } catch (error) { console.log('drupalgap_load_blocks - ' + error); } } /** * Loads language files. */ function drupalgap_load_locales() { try { // Load any drupalgap.settings.locale specified language files. if (typeof drupalgap.settings.locale === 'undefined') { return; } for (var language_code in drupalgap.settings.locale) { if (!drupalgap.settings.locale.hasOwnProperty(language_code)) { continue; } var language = drupalgap.settings.locale[language_code]; var file_path = 'locale/' + language_code + '.json'; if (!drupalgap_file_exists(file_path)) { continue; } drupalgap.locale[language_code] = drupalgap_file_get_contents( file_path, { dataType: 'json' } ); } // Load any language files specified by modules, and merge them into the // global language file (or create a new one if it doesn't exist). var modules = module_implements('locale'); for (var i = 0; i < modules.length; i++) { var module = modules[i]; var fn = window[module + '_locale']; var languages = fn(); for (var j = 0; j < languages.length; j++) { var language_code = languages[j]; var file_path = drupalgap_get_path('module', module) + '/locale/' + language_code + '.json'; var translations = drupalgap_file_get_contents( file_path, { dataType: 'json' } ); if (typeof drupalgap.locale[language_code] === 'undefined') { drupalgap.locale[language_code] = translations; } else { $.extend( drupalgap.locale[language_code], drupalgap.locale[language_code], translations ); } } } } catch (error) { console.log('drupalgap_load_locales - ' + error); } } /** * Checks for an Internet connection, returns true if connected, false otherwise. * @returns {boolean} */ function drupalgap_has_connection() { try { drupalgap_check_connection(); module_invoke_all('device_connection'); return drupalgap.online; } catch (error) { console.log('drupalgap_has_connection - ' + error); } } /** * Checks the devices connection and sets drupalgap.online to true if the * device has a connection, false otherwise. * @return {String} * A string indicating the type of connection according to PhoneGap. */ function drupalgap_check_connection() { try { // If we're not in phonegap, just use the navigator.onLine value. if (drupalgap.settings.mode != 'phonegap' || typeof parent.window.ripple === 'function' ) { drupalgap.online = navigator.onLine; return 'Ethernet connection'; // @TODO detect real connection type. } // Determine what connection phonegap has. var networkState = navigator.connection.type; var states = {}; states[Connection.UNKNOWN] = 'Unknown connection'; states[Connection.ETHERNET] = 'Ethernet connection'; states[Connection.WIFI] = 'WiFi connection'; states[Connection.CELL_2G] = 'Cell 2G connection'; states[Connection.CELL_3G] = 'Cell 3G connection'; states[Connection.CELL_4G] = 'Cell 4G connection'; states[Connection.NONE] = 'No network connection'; drupalgap.online = states[networkState] != 'No network connection'; return states[networkState]; } catch (error) { console.log('drupalgap_check_connection - ' + error); } } /** * @deprecated Use empty() instead. * Returns true if given value is empty. A generic way to test for emptiness. * @param {*} value * @return {Boolean} */ function drupalgap_empty(value) { try { console.log( 'WARNING: drupalgap_empty() is deprecated! ' + 'Use empty() instead.' ); return empty(value); } catch (error) { console.log('drupalgap_empty - ' + error); } } /** * Checks if a given file exists, returns true or false. * @param {string} path * A path to a file * @return {bool} * True if file exists, else false. */ function drupalgap_file_exists(path) { try { var file_exists = false; jQuery.ajax({ async: false, type: 'HEAD', dataType: 'text', url: path, success: function() { file_exists = true; }, error: function(xhr, textStatus, errorThrown) { } }); return file_exists; } catch (error) { console.log('drupalgap_file_exists - ' + error); } } /** * Reads entire file into a string and returns the string. Returns false if * it fails. * @param {String} path * @param {Object} options * @return {String} */ function drupalgap_file_get_contents(path, options) { try { var file = false; var default_options = { type: 'GET', url: path, dataType: 'html', data: null, async: false, success: function(data) { file = data; }, error: function(xhr, textStatus, errorThrown) { console.log( 'drupalgap_file_get_contents - failed to load file (' + path + ')' ); } }; $.extend(default_options, options); jQuery.ajax(default_options); return file; } catch (error) { console.log('drupalgap_file_get_contents - ' + error); } } /** * @see https://api.drupal.org/api/drupal/includes!common.inc/function/format_interval/7 * @param {Number} interval The length of the interval in seconds. * @return {String} */ function drupalgap_format_interval(interval) { try { // @TODO - deprecate this and move it to jDrupal as format_interval(). var granularity = 2; if (arguments[1]) { granularity = arguments[1]; } var langcode = null; if (arguments[2]) { langcode = langcode[2]; } var units = { '1 year|@count years': 31536000, '1 month|@count months': 2592000, '1 week|@count weeks': 604800, '1 day|@count days': 86400, '1 hour|@count hours': 3600, '1 min|@count min': 60, '1 sec|@count sec': 1 }; var output = ''; for (var key in units) { if (!units.hasOwnProperty(key)) { continue; } var value = units[key]; var key = key.split('|'); if (interval >= value) { var count = Math.floor(interval / value); output += (output ? ' ' : '') + drupalgap_format_plural( count, key[0], key[1] ); if (output.indexOf('@count') != -1) { output = output.replace('@count', count); } interval %= value; granularity--; } if (granularity == 0) { break; } } return output ? output : '0 sec'; } catch (error) { console.log('drupalgap_format_interval - ' + error); } } /** * @see http://api.drupal.org/api/drupal/includes%21common.inc/function/format_plural/7 * @param {Number} count * @param {String} singular * @param {String} plural * @return {String} */ function drupalgap_format_plural(count, singular, plural) { // @TODO - deprecate this and move it to jDrupal as format_plural(). if (count == 1) { return singular; } return plural; } /** * @deprecated - Use function_exists() instead. * @param {String} name * @return {Boolean} */ function drupalgap_function_exists(name) { try { console.log('WARNING - drupalgap_function_exists() is deprecated. ' + 'Use function_exists() instead!'); return function_exists(name); } catch (error) { console.log('drupalgap_function_exists - ' + error); } } /** * Given an html string from a *.tpl.html file, this will extract all of the * placeholders names and return them in an array. Returns false otherwise. * @param {String} html * @return {*} */ function drupalgap_get_placeholders_from_html(html) { try { var placeholders = false; if (html) { placeholders = html.match(/(?!{:)([\w]+)(?=:})/g); } return placeholders; } catch (error) { console.log('drupalgap_get_placeholders_from_html - ' + error); } } /** * Returns the current page's title. * @return {String} */ function drupalgap_get_title() { try { return drupalgap.page.title; } catch (error) { console.log('drupalgap_get_title - ' + error); } } /** * Returns the IP Address of the current user as reported by PHP via the last * System Connect call's $_SERVER['REMOTE_ADDR'] value. * @return {String|Null} */ function drupalgap_get_ip() { try { return drupalgap.remote_addr; } catch (error) { console.log('drupalgap_get_ip - ' + error); } } /** * Given a router path, this will return an array containing the indexes of * where the wildcards (%) are present in the router path. Returns false if * there are no wildcards present. * @param {String} router_path * @return {Boolean} */ function drupalgap_get_wildcards_from_router_path(router_path) { // @todo - Is this function even used? Doesn't look like it. var wildcards = false; return wildcards; } /** * Given a drupal image file uri, this will return the path to the image on the * Drupal site. * @param {String} uri * @return {String} */ function drupalgap_image_path(uri) { try { var altered = false; // If any modules want to alter the path, let them do it. var modules = module_implements('image_path_alter'); if (modules) { for (var index in modules) { if (!modules.hasOwnProperty(index)) { continue; } var module = modules[index]; var result = module_invoke(module, 'image_path_alter', uri); if (result) { altered = true; uri = result; break; } } } if (!altered) { // No one modified the image path, we'll use the default approach to // generating the image src path. var src = Drupal.settings.site_path + Drupal.settings.base_path + uri; if (src.indexOf('public://') != -1) { src = src.replace('public://', Drupal.settings.file_public_path + '/'); } else if (src.indexOf('private://') != -1) { src = src.replace( 'private://', Drupal.settings.file_private_path + '/' ); } return src; } else { return uri; } } catch (error) { console.log('drupalgap_image_path - ' + error); } } /** * @deprecated - This is no longer needed since the includes are built via the * makefile. Loads the js files in includes specified by drupalgap.includes. */ function drupalgap_includes_load() { try { if (drupalgap.includes != null && drupalgap.includes.length != 0) { for (var index in drupalgap.includes) { if (!drupalgap.includes.hasOwnProperty(index)) { continue; } var include = drupalgap.includes[index]; var include_path = 'includes/' + include.name + '.inc.js'; jQuery.ajax({ async: false, type: 'GET', url: include_path, data: null, success: function() { if (Drupal.settings.debug) { // Print the include path to the console. dpm(include_path); } }, dataType: 'script', error: function(xhr, textStatus, errorThrown) { console.log(errorThrown); } }); } } } catch (error) { console.log('drupalgap_includes_load - ' + error); } } /** * Given an html list element id and an array of items, this will clear the * list, populate it with the items, and then refresh the list. * @param {String} list_css_selector * @param {Array} items */ function drupalgap_item_list_populate(list_css_selector, items) { try { // @todo - This could use some validation and alerts for improper input. $(list_css_selector).html(''); for (var i = 0; i < items.length; i++) { $(list_css_selector).append($('
  • ', { html: items[i] })); } $(list_css_selector).listview('refresh').listview(); } catch (error) { console.log('drupalgap_item_list_populate - ' + error); } } /** * Given an html table element id and an array of rows, this will clear the * table, populate it with the rows, and then refresh the table. * @param {String} table_css_selector * @param {Array} rows * rows follow the. */ function drupalgap_table_populate(table_css_selector, rows) { try { // Select only the body. Other things are already setup table_css_selector = table_css_selector + '> tbody '; $(table_css_selector).html(''); for (var i = 0; i < rows.length; i++) { var row = rows[i]; var rowhtml = ''; for (var j = 0; j < row.length; j++) { rowhtml = rowhtml + '' + row[j] + ''; } $('').html(rowhtml).appendTo($(table_css_selector)); } $(table_css_selector).rebuild(); } catch (error) { console.log('drupalgap_table_populate - ' + error); } } /** * Given a jQM page event, and the corresponding callback function name that * handles the event, this function will call the callback function, if it has * not already been called on the current page. This really is only used by * menu_execute_active_handler() to prevent jQM from firing inline page event * handlers more than once. You may optionally pass in a 4th argument, a string, * to append to the suffix of the unique key of recorded fired page events. * @param {String} event * @param {String} callback * @param {*} page_arguments */ function drupalgap_jqm_page_event_fire(event, callback, page_arguments) { try { // Concatenate the event name and the callback name together into a unique // key so multiple callbacks can handle the same event. var key = event + '-' + callback; // Is there an optional 4th argument coming in (the suffix)? if (typeof arguments[3] !== 'undefined') { if (arguments[3]) { key += '-' + arguments[3]; } } if ($.inArray(key, drupalgap.page.jqm_events) == -1 && function_exists(callback)) { drupalgap.page.jqm_events.push(key); var fn = window[callback]; if (page_arguments) { // If the page arguments aren't an array, place them into an array so // they can be applied to the callback function. if (!$.isArray(page_arguments)) { page_arguments = [page_arguments]; } fn.apply(null, Array.prototype.slice.call(page_arguments)); } else { fn(); } } } catch (error) { console.log('drupalgap_jqm_page_event_fire - ' + error); } } /** * Returns array of jQM Page event names. * @return {Array} * @see http://api.jquerymobile.com/category/events/ */ function drupalgap_jqm_page_events() { return [ 'pagebeforechange', 'pagebeforecreate', 'pagebeforehide', 'pagebeforeload', 'pagebeforeshow', 'pagechange', 'pagechangefailed', 'pagecreate', 'pagehide', 'pageinit', 'pageload', 'pageloadfailed', 'pageremove', 'pageshow' ]; } /** * Given a JSON object with a page id, a jQM page event name, a callback * function to handle the jQM page event and any page arguments (as a JSON * string), this function will return the inline JS code needed to handle the * event. You may optionally pass in a unique second argument (string) to * allow the same page event handler to be fired more than once on a page. * @param {Object} options * @return {String} */ function drupalgap_jqm_page_event_script_code(options) { try { if (!options.page_id) { options.page_id = drupalgap_get_page_id(); } if (!options.jqm_page_event) { options.jqm_page_event = 'pageshow'; } // Build the arguments to send to the event fire handler. var event_fire_args = '"' + options.jqm_page_event + '", "' + options.jqm_page_event_callback + '", ' + options.jqm_page_event_args; if (arguments[1]) { event_fire_args += ', "' + arguments[1] + '"'; } // Build the inline JS and return it. return ''; } catch (error) { console.log('drupalgap_jqm_page_event_script_code - ' + error); } } /** * Returns the suggested max width for elements within the content area. * @return {Number} */ function drupalgap_max_width() { try { var padding = parseInt($('.ui-content').css('padding')); if (isNaN(padding)) { padding = 16; } // use a 16px default if needed return $(document).width() - padding * 2; } catch (error) { console.log('drupalgap_max_width - ' + error); } } /** * Checks to see if the current user has access to the given path. Returns true * if the user has access, false otherwise. You may optionally pass in a user * account object as the second argument to check access on a specific user. * Also, you may optionally pass in an entity object as the third argument, if * that entity needs to be passed along to an 'access_callback' handler. * @param {String} routerPath The router path of the destination. * @param {String} path The destination path. * @return {Boolean} */ function drupalgap_menu_access(routerPath, path) { try { // User #1 is allowed to do anything, I mean anything. if (Drupal.user.uid == 1) { return true; } // Everybody else will not have access unless we prove otherwise. var access = false; if (drupalgap.menu_links[routerPath]) { // Check to see if there is an access callback specified with the menu // link. if (typeof drupalgap.menu_links[routerPath].access_callback === 'undefined') { // No access call back specified, if there are any access arguments // on the menu link, then it is assumed they are user permission machine // names, so check that user account's role(s) for that permission to // grant access. if (drupalgap.menu_links[routerPath].access_arguments) { if ($.isArray(drupalgap.menu_links[routerPath].access_arguments)) { for (var index in drupalgap.menu_links[routerPath].access_arguments) { if (!drupalgap.menu_links[routerPath].access_arguments.hasOwnProperty(index)) { continue; } var permission = drupalgap.menu_links[routerPath].access_arguments[index]; access = user_access(permission); if (access) { break; } } } } else { // There is no access callback and no access arguments specified with // the menu link, so we'll assume everyone has access. access = true; } } else { // An access callback function is specified for this routerPath... var function_name = drupalgap.menu_links[routerPath].access_callback; if (function_exists(function_name)) { // Grab the access callback function. If there are any access args // send them along, or just call the function directly. // access arguments. var fn = window[function_name]; if (drupalgap.menu_links[routerPath].access_arguments) { var access_arguments = drupalgap.menu_links[routerPath].access_arguments.slice(0); // If we have an entity loaded, replace the first integer we find // in the page arguments with the loaded entity. if (arguments[2]) { var entity = arguments[2]; for (var index in access_arguments) { if (!access_arguments.hasOwnProperty(index)) { continue; } var page_argument = access_arguments[index]; if (is_int(parseInt(page_argument))) { access_arguments[index] = entity; break; } } } else { // Replace any integer arguments with the corresponding path argument. for (var i = 0; i < access_arguments.length; i++) { if (is_int(access_arguments[i])) { access_arguments[i] = arg(i, path); } } } return fn.apply(null, Array.prototype.slice.call(access_arguments)); } else { return fn(); } } else { console.log('drupalgap_menu_access - access call back (' + function_name + ') does not exist' ); } } } else { console.log('drupalgap_menu_access - routerPath (' + routerPath + ') does not exist'); } return access; } catch (error) { console.log('drupalgap_menu_access - ' + error); } } /** * @deprecated Use module_load() instead. * @param {String} name * @return {Object} */ function drupalgap_module_load(name) { try { return module_load(name); } catch (error) { console.log('drupalgap_module_load - ' + error); } } /** * Given a module bundle type, this will return the path to that module bundle's * directory. * @param {String} bundle * @return {String} */ function drupalgap_modules_get_bundle_directory(bundle) { try { dir = ''; if (bundle == 'core') { dir = 'modules'; } else if (bundle == 'contrib') { dir = 'app/modules'; } else if (bundle == 'custom') { dir = 'app/modules/custom'; } return dir; } catch (error) { console.log('drupalgap_modules_get_bundle_directory - ' + error); } } /** * Given a router path (and optional path, defaults to current drupalgap path if * one isn't provided), this takes the path's arguments and replaces any * wildcards (%) in the router path with the corresponding path argument(s). It * then returns the assembled path. Returns false otherwise. * @param {String} input_path * @return {*} */ function drupalgap_place_args_in_path(input_path) { try { var assembled_path = false; if (input_path) { // Determine path to use and break it up into its args. var path = drupalgap_path_get(); if (arguments[1]) { path = arguments[1]; } var path_args = arg(null, path); // Grab wild cards from router path then replace each wild card with // the corresponding path arg. var wildcards; var input_path_args = arg(null, input_path); if (input_path_args && input_path_args.length > 0) { for (var index in input_path_args) { if (!input_path_args.hasOwnProperty(index)) { continue; } var _arg = input_path_args[index]; if (_arg == '%') { if (!wildcards) { wildcards = []; } wildcards.push(index); } } if (wildcards && wildcards.length > 0) { for (var index in wildcards) { if (!wildcards.hasOwnProperty(index)) { continue; } var wildcard = wildcards[index]; if (path_args[wildcard]) { input_path_args[wildcard] = path_args[wildcard]; } } assembled_path = input_path_args.join('/'); } } } return assembled_path; } catch (error) { console.log('drupalgap_place_args_in_path - ' + error); } } /** * Given an args array, this returns true if the path in the array will have an * entity (id) present in it. * @param {Array} args * @return {Boolean} */ function drupalgap_path_has_entity_arg(args) { try { if (args.length > 1 && ( args[0] == 'comment' || args[0] == 'file' || args[0] == 'node' || (args[0] == 'taxonomy' && (args[1] == 'vocabulary' || args[1] == 'term') ) || args[0] == 'user' || args[0] == 'item' ) ) { return true; } return false; } catch (error) { console.log('drupalgap_path_has_entity_arg - ' + error); } } /** * Given a page id, this will remove it from the DOM. * @param {String} page_id */ function drupalgap_remove_page_from_dom(page_id) { try { $('#' + page_id).empty().remove(); } catch (error) { console.log('drupalgap_remove_page_from_dom - ' + error); } } /** * Restart the app. */ function drupalgap_restart() { try { location.reload(); } catch (error) { console.log('drupalgap_restart - ' + error); } } /** * Implementation of drupal_set_title(). * @param {String} title */ function drupalgap_set_title(title) { try { if (title) { drupalgap.page.title = title; } } catch (error) { console.log('drupalgap_set_title - ' + error); } } /** * Returns true if the loader spinner is enabled, false otherwise. Defaults to true if no config for it is present. * @returns {Boolean} */ function drupalgap_loader_enabled() { if (!drupalgap.settings.loader) { drupalgap.settings.loader = {}; } return typeof drupalgap.settings.loader.enabled !== 'undefined' ? drupalgap.settings.loader.enabled : true; } /** * Toggle on or off the loader spinner, send true to turn it on, false to turn it off. * @param {Boolean} enable */ function drupalgap_loader_enable(enable) { drupalgap.settings.loader.enabled = enable; } /** * Implements hook_services_preprocess(). * @param {Object} options */ function drupalgap_services_preprocess(options) { if (drupalgap_loader_enabled()) { drupalgap_loading_message_show(); } } /** * Implements hook_services_postprocess(). * @param {Object} options * @param {Object} result */ function drupalgap_services_postprocess(options, result) { if (drupalgap_loader_enabled()) { drupalgap_loading_message_hide(); } } /** * Implements hook_services_request_pre_postprocess_alter(). * @param {Object} options * @param {*} result */ function drupalgap_services_request_pre_postprocess_alter(options, result) { try { // Extract drupalgap system connect service resource results. if (options.service == 'system' && options.resource == 'connect') { drupalgap.remote_addr = result.remote_addr; drupalgap.entity_info = result.entity_info; drupalgap.field_info_instances = result.field_info_instances; drupalgap.field_info_fields = result.field_info_fields; drupalgap.field_info_extra_fields = result.field_info_extra_fields; drupalgap.taxonomy_vocabularies = drupalgap_taxonomy_vocabularies_extract( result.taxonomy_vocabularies ); drupalgap_service_resource_extract_results({ service: options.service, resource: options.resource, data: result }); } // Whenever a user logs in, out or registers, remove all pages from the DOM. else if (options.service == 'user' && (options.resource == 'logout' || options.resource == 'login' || options.resource == 'register')) { drupalgap_remove_pages_from_dom(); } // Whenever an entity is created, updated or deleted, remove the // corresponing DrupalGap core page(s) from the DOM so the pages will be // rebuilt properly next time they are loaded. else if ( in_array(options.resource, ['create', 'update', 'delete']) && in_array(options.service, entity_types()) ) { var entity_type = options.entity_type; var entity_id = options.entity_id; var bundle = options.bundle || null; var paths = []; if (options.resource != 'create') { var prefix = entity_type; if (in_array(entity_type, ['taxonomy_vocabulary', 'taxonomy_term'])) { prefix = prefix.replace('_', '/', prefix); } paths.push(prefix + '/' + entity_id); paths.push(prefix + '/' + entity_id + '/view'); // @todo This page won't get removed since it is the current page... // maybe when an entity is updated or deleted, we need a transitional // page that says "Deleting [entity]..." that way we can remove this // page (since it is not possible to remove the current page in jQM). // Actually now that I think about, we should have a confirmation page // when deleting an entity just like Drupal, and that should take care // of the deletion case. Not sure what to do about the update case... // maybe some type of pageshow handler on entity view pages that can // remove the edit form for the entity. paths.push(prefix + '/' + entity_id + '/edit'); } else { switch (entity_type) { case 'node': // @todo This page won't get removed since it is the current page... paths.push('node/add/' + bundle); break; } } // Add extras depending on the entity type. switch (entity_type) { case 'node': paths.push('node'); break; case 'taxonomy_vocabulary': paths.push('taxonomy/vocabularies'); break; case 'user': paths.push('user-listing'); break; } // Convert the paths to page ids, then remove them from the DOM. var pages = []; for (var index in paths) { if (!paths.hasOwnProperty(index)) { continue; } var path = paths[index]; pages.push(drupalgap_get_page_id(path)); } for (var index in pages) { if (!pages.hasOwnProperty(index)) { continue; } var page_id = pages[index]; drupalgap_remove_page_from_dom(page_id); } } } catch (error) { console.log('drupalgap_services_request_pre_postprocess_alter - ' + error); } } /** * @deprecated - Loads the settings specified in app/settings.js into the app. */ function drupalgap_settings_load() { try { console.log('WARNING: drupalgap_settings_load() is deprecated!'); //drupal_settings_load(); } catch (error) { console.log('drupalgap_settings_load - ' + error); } } /** * This calls all implementations of hook_theme and builds the DrupalGap theme * registry. */ function drupalgap_theme_registry_build() { try { var modules = module_implements('theme'); for (var index in modules) { if (!modules.hasOwnProperty(index)) { continue; } var module = modules[index]; var function_name = module + '_theme'; var fn = window[function_name]; var hook_theme = fn(); for (var element in hook_theme) { if (!hook_theme.hasOwnProperty(element)) { continue; } var variables = hook_theme[element]; variables.path = drupalgap_get_path( 'theme', drupalgap.settings.theme ); drupalgap.theme_registry[element] = variables; } } } catch (error) { console.log('drupalgap_theme_registry_build - ' + error); } } /** * Given a variable name and value, this will save the value to local storage, * keyed by its name. * @param {String} name * @param {*} value * @return {*} */ function variable_set(name, value) { try { if (!value) { value = ' '; } // store null values as a single space* else if (is_int(value)) { value = value.toString(); } else if (typeof value === 'object') { value = JSON.stringify(value); } return window.localStorage.setItem(name, value); // * phonegap won't store an empty string in local storage } catch (error) { drupalgap_error(error); } } /** * Given a variable name and a default value, this will first attempt to load * the variable from local storage, if it can't then the default value will be * returned. * @param {String} name * @param {*} default_value * @return {*} */ function variable_get(name, default_value) { try { var value = window.localStorage.getItem(name); if (!value) { value = default_value; } if (value == ' ') { value = ''; } // Convert single spaces to empty strings. return value; } catch (error) { drupalgap_error(error); } } /** * Given a variable name, this will remove the value from local storage. * @param {String} name * @return {*} */ function variable_del(name) { try { return window.localStorage.removeItem(name); } catch (error) { drupalgap_error(error); } } /** * Returns the current time as a string with the format: "yyyy-mm-dd hh:mm:ss". * @return {String} */ function date_yyyy_mm_dd_hh_mm_ss() { try { var result; if (arguments[0]) { result = arguments[0]; } else { result = date_yyyy_mm_dd_hh_mm_ss_parts(); } return result['year'] + '-' + result['month'] + '-' + result['day'] + ' ' + result['hour'] + ':' + result['minute'] + ':' + result['second']; } catch (error) { console.log('date_yyyy_mm_dd_hh_mm_ss - ' + error); } } /** * Returns an array with the parts for the current time. You may optionally * pass in a JS date object to use that date instead. * @return {Array} */ function date_yyyy_mm_dd_hh_mm_ss_parts() { try { var result = []; var now = null; if (arguments[0]) { now = arguments[0]; } else { now = new Date(); } var year = '' + now.getFullYear(); var month = '' + (now.getMonth() + 1); if (month.length == 1) { month = '0' + month; } var day = '' + now.getDate(); if (day.length == 1) { day = '0' + day; } var hour = '' + now.getHours(); if (hour.length == 1) { hour = '0' + hour; } var minute = '' + now.getMinutes(); if (minute.length == 1) { minute = '0' + minute; } var second = '' + now.getSeconds(); if (second.length == 1) { second = '0' + second; } result['year'] = year; result['month'] = month; result['day'] = day; result['hour'] = hour; result['minute'] = minute; result['second'] = second; return result; } catch (error) { console.log('date_yyyy_mm_dd_hh_mm_ss_parts - ' + error); } } /** * Given a year and month (0-11), this will return the number of days in that * month. * @see http://stackoverflow.com/a/1810990/763010 * @param {Number} year * @param {Number} month * @return {Number} */ function date_number_of_days_in_month(year, month) { try { var d = new Date(year, month, 0); return d.getDate(); } catch (error) { console.log('date_number_of_days_in_month - ' + error); } } /** * @see http://www.dconnell.co.uk/blog/index.php/2012/03/12/scroll-to-any-element-using-jquery/ */ function scrollToElement(selector, time, verticalOffset) { try { time = typeof(time) != 'undefined' ? time : 1000; verticalOffset = typeof(verticalOffset) != 'undefined' ? verticalOffset : 0; element = $(selector); offset = element.offset(); offsetTop = offset.top + verticalOffset; $('html, body').animate({ scrollTop: offsetTop }, time); } catch (error) { console.log('scrollToElement - ' + error); } } /** * Autocomplete global variables. Used to hold onto various global variables * needed for an autocomplete. */ // The autocomplete text field input selector. var _theme_autocomplete_input_selector = {}; // The autocomplete remote boolean. var _theme_autocomplete_remote = {}; // The theme autocomplete variables. var _theme_autocomplete_variables = {}; // The theme autocomplete variables. var _theme_autocomplete_success_handlers = {}; /** * Themes an autocomplete. * @param {Object} variables * @return {String} */ function theme_autocomplete(variables) { try { var html = ''; // We need to have a unique identifier for this autocomplete. If it is a // field, use the field name. Otherwise use the id attribute if it is // provided or generate a random one. Then finally attach the autocomplete // id to the variables so it can be passed along. var autocomplete_id = null; if (typeof variables.field_info_field !== 'undefined') { autocomplete_id = variables.field_info_field.field_name + '_' + variables.delta; } else if (typeof variables.attributes.id !== 'undefined') { autocomplete_id = variables.attributes.id; } else { autocomplete_id = user_password(); } variables.autocomplete_id = autocomplete_id; // Hold onto a copy of the variables. _theme_autocomplete_variables[autocomplete_id] = {}; $.extend(true, _theme_autocomplete_variables[autocomplete_id], variables); // Are we dealing with a remote data set? var remote = false; if (variables.remote) { remote = true; } variables.remote = remote; _theme_autocomplete_remote[autocomplete_id] = variables.remote; // Make sure we have an id to use on the list. var id = null; if (variables.attributes.id) { id = variables.attributes.id; } else { id = 'autocomplete_' + user_password(); variables.attributes.id = id; } // We need a hidden input to hold the value. If a default value // was provided by a form element, use it. var hidden_attributes = {}; $.extend(hidden_attributes, variables.attributes); if ( variables.element && typeof variables.element.default_value !== 'undefined' ) { hidden_attributes.value = variables.element.default_value; } html += theme('hidden', { attributes: hidden_attributes }); // Now we need an id for the list. var list_id = id + '-list'; // Build the widget variables. var widget = { attributes: { 'id': list_id, 'data-role': 'listview', 'data-filter': 'true', 'data-inset': 'true', 'data-filter-placeholder': '...' } }; // Handle a remote data set. var js = ''; if (variables.remote) { widget.items = []; // We have a remote data set. js += ''; } else { // Prepare the items then set the data filter reveal attribute. widget.items = _theme_autocomplete_prepare_items(variables); widget.attributes['data-filter-reveal'] = true; } // Save a reference to the autocomplete text field input. var selector = '#' + drupalgap_get_page_id() + ' #' + id + ' + form.ui-filterable' + ' input[data-type="search"]'; js += ''; // If there was a default value, set it's key title in the autocomplete's // text field. if (variables.default_value_label) { js += drupalgap_jqm_page_event_script_code({ page_id: drupalgap_get_page_id(), jqm_page_event: 'pageshow', jqm_page_event_callback: '_theme_autocomplete_set_default_value_label', jqm_page_event_args: JSON.stringify({ selector: selector, default_value_label: variables.default_value_label }) }, id); } // Theme the list and add the js to it, then return the html. html += theme('item_list', widget); html += js; return html; } catch (error) { console.log('theme_autocomplete - ' + error); } } /** * An internal function used to handle remote data for an autocomplete. * @param {Object} list The unordered list that displays the items. * @param {Object} e * @param {Object} data * @param {String} autocomplete_id */ function _theme_autocomplete(list, e, data, autocomplete_id) { try { var autocomplete = _theme_autocomplete_variables[autocomplete_id]; // Make sure a filter is present. if (typeof autocomplete.filter === 'undefined') { console.log( '_theme_autocomplete - A "filter" was not supplied.' ); return; } // Make sure a value and/or label has been supplied so we know how to render // the items in the autocomplete list. var value_provided = typeof autocomplete.value !== 'undefined'; var label_provided = typeof autocomplete.label !== 'undefined'; if (!value_provided && !label_provided) { console.log( '_theme_autocomplete - A "value" and/or "label" was not supplied.' ); return; } else { // We have a value and/or label. If one isn't provided, set it equal to // the other. if (!value_provided) { autocomplete.value = autocomplete.label; } else if (!label_provided) { autocomplete.label = autocomplete.value; } } // Setup the vars to handle this widget. var $ul = $(list), $input = $(data.input), value = $input.val(), html = ''; // Clear the list. $ul.html(''); // If a value has been input, start the autocomplete search. if (value && value.length > 0 && !autocomplete._searching) { autocomplete._searching = true; // Show the loader icon. $ul.html('
  • ' + '' + '
  • '); $ul.listview('refresh'); // Let's first build the success handler that will place the items into // the autocomplete list. _theme_autocomplete_success_handlers[autocomplete_id] = function( _autocomplete_id, result_items, _wrapped, _child) { try { autocomplete._searching = false; // If there are no results, and then if an empty callback handler was // provided, call it. // empty callback handler, then call it and return. if (result_items.length == 0) { if (autocomplete.empty_callback) { var fn = window[autocomplete.empty_callback]; fn(value); } } else { // Convert the result into an items array for a list. Each item will // be a JSON object with a "value" and "label" properties. var items = []; var _value = autocomplete.value; var _label = autocomplete.label; for (var index in result_items) { if (!result_items.hasOwnProperty(index)) { continue; } var object = result_items[index]; var _item = null; if (_wrapped) { _item = object[_child]; } else { _item = object; } var item = { value: _item[_value], label: _item[_label] }; items.push(item); } // Now render the items, add them to list and refresh the list. if (items.length != 0) { autocomplete.items = items; var _items = _theme_autocomplete_prepare_items(autocomplete); for (var index in _items) { if (!_items.hasOwnProperty(index)) { continue; } var item = _items[index]; html += '
  • ' + item + '
  • '; } $ul.html(html); $ul.listview('refresh'); $ul.trigger('updatelayout'); } } // Anybody want to act on the completion of the autocomplete? if (autocomplete.finish_callback) { var fn = window[autocomplete.finish_callback]; fn(value); } } catch (error) { console.log('_theme_autocomplete_success_handlers[' + _autocomplete_id + '] - ' + error); } }; // Depending on the handler, build the path and call the Drupal site for // the data. If it's a custom path, see if a handler was provided, // otherwise just default to views. var handler = null; if (autocomplete.custom) { if (autocomplete.handler) { handler = autocomplete.handler; } else if (autocomplete.field_info_field && autocomplete.field_info_field.settings.handler) { handler = autocomplete.field_info_field.settings.handler; } else { handler = 'views'; } } else if (autocomplete.field_info_field) { handler = autocomplete.field_info_field.settings.handler; } else { handler = 'views'; } switch (handler) { // Views (and Organic Groups) case 'views': // Prepare the path to the view. var path = autocomplete.path + '?' + autocomplete.filter + '=' + encodeURIComponent(value); // Any extra params to send along? if (autocomplete.params) { path += '&' + autocomplete.params; } // Retrieve JSON results. Keep in mind, we use this for retrieving // Views JSON results and custom hook_menu() path results in Drupal. views_datasource_get_view_result(path, { success: function(results) { // If this was a custom path, don't use a wrapper around the // results like the one used by Views Datasource. var wrapped = true; if (autocomplete.custom) { wrapped = false; } // Extract the result items based on the presence of the wrapper // or not. var result_items = null; if (wrapped) { result_items = results[results.view.root]; } else { result_items = results; } // Finally call the success handler. Note, since we route custom Drupal hook_menu() item JSON page // callbacks through this Views handler, we don't attempt to send along the view to the handler. Hack. var fn = _theme_autocomplete_success_handlers[autocomplete_id]; if (results.view) { fn(autocomplete_id, result_items, wrapped, results.view.child); } else { fn(autocomplete_id, result_items, wrapped); } } }); break; // Simple entity selection mode (provided by the entity reference // module), use the Index resource for the entity type. case 'base': case 'og': var field_settings = autocomplete.field_info_field.settings; var index_resource = field_settings.target_type + '_index'; if (!function_exists(index_resource)) { console.log('WARNING - _theme_autocomplete - ' + index_resource + '() does not exist!' ); return; } var options = { fields: [autocomplete.value, autocomplete.filter], parameters: { }, parameters_op: { } }; options.parameters[autocomplete.filter] = '%' + value + '%'; options.parameters_op[autocomplete.filter] = 'like'; var bundles = entityreference_get_target_bundles(field_settings); if (bundles) { options.parameters[entity_get_bundle_name(field_settings.target_type)] = bundles.join(','); } window[index_resource](options, { success: function(results) { _theme_autocomplete_success_handlers[autocomplete_id](autocomplete_id, results, false); } }); break; // An entity index resource call. Figure out which entity type index // to call, and build a default query if one wasn't provided. case 'index': if (!autocomplete.entity_type) { console.log( 'WARNING - _theme_autocomplete - no entity_type provided' ); return; } var function_name = autocomplete.entity_type + '_index'; var fn = window[function_name]; var query = null; if (autocomplete.query) { query = autocomplete.query; } else { query = { parameters: { }, parameters_op: { } }; var fields = [ entity_primary_key(autocomplete.entity_type), entity_primary_key_title(autocomplete.entity_type) ]; if (autocomplete.entity_type == 'taxonomy_term') { if (autocomplete.vid) { query.parameters['vid'] = autocomplete.vid; } if (autocomplete.parent) { query.parameters['parent'] = autocomplete.parent; } } query.fields = fields; query.parameters[autocomplete.filter] = '%' + value + '%'; query.parameters_op[autocomplete.filter] = 'like'; } fn.apply(null, [query, { success: function(results) { var fn = _theme_autocomplete_success_handlers[autocomplete_id]; fn(autocomplete_id, results, false, null); } }]); break; // The default handler... default: // If we made it this far, and don't have a handler, then warn the // developer. if (!handler) { console.log('WARNING - _theme_autocomplete - no handler provided'); return; } break; } } else { // The autocomplete text field was emptied, clear out the hidden value. $('#' + autocomplete.id).val(''); } } catch (error) { console.log('_theme_autocomplete - ' + error); } } /** * An internal function used to prepare the items for an autocomplete list. * @param {Object} variables * @return {*} */ function _theme_autocomplete_prepare_items(variables) { try { // Make sure we have an items array. var items = []; if (variables.items) { items = variables.items; } // Prepare the items, and return them. var _items = []; if (items.length > 0) { for (var index in items) { if (!items.hasOwnProperty(index)) { continue; } var item = items[index]; var value = ''; var label = ''; if (typeof item === 'string') { value = item; label = item; } else { value = item.value; label = item.label; } var options = { attributes: { value: value, onclick: '_theme_autocomplete_click(\'' + variables.attributes.id + '\', this, \'' + variables.autocomplete_id + '\')' } }; var _item = l(label, null, options); _items.push(_item); } } return _items; } catch (error) { console.log('_theme_autocomplete_prepare_items - ' + error); } } /** * An internal function used to handle clicks on items in autocomplete results. * @param {String} id The id of the hidden input that holds the value. * @param {Object} item The list item anchor that was just clicked. * @param {String} autocomplete_id */ function _theme_autocomplete_click(id, item, autocomplete_id) { try { // Set the hidden input with the value, and the text field with the text. var list_id = id + '-list'; $('#' + id).val($(item).attr('value')); $(_theme_autocomplete_input_selector[autocomplete_id]).val($(item).html()); if (_theme_autocomplete_remote[autocomplete_id]) { $('#' + list_id).html(''); } else { $('#' + list_id + ' li').addClass('ui-screen-hidden'); $('#' + list_id).listview('refresh'); } // Now fire the item onclick handler, if one was provided. if ( _theme_autocomplete_variables[autocomplete_id].item_onclick && function_exists( _theme_autocomplete_variables[autocomplete_id].item_onclick ) ) { var fn = window[_theme_autocomplete_variables[autocomplete_id].item_onclick]; fn(id, $(item)); } } catch (error) { console.log('_theme_autocomplete_click - ' + error); } } /** * Used to set a default value in an autocomplete's text field. * @param {Object} options */ function _theme_autocomplete_set_default_value_label(options) { try { setTimeout(function() { $(options.selector).val(options.default_value_label).trigger('create'); }, 250); } catch (error) { console.log('_theme_autocomplete_set_default_value_label - ' + error); } } /** * Given a block delta, this will return the corresponding * block from drupalgap.blocks. * @param {String} delta * @return {Object} */ function drupalgap_block_load(delta) { try { var block = null; if (drupalgap.blocks) { for (var index in drupalgap.blocks) { if (!drupalgap.blocks.hasOwnProperty(index)) { continue; } var object = drupalgap.blocks[index]; if (object[delta]) { block = object[delta]; break; } } } if (block == null) { var msg = 'drupalgap_block_load - ' + t('failed to load') + ' "' + delta + '" ' + t('block!'); drupalgap_alert(msg); } return block; } catch (error) { console.log('drupalgap_block_load - ' + error); } } /** * Renders the html string for a block. * @param {Object} region * @param {String} current_path * @param {String} block_delta * @param {Object} block_settings * @param {Object} block_counts * @return {String} */ function drupalgap_block_render(region, current_path, block_delta, block_settings, block_counts) { try { var content = ''; // Check the block's visibility settings. If an access_callback // function is specified on the block's settings, we'll call that // to determine the visibility, otherwise we'll fall back to the // default visibility determination mechanism. var render_block = false; if ( block_settings.access_callback && function_exists(block_settings.access_callback) ) { var fn = window[block_settings.access_callback]; render_block = fn({ path: current_path, delta: block_delta, region: region.name, theme: drupalgap.settings.theme, settings: block_settings }); } else if (drupalgap_check_visibility('block', block_settings)) { render_block = true; // The 'offline' and 'error' pages only have the 'main' system // block visible. if (block_delta != 'main' && ( current_path == 'offline' || current_path == 'error') ) { render_block = false; } } if (render_block) { var block = drupalgap_block_load(block_delta); if (block_counts) { block_counts.block_count++; } if (menu_load(block_delta) && block_counts) { block_counts.block_menu_count++; } if (block) { content = module_invoke( block.module, 'block_view', block_delta, region ); } } return typeof content === 'string' ? content : drupalgap_render(content); } catch (error) { console.log('drupalgap_block_render - ' + error); } } /** * Converts a JSON object to an XML/HTML tag attribute string and returns the * string. * @param {Object} attributes * @return {String{ */ function drupalgap_attributes(attributes) { try { var attribute_string = ''; if (attributes) { for (var name in attributes) { if (!attributes.hasOwnProperty(name)) { continue; } var value = attributes[name]; if (value != '') { // @todo - if someone passes in a value with double quotes, this // will break. e.g. // 'onclick':'_drupalgap_form_submit("' + form.id + "');' // will break, but // 'onclick':'_drupalgap_form_submit(\'' + form.id + '\');' // will work. attribute_string += name + '="' + value + '" '; } else { // The value was empty, just place the attribute name on the // element. attribute_string += name + ' '; } } } return attribute_string; } catch (error) { console.log('drupalgap_attributes - ' + error); } } /** * Used by drupalgap_render_region to check the visibility settings on region * links and blocks. Just like Drupal Blocks, this function checks the * visibility rules specified by role or pages specified in data. Returns true * by default, otherwise it will return true or false depending on the first * visibility setting present in data. * @param {String} type * @param {Object} data * @return {Boolean} */ function drupalgap_check_visibility(type, data) { try { var visible = true; if (typeof data === 'undefined') { console.log( 'drupalgap_check_visibility - WARNING - no data provided for type (' + type + ')' ); } // Roles. else if (typeof data.roles !== 'undefined' && data.roles && data.roles.value && data.roles.value.length != 0) { for (var role_index in data.roles.value) { if (!data.roles.value.hasOwnProperty(role_index)) { continue; } var role = data.roles.value[role_index]; if (drupalgap_user_has_role(role)) { // User has role, show/hide the block accordingly. if (data.roles.mode == 'include') { visible = true; } if (data.roles.mode == 'exclude') { visible = false; } } else { // User does not have role, show/hide the block accordingly. if (data.roles.mode == 'include') { visible = false; } if (data.roles.mode == 'exclude') { visible = true; } } // Break out of the loop if already determined to be visible. if (visible) { break; } } } // Pages. else if (typeof data.pages !== 'undefined' && data.pages && data.pages.value && data.pages.value.length != 0) { var current_path = drupalgap_path_get(); var current_path_parts = current_path.split('/'); for (var page_index in data.pages.value) { if (!data.pages.value.hasOwnProperty(page_index)) { continue; } var path = data.pages.value[page_index]; if (path == '') { path = drupalgap.settings.front; } if (path == current_path) { if (data.pages.mode == 'include') { visible = true; } else if (data.pages.mode == 'exclude') { visible = false; } break; } else { // It wasn't a direct path match, is there a wildcard that matches // the router path? if (path.indexOf('*') != -1) { var router_path = drupalgap_get_menu_link_router_path(current_path); if (router_path.replace(/%/g, '*') == path) { if (data.pages.mode == 'include') { visible = true; } else if (data.pages.mode == 'exclude') { visible = false; } break; } else { var path_parts = path.split('/'); var match = true; if (path_parts.length == 0) { match = false; } else if (path_parts.length == current_path_parts.length) { for (var i = 0; i < path_parts.length; i++) { if (path_parts[i] != current_path_parts[i]) { match = false; break; } } } if (match) { if (data.pages.mode == 'include') { visible = false; } else if (data.pages.mode == 'exclude') { visible = true; } } } } else { // There's no wildcard in the rule, and it wasn't a direct path // match. if (data.pages.mode == 'include') { visible = false; } else if (data.pages.mode == 'exclude') { visible = true; } } } } } return visible; } catch (error) { console.log('drupalgap_check_visibility - ' + error); } } /** * @deprecated * @ see entity_get_bundle() */ function drupalgap_get_bundle(entity_type, entity) { try { var msg = 'WARNING - drupalgap_get_bundle() is deprecated, use ' + 'entity_get_bundle() instead!'; console.log(msg); return entity_get_bundle(entity_type, entity); } catch (error) { console.log('drupalgap_get_bundle - ' + error); } } /** * Returns the path to a system item (module, theme, etc.), returns false if it * can't find it. * @param {String} type * @param {String} name * @return {*} */ function drupalgap_get_path(type, name) { try { var path = null; if (type == 'module') { var found_module = false; for (var bundle in Drupal.modules) { if (!Drupal.modules.hasOwnProperty(bundle)) { continue; } var modules = Drupal.modules[bundle]; if (found_module) { break; } else { for (var index in modules) { if (!modules.hasOwnProperty(index)) { continue; } var module = modules[index]; if (module.name == name) { found_module = true; path = ''; if (bundle == 'core') { path += 'modules'; } else if (bundle == 'contrib') { path += 'app/modules'; } else if (bundle == 'custom') { path += 'app/modules/custom'; } else { var msg = 'drupalgap_get_path - unknown module bundle (' + bundle + ')'; drupalgap_alert(msg); break; } path += '/' + name; break; } } } } } else if (type == 'theme') { if (name == 'easystreet3' || name == 'ava') { path = 'themes/' + name; } else { path = 'app/themes/' + name; } } else { console.log( 'WARNING: drupalgap_get_path - unsupported type (' + type + ')' ); } return path; } catch (error) { console.log('drupalgap_get_path - ' + error); } } /** * Given an error message, this will log the message to the console and goto * the error page, if it isn't there already. If Drupal.settings.debug is set * to true, this function will also alert the error. You may optionally send in * a second message that will be displayed to the user via an alert dialog box. * @param {String} message */ function drupalgap_error(message) { try { // Generate a developer error message, log it to the console, then alert // the message if debugging is enabled. var error_message = 'drupalgap_error() - ' + arguments.callee.caller.name + ' - ' + message; dpm(error_message); if (Drupal.settings.debug) { drupalgap_alert(error_message); } // If a message for the user was passed in, display it to the user. if (arguments[1]) { drupalgap_alert(arguments[1]); } // Goto the error page if we are not already there. if (drupalgap_path_get() != 'error') { drupalgap_goto('error'); } } catch (error) { console.log('drupalgap_error - ' + error); } } /** * Given a link JSON object, this will return its attribute class value, or null * if it isn't set. * @param {Object} link * @return {String} */ function drupalgap_link_get_class(link) { try { var css_class = null; if ( link.options && link.options.attributes && link.options.attributes['class'] && !empty(link.options.attributes['class']) ) { css_class = link.options.attributes['class']; } return css_class; } catch (error) { console.log('drupalgap_link_get_class - ' + error); } } /** * Get the current DrupalGap path. * @return {String} */ function drupalgap_path_get() { try { return drupalgap.path; } catch (error) { console.log('drupalgap_path_get - ' + error); } } /** * Set the current DrupalGap path. * @param {String} path */ function drupalgap_path_set(path) { try { drupalgap.path = path; } catch (error) { console.log('drupalgap_path_set - ' + error); } } /** * Get the current DrupalGap router_path. * @return {String} */ function drupalgap_router_path_get() { try { return drupalgap.router_path; } catch (error) { console.log('drupalgap_router_path_get - ' + error); } } /** * Set the current DrupalGap router_path. * @param {String} router_path */ function drupalgap_router_path_set(router_path) { try { drupalgap.router_path = router_path; } catch (error) { console.log('drupalgap_router_path_set - ' + error); } } /** * Returns the path used for a given page id. Essentially a reverse path look up, given a page id. * @param {String} page_id * @returns {String|null} */ function drupalgap_get_path_from_page_id(page_id) { if (!page_id) { page_id = drupalgap_get_page_id(); } // SUPPORTS THE FOLLOWING PATH PATTERNS: // * foo // */* foo/bar, node/123 // *-*/* foo-bar/chew, foo-bar/123 // */*/*/... foo/bar/chew, foo/123/bar, foo/bar/chew/you, etc // First check to see if there is an exact router match, if there is use it. If there is no exact match, split the // page id by underscore into arguments so we can attempt to lookup a potential router. if (drupalgap.menu_links[page_id]) { return drupalgap.menu_links[page_id].path; } else { var args = page_id.split('_'); if (args.length < 2) { return null; } var slashPath = ''; var hyphenPath = ''; for (var i = 0; i < args.length; i++) { slashPath += args[i]; if (i != args.length -1) { slashPath += '/'; } hyphenPath += args[i]; if (i == 0) { hyphenPath += '-'; } else if (i != 0 && i != args.length -1) { hyphenPath += '/'; } } var potentialPaths = [slashPath, hyphenPath]; for (var j = 0; j < potentialPaths.length; j++) { var routerPath = drupalgap_get_menu_link_router_path(potentialPaths[j]); if (!drupalgap.menu_links[routerPath]) { continue; } return potentialPaths[j]; } } return null; } /** * Implementation of arg(index = null, path = null). * @return {*} */ function arg() { try { var result = null; // If there were zero or one arguments provided. if (arguments.length == 0 || arguments.length == 1) { // Split the path into parts. var drupalgap_path = drupalgap_path_get(); var args = drupalgap_path.split('/'); // If no arguments were provided just return the split array, otherwise // return whichever argument was requested. if (arguments.length == 0) { result = args; } else if (args[arguments[0]]) { result = args[arguments[0]]; } } else { // A path was provided, split it into parts, then return the split array // if they didn't request a specific index, otherwise return the value of // the specific index inside the split array. var path = arguments[1]; var args = path.split('/'); if (arguments[0] && args[arguments[0]]) { result = args[arguments[0]]; } else { result = args; } } return result; } catch (error) { console.log('arg - ' + error); } } /** * Returns a link. * @return {String} */ function l() { try { // Grab the text and the path from the arguments and then build a simple // link object. var text = arguments[0]; var path = arguments[1]; var link = {'text': text, 'path': path}; // Determine if there are any incoming link options, if there are, attach // them to the link object. If there are any attributes, extract them from // the options and attach them directly to the link object. var options = null; if (arguments[2]) { options = arguments[2]; if (options.attributes) { link.attributes = options.attributes; } link.options = options; } return theme('link', link); } catch (error) { console.log('l - ' + error); } } /** * Returns a button link. * @return {String} */ function bl() { try { // Grab the text and the path. var text = arguments[0]; var path = arguments[1]; // Build the default options and attributes, if necessary. var options = null; if (arguments[2]) { options = arguments[2]; } else { options = {}; } if (!options.attributes) { options.attributes = { }; } options.attributes['data-role'] = 'button'; return l(text, path, options); } catch (error) { console.log('bl - ' + error); } } /** * Returns translated text. * @param {String} str The string to translate * @return {String} */ function t(str) { var lang = arguments[3] ? arguments[3] : Drupal.settings.language_default; if ( lang != 'und' && typeof drupalgap.locale[lang] !== 'undefined' && drupalgap.locale[lang][str] ) { return drupalgap.locale[lang][str]; } return str; } // Holds onto any query string during the page go process. var _drupalgap_goto_query_string = null; /** * Given a path, this will change the current page in the app. * @param {String} path * @return {*} */ function drupalgap_goto(path) { try { // Extract any incoming options, set any defaults that weren't provided, // then populate the global page options variable. var options = {}; if (arguments[1]) { options = arguments[1]; if (typeof options.form_submission === 'undefined') { options.form_submission = false; } } drupalgap.page.options = options; // Prepare the path. path = _drupalgap_goto_prepare_path(path, true); if (!path) { return false; } // Invoke all implementations of hook_drupalgap_goto_preprocess(). module_invoke_all('drupalgap_goto_preprocess', path); // Determine the router path. var router_path = drupalgap_get_menu_link_router_path(path); // Make sure we have a menu link item that can handle this router path, // otherwise we'll goto the 404 page. if (!drupalgap.menu_links[router_path]) { // Is anyone trying to handle this 404? var new_path = false; var invocation_results = module_invoke_all('404', router_path); if (invocation_results) { for (var index in invocation_results) { if (!invocation_results.hasOwnProperty(index)) { continue; } var result = invocation_results[index]; if (result !== false) { new_path = result; break; } } } // If a 404 handler provided a new path use it, otherwise just use the // system 404 page. Either way, update the router path before continuing // with a normal page build. if (new_path) { path = new_path; } else { path = '404'; } router_path = drupalgap_get_menu_link_router_path(path); } // Make sure the user has access to this router path, if they don't send // them to the 401 page. // @TODO - for now we're going to skip access checks on local tasks, since // they are covered by menu_block_view(), but if someone were to navigate // directly to e.g. a node's edit page, they would be able to see the page. // Of course Drupal would actually prevent them from updating the node on // the live site, but nonetheless this needs to be fixed. It's a tough issue // though and related to https://github.com/signalpoint/DrupalGap/issues/257 if ( drupalgap.menu_links[router_path].type != 'MENU_DEFAULT_LOCAL_TASK' && drupalgap.menu_links[router_path].type != 'MENU_LOCAL_TASK' && !drupalgap_menu_access(router_path, path) ) { path = '401'; router_path = drupalgap_get_menu_link_router_path(path); } // If the new router path is the same as the current router path and the new // path is the same as the current path, we may need to cancel the // navigation attempt (i.e. don't go anywhere), unless it is a // form submission, then continue. if ( router_path == drupalgap_router_path_get() && path == drupalgap_path_get() ) { // If it's a form submission, we'll continue onward... if (options.form_submission) { } // If we're reloading the current page, we need to set aside this path // and navigate to the system's _reload page, which will then handle the // actual reloading of the page. // @see system_drupalgap_goto_post_process() else if (options.reloadPage) { _system_reload_page = path; path = '_reload'; router_path = drupalgap_get_menu_link_router_path(path); } // Otherwise, just stop the navigation attempt. else { return false; } } // Grab the page id. var page_id = drupalgap_get_page_id(path); // Return if we are trying to go to the path we are already on, unless this // was a form submission, then we'll let the page rebuild itself. For // accuracy we compare the jQM active page url with the destination page // id. // @todo - this boolean doesn't match the comment description of the code // block, i.e. the form_submission check is opposite of what it says if (drupalgap_jqm_active_page_url() == page_id && options.form_submission) { // Clear any messages from the page before returning. drupalgap_clear_messages(); return false; } // @TODO when drupalgap_goto() is called during the bootstrap, we shouldn't // put the path onto the back_path array. // Save the back path. var back_paths_to_ignore = ['user/logout', '_reload']; if (!in_array(drupalgap_path_get())) { drupalgap.back_path.push(drupalgap_path_get()); } // Set the current menu path to the path input. drupalgap_path_set(path); // Set the drupalgap router path. drupalgap_router_path_set(router_path); // If the page is already in the DOM and we're asked to reload it, then // remove the page and let it rebuild itself. If we're not reloading the // page and we're not in the middle of a form submission, prevent the page // from processing then change to it. if (drupalgap_page_in_dom(page_id)) { // If there are any hook_menu() item options for this router path, bring // them into the current options without overwriting any existing values. if (drupalgap.menu_links[router_path].options) { options = $.extend( {}, drupalgap.menu_links[router_path].options, options ); } // Reload the page? If so, remove the page from the DOM, delete the // reloadPage option, then set the reloadingPage option to true so others // down the line will know the page is reloading. We can't pass along the // actual reloadPage option since it may collide with jQM later on. We // have to use 'force' when removing the page from the DOM since DG won't // remove it since it thinks we are already on the page, so it won't // remove it. if (typeof options.reloadPage !== 'undefined' && options.reloadPage) { // @TODO we may need to leave the query ONLY if the above call to // _drupalgap_goto_prepare_path() yielded a query string, otherwise we // may be leaving unwanted queries in _GET()! var leaveQuery = _drupalgap_goto_query_string ? true : false; drupalgap_remove_page_from_dom(page_id, { force: true, leaveQuery: leaveQuery }); delete options.reloadPage; options.reloadingPage = true; } else if (!options.form_submission) { drupalgap_clear_messages(); _drupalgap_goto_query_string = null; drupalgap.page.process = false; // @TODO changePage has been deprecated as of jQM 1.4.0 and will be // removed in 1.50. // @see https://api.jquerymobile.com/jQuery.mobile.changePage/ $.mobile.changePage('#' + page_id, options); module_invoke_all('drupalgap_goto_post_process', path); return; } } else if (typeof options.reloadPage !== 'undefined' && options.reloadPage) { // The page is not in the DOM, and we're being asked to reload it, so // we'll just delete the reloadPage option. delete options.reloadPage; } // Generate the page. drupalgap_goto_generate_page_and_go(path, page_id, options, drupalgap.menu_links[router_path]); } catch (error) { console.log('drupalgap_goto - ' + error); } } /** * Generate a JQM page by running it through the theme then attach the * page to the of the document, then change to the page. Remember, * the rendering of the page does not take place here, that is covered by * the pagebeforechange event in theme.inc.js which happens after we change * the page here. * @param {String} path * @param {String} page_id * @param {Object} options * @param {Object} menu_link The menu link object from drupalgap.menu_links. */ function drupalgap_goto_generate_page_and_go(path, page_id, options, menu_link) { try { // First see if the theme implements hook_page_tpl_html() and use its string for the page placeholders, // otherwise use the file read method from DrupalGap's early days, and notify the developer to upgrade // their theme. var html = ''; var themeName = drupalgap.settings.theme; var pageTplHtmlFunctionName = themeName + '_page_tpl_html'; if (function_exists(pageTplHtmlFunctionName)) { html = window[pageTplHtmlFunctionName](); } else { var pageTemplatePath = path_to_theme() + '/page.tpl.html'; console.log('@deprecated: ' + pageTemplatePath + ' - use ' + pageTplHtmlFunctionName + '() in ' + themeName + '.js instead, see: http://docs.drupalgap.org/7/Themes/Create_a_Custom_Theme'); html = drupalgap_file_get_contents(pageTemplatePath); } // Reset the internal query string. _drupalgap_goto_query_string = null; // If options wasn't set, set it as an empty JSON object. if (typeof options === 'undefined') { options = {}; } // Add page to DOM. drupalgap_add_page_to_dom({ page_id: page_id, html: html, menu_link: menu_link }); // Setup change page options if necessary. if (drupalgap_path_get() == path && options.form_submission) { options.allowSamePageTransition = true; } // Let's change to the page. Web apps and the ripple emulator do not // seem to like the 'index.html' prefix, so we'll remove that. var destination = 'index.html#' + page_id; if ( drupalgap.settings.mode != 'phonegap' || typeof parent.window.ripple === 'function' ) { destination = '#' + page_id; } $.mobile.changePage(destination, options); // @see the pagebeforechange handler in page.inc.js // Invoke all implementations of hook_drupalgap_goto_post_process(). module_invoke_all('drupalgap_goto_post_process', path); } catch (error) { console.log('drupalgap_goto_generate_page_and_go - ' + error); } } /** * Prepares a drupalgap page path. * @deprecated Use _drupalgap_goto_prepare_path() instead. * @param {String} path * @return {String} */ function drupalgap_goto_prepare_path(path) { try { console.log('WARNING - drupalgap_goto_prepare_path() is deprecated, ' + 'use _drupalgap_goto_prepare_path() instead!'); return _drupalgap_goto_prepare_path(path); } catch (error) { console.log('drupalgap_goto_prepare_path - ' + error); } } /** * An internal function used to prepare the path for menu routing. An optional * second parameter (boolean) may be passed in, and if it is set to true it will * process any _GET query string parameters. * @param {String} path * @return {String} */ function _drupalgap_goto_prepare_path(path) { try { // Pull out any query string parameters and populate them into _GET, if we // were instructed to do so. Hold onto a global copy of the query string // for page go processing. if (typeof arguments[1] !== 'undefined' && arguments[1]) { var pos = path.indexOf('?'); if (pos != -1 && pos != path.length - 1) { var query = path.substr(pos + 1, path.length - pos); _drupalgap_goto_query_string = query; path = path.substr(0, pos); var parts = query.split('&'); for (var i = 0; i < parts.length; i++) { pos = parts[i].indexOf('='); if (pos == -1) { continue; } query = parts[i].split('='); if (query.length != 2) { continue; } _GET( decodeURIComponent(query[0]), decodeURIComponent(query[1]), path ); } } } // If the path is an empty string, change it to the front page path. if (path == '') { if (!drupalgap.settings.front) { drupalgap_alert( 'drupalgap_goto_prepare_path - ' + t('no front page specified in settings.js!') ); return false; } else { path = drupalgap.settings.front; } } // Change 'user' to 'user/login' for anonymous users, or change it to e.g. // 'user/123' for authenticated users. else if (path == 'user') { if (Drupal.user.uid != 0) { path = 'user/' + Drupal.user.uid; } else { path = 'user/login'; } } // Finally return the path. return path; } catch (error) { console.log('drupalgap_goto_prepare_path - ' + error); } } /** * Change the page to the previous page. */ function drupalgap_back() { try { var active_page_id = $('.ui-page-active').attr('id'); if (active_page_id == drupalgap.settings.front) { var msg = t('Exit') + ' ' + drupalgap.settings.title + '?'; if (drupalgap.settings.exit_message) { msg = drupalgap.settings.exit_message; } drupalgap_confirm(msg, { confirmCallback: _drupalgap_back_exit }); } else if (active_page_id == '_drupalgap_splash') { return; } else { _drupalgap_back(); } } catch (error) { console.log('drupalgap_back - ' + error); } } /** * The DrupalGap "Back to the Future" function goes forward to the previous page by first * removing that page from the DOM, then going "forward" to that page using drupalgap_goto() * with a reloadPage option set to true. */ function drupalgap_back_2tf() { var back = drupalgap_back_path(); drupalgap_remove_page_from_dom(drupalgap_get_page_id(back)); drupalgap_goto(back, { reloadPage: true }); } /** * Returns the previous page path, or the front page if there is none. * @returns {String} */ function drupalgap_back_path() { return drupalgap.back_path.length ? drupalgap.back_path[drupalgap.back_path.length - 1] : drupalgap.settings.front; } /** * An internal function used to change the page to the previous page. */ function _drupalgap_back() { try { // @WARNING - any changes here (except the history.back() call) need to be // reflected into the window "navigate" handler below drupalgap.back = true; // Properly handle iOS9 back button clicks, and default back button clicks. if (typeof device !== 'undefined' && device.platform === "iOS" && parseInt(device.version) === 9) { $.mobile.back(); } else { history.back(); } // Update the path and router path. var from = drupalgap_path_get(); drupalgap_path_set(drupalgap.back_path.pop()); var to = drupalgap_path_get(); drupalgap_router_path_set(drupalgap_get_menu_link_router_path(to)); module_invoke_all('drupalgap_back', from, to); } catch (error) { console.log('_drupalgap_back - ' + error); } } /** * An internal function used to exit the app when the back button is clicked. * @param {Number} button Which button was pressed. */ function _drupalgap_back_exit(button) { try { button === 1 ? navigator.app.exitApp() : ''; } catch (error) { console.log('_drupalgap_back_exit - ' + error); } } $(window).on('navigate', function(event, data) { // If we're already moving backwards (aka from a "soft" back button click), // then don't do anything. if (drupalgap.back) { return; } // In web-app mode, clicking the back button on your browser (or Android // device browser), does not actually fire drupalgap_back(), so we mimic // it here, but skip the history.back() call because that's already // happened via the hardware button. if (drupalgap.settings.mode == 'web-app') { // Grab the direction we're travelling. // back, forward (or undefined, aka moving from splash to front page) var direction = data.state.direction; if (direction == 'back' && drupalgap.back_path.length > 0) { // @WARNING - any changes here should be reflected into _drupalgap_back(). drupalgap.back = true; // Update the path and router path. var from = drupalgap_path_get(); drupalgap_path_set(drupalgap.back_path.pop()); var to = drupalgap_path_get(); drupalgap_router_path_set(drupalgap_get_menu_link_router_path(to)); module_invoke_all('drupalgap_back', from, to); } } }); /** * Execute the page callback associated with the current path and return its * content. * @return {Object} */ function menu_execute_active_handler() { try { // Determine the path and then grab the page id. var path = null; if (arguments[0]) { path = arguments[0]; } if (!path) { path = drupalgap_path_get(); } var page_id = drupalgap_get_page_id(path); // @todo - Make sure the user has access to this DrupalGap menu path! // Get the router path. var router_path = drupalgap_router_path_get(); if (!router_path) { return; } // Call the page call back for this router path and send along any // arguments. var function_name = drupalgap.menu_links[router_path].page_callback; var page_arguments = []; if (function_exists(function_name)) { // Grab the page callback function and get ready to build the html. var fn = window[function_name]; var content = ''; // Are there any arguments to send to the page callback? if (drupalgap.menu_links[router_path].page_arguments) { // For each page argument, if the argument is an integer, grab the // corresponding arg(#), otherwise just push the arg onto the page // arguments. Then try to prepare any entity that may be present in // the url so the entity is sent via the page arguments to the page // callback, instead of just sending the integer. var args = arg(null, path); for (var index in drupalgap.menu_links[router_path].page_arguments) { if (!drupalgap.menu_links[router_path].page_arguments.hasOwnProperty(index)) { continue; } var object = drupalgap.menu_links[router_path].page_arguments[index]; if (is_int(object) && args[object]) { page_arguments.push(args[object]); } else { page_arguments.push(object); } } // Call the page callback function with the page arguments. content = fn.apply(null, Array.prototype.slice.call(page_arguments)); } else { // There are no arguments, just return the page callback result. content = fn(); } // If the content came back as a string, convert it to a render object // so any jQM page events can be attached to the content if necessary. if (typeof content === 'string') { content = { content: { markup: content } }; } // Clear out any previous jQM page events, then if there are any jQM // event callback functions attached to the menu link for this page, set // each event up to be fired with inline JS on the page. We pass along // any page arguments to the jQM event handler function. drupalgap.page.jqm_events = []; var jqm_page_events = drupalgap_jqm_page_events(); var jqm_page_event_args = null; if (page_arguments.length > 0) { jqm_page_event_args = JSON.stringify(page_arguments); } for (var i = 0; i < jqm_page_events.length; i++) { if (drupalgap.menu_links[router_path][jqm_page_events[i]]) { var jqm_page_event = jqm_page_events[i]; var jqm_page_event_callback = drupalgap.menu_links[router_path][jqm_page_event]; if (function_exists(jqm_page_event_callback)) { var options = { 'page_id': page_id, 'jqm_page_event': jqm_page_event, 'jqm_page_event_callback': jqm_page_event_callback, 'jqm_page_event_args': jqm_page_event_args }; content[jqm_page_event] = { markup: drupalgap_jqm_page_event_script_code(options) }; } else { console.log( 'menu_execute_active_handler (' + path + ') - the jQM ' + jqm_page_event + ' call back function ' + jqm_page_event_callback + ' does not exist!' ); } } } // Add a pageshow handler for the page title. if (typeof content === 'object') { var options = { 'page_id': page_id, 'jqm_page_event': 'pageshow', 'jqm_page_event_callback': '_drupalgap_page_title_pageshow', 'jqm_page_event_args': jqm_page_event_args }; content['drupalgap_page_title_pageshow'] = { markup: drupalgap_jqm_page_event_script_code(options) }; } // And finally return the content. return content; } else { // No page call back specified. console.log( 'menu_execute_active_handler - no page callback (' + router_path + ')' ); console.log(JSON.stringify(drupalgap.menu_links[router_path])); } } catch (error) { console.log('menu_execute_active_handler(' + path + ') - ' + error); } } /** * Gets a router item. * @return {Object} */ function menu_get_item() { try { var path = null; var router_item = null; if (arguments[0]) { path = arguments[0]; } if (arguments[1]) { router_item = arguments[1]; } if (path && drupalgap.menu_links[path]) { return drupalgap.menu_links[path]; } else { return null; } } catch (error) { console.log('menu_get_item - ' + error); } } /** * Returns default menu item options. * @return {Object} */ function menu_item_default_options() { return { attributes: menu_item_default_attributes() }; } /** * Returns default menu item attributes. * @return {Object} */ function menu_item_default_attributes() { return { 'class': '' }; } /** * Returns an array containing the names of system-defined (default) menus. * @return {Object} */ function menu_list_system_menus() { try { var system_menus = { 'user_menu_anonymous': { 'title': t('User menu authenticated') }, 'user_menu_authenticated': { 'title': t('User menu authenticated') }, 'main_menu': { 'title': t('Main menu') }, 'primary_local_tasks': { 'title': t('Primary Local Tasks') } }; // Add the menu_name to each menu as a property. for (var menu_name in system_menus) { if (!system_menus.hasOwnProperty(menu_name)) { continue; } var menu = system_menus[menu_name]; menu.menu_name = menu_name; } return system_menus; } catch (error) { console.log('menu_list_system_menus - ' + error); } } /** * Collects and alters the menu definitions. */ function menu_router_build() { // Calls all hook_menu implementations and builds a collection of menu links. try { // For each module that implements hook_menu, iterate over each of the // menu links defined by the hook, then add each menu item to // drupalgap.menu_links keyed by the path. var modules = module_implements('menu'); var function_name; var fn; var menu_links; for (var index in modules) { if (!modules.hasOwnProperty(index)) { continue; } var module = modules[index]; // Determine the hook function name, grab the function, and call it // to retrieve the hook's menu links. function_name = module + '_menu'; fn = window[function_name]; menu_links = fn(); // Iterate over each item. for (var path in menu_links) { if (!menu_links.hasOwnProperty(path)) { continue; } var menu_item = menu_links[path]; // Attach module name to item. menu_item.module = module; // Set a default type for the item if one isn't provided. if (typeof menu_item.type === 'undefined') { menu_item.type = 'MENU_NORMAL_ITEM'; } // Set default options and attributes if none have been provided. // @TODO - Now that we are doing this here, there may be a few // places throughout the code that were checking for these // undefined objects that will no longer need to be checked. if (typeof menu_item.options === 'undefined') { menu_item.options = menu_item_default_options(); } else if (typeof menu_item.options.attributes === 'undefined') { menu_item.options.attributes = menu_item_default_attributes(); } // Make the path available as a property in the menu link. menu_item.path = path; // Determine any parent, sibling, and child paths for the item. drupalgap_menu_router_build_menu_item_relationships( path, menu_item ); // Attach item to menu links. drupalgap.menu_links[path] = menu_item; } } } catch (error) { console.log('menu_router_build - ' + error); } } /** * Given a menu link path, this determines and returns the router path as a * string. * @param {String} path * @return {String} */ function drupalgap_get_menu_link_router_path(path) { try { // @TODO - Why is this function called twice sometimes? E.G. via an MVC item // view item/local_users/user/0, this function gets called twice in one page // load, that can't be good. // @TODO - this function has a limitation in the types of menu paths it can // handle, for example a menu path of 'collection/%/%/list' with a path of // 'collection/local_users/user/list' can't find eachother. So we had to // change the mvc_menu() item path to be collection/list/%/%. // @TODO - each time this function is called, we should create a static // record of the result router path, keyed by the incoming path, that way // this heavy function can be called more often with less resource. // Is this path defined in drupalgap.menu_links? If it is, use it's router // path if it is defined, otherwise just set its router path to its own // path. if (drupalgap.menu_links[path]) { if (typeof drupalgap.menu_links[path].router_path === 'undefined') { return path; } else { return drupalgap.menu_links[path].router_path; } } // Let's figure out where to route this menu item, and attach the // router to the item. var router_path = null; var args = arg(null, path); // If there is an integer in the path, replace it with the wildcard // defined via hook_menu implementation. if (args) { var args_size = args.length; switch (args[0]) { case 'comment': case 'user': case 'node': if (args_size > 1 && is_int(parseInt(args[1]))) { args[1] = '%'; router_path = args.join('/'); } break; case 'taxonomy': if (args_size > 2 && (args[1] == 'vocabulary' || args[1] == 'term') && is_int(parseInt(args[2])) ) { args[2] = '%'; router_path = args.join('/'); } break; default: if (args_size > 1 && is_int(parseInt(args[1]))) { args[1] = '%'; router_path = args.join('/'); } break; } } // If we haven't found a router path yet, try some other techniques to find // it. If all else fails, just set the router path to the path itself. if (!router_path) { // Are there any paths in drupalgap.menu_links that would be good // candidates as the router for this path? Let's start at the back of the // argument list and start replacing each with a wildcard (%) and then // see if there is a path in drupalgap.menu_links that can handle it. if (args && args.length > 1) { var temp_router_path; for (var i = args.length - 1; i != -1; i--) { temp_router_path = ''; for (var j = 0; j < args.length; j++) { if (j < i) { temp_router_path += args[j]; } else { temp_router_path += '%'; } if (j != args.length - 1) { temp_router_path += '/'; } } // If we found a router path, let's use it. if (drupalgap.menu_links[temp_router_path]) { router_path = temp_router_path; break; } } } } // If the router path is a default menu local task, inherit its parent // router path. if (drupalgap.menu_links[router_path] && drupalgap.menu_links[router_path].type == 'MENU_DEFAULT_LOCAL_TASK' && drupalgap.menu_links[router_path].parent) { router_path = drupalgap.menu_links[router_path].parent; } // If there isn't a router and we couldn't find one, we'll just route // to the path itself. if (!router_path) { router_path = path; } // Finally, return the router path. return router_path; } catch (error) { console.log('drupalgap_get_menu_link_router_path - ' + error); } } /** * Loads all of the menus specified in drupalgap.settings.menus into * drupalgap.menus. This is called after menu_router_build(), so any system * defined menus will already be present and should be overwritten with any * customizations present in the settings. It then iterates over the menu links * specified in drupalgap.menu_links and attaches any of them that have a * menu_name to their corresponding menu in drupalgap.menus. Any menu link items * that have a 'region' property specified will be added to * drupalgap.theme.regions[region]. */ function drupalgap_menus_load() { try { if (drupalgap.settings.menus) { // Process each menu defined in the settings. for (var menu_name in drupalgap.settings.menus) { if (!drupalgap.settings.menus.hasOwnProperty(menu_name)) { continue; } var menu = drupalgap.settings.menus[menu_name]; // If the menu does not already exist, it is a custom menu, so create // the menu and its corresponding block. if (!drupalgap.menus[menu_name]) { // If the custom menu doesn't have its machine name set, set it. if (!menu.menu_name) { menu.menu_name = menu_name; } // Save the custom menu, as long is its name isn't 'regions', // because that is a special "system" menu that allows menu links to // be placed directly in regions via settings.js. Keep in mind the // 'regions' menu is in fact NOT a system menu. if (menu_name != 'regions') { menu_save(menu); // Make a block for this custom menu. var block_delta = menu.menu_name; drupalgap.blocks[0][block_delta] = { name: block_delta, delta: block_delta, module: 'menu' }; } } else { // The menu is a system defined menu, merge it together with any // custom settings. $.extend(true, drupalgap.menus[menu_name], menu); } } // Now that we have all of the menus loaded up, and the menu router is // built, let's iterate over all the menu links and perform various // operations on them. for (var path in drupalgap.menu_links) { if (!drupalgap.menu_links.hasOwnProperty(path)) { continue; } var menu_link = drupalgap.menu_links[path]; // Let's grab any links from the router that have a menu specified, // and add the link to the router. if (menu_link.menu_name) { if (drupalgap.menus[menu_link.menu_name]) { // Create a links array for the menu if one doesn't exist already. if (!drupalgap.menus[menu_link.menu_name].links) { drupalgap.menus[menu_link.menu_name].links = []; } // Add the path to the menu link inside the menu. menu_link.path = path; // Now push the link onto the menu. We only care about the title, // path and options, as this is just a link. The rest of the // menu link data can be retrieved from drupalgap.menu_links. var link = drupalgap_menus_load_convert_menu_link_to_link_json(menu_link); drupalgap.menus[menu_link.menu_name].links.push(link); } else { console.log( 'drupalgap_menus_load - menu does not exist (' + menu_link.menu_name + '), cannot attach link to it (' + path + ')' ); } } // If the menu link is set to a specific region, create a links array // for the region if one doesn't exist already, then add the menu item // to the links array as a link. if (menu_link.region) { if (!drupalgap.theme.regions[menu_link.region.name].links) { drupalgap.theme.regions[menu_link.region.name].links = []; } drupalgap.theme.regions[menu_link.region.name].links.push( menu_link ); } } // If there are any region menu links defined in settings.js, create a // links array for the region if one doesn't exist already, then add the // menu item to the links array as a link. if (typeof drupalgap.settings.menus.regions !== 'undefined') { for (var region in drupalgap.settings.menus.regions) { if (!drupalgap.settings.menus.regions.hasOwnProperty(region)) { continue; } var menu = drupalgap.settings.menus.regions[region]; if ( typeof menu.links !== 'undefined' && $.isArray(menu.links) && menu.links.length > 0 ) { if (!drupalgap.theme.regions[region].links) { drupalgap.theme.regions[region].links = []; } for (var index in menu.links) { if (!menu.links.hasOwnProperty(index)) { continue; } var link = menu.links[index]; drupalgap.theme.regions[region].links.push(link); } } } } } } catch (error) { console.log('drupalgap_menus_load - ' + error); } } /** * Given a menu link item from drupalgap_menus_load(), this will return a JSON * object representing a link object compatible with theme_link(). It contains * the link title, path and options. * @param {Object} menu_link * @return {Object} */ function drupalgap_menus_load_convert_menu_link_to_link_json(menu_link) { try { var link = {}; if (menu_link.title) { // TODO - this is strange, we have to fill the 'text' value so theme_link // will play nice. These two properties, and their usage, need a thorough // review, only one should probably be used. // UPDATE - the link.text property is probably no longer used, it should // be safe to get rid of. In fact, this whole function is dumb and should // go away. link.title = menu_link.title; link.text = menu_link.title; } if (menu_link.path) { link.path = menu_link.path; } if (menu_link.options) { link.options = menu_link.options; } // If it is a menu link on a region, and it has options, set the link // options to the ones provided in the menu link region settings. if (menu_link.region && menu_link.region.options) { link.options = menu_link.options = menu_link.region.options; } return link; } catch (error) { console.log( 'drupalgap_menus_load_convert_menu_link_to_link_json - ' + error ); } } /** * Given a path, and its corresponding menu item, this will determine any * parent, sibling, and/or child menu item paths and set the references on each * so they are all aware of eachother's paths. * @param {String} path * @param {Object} menu_item */ function drupalgap_menu_router_build_menu_item_relationships(path, menu_item) { try { // Split up the path arguments. var args = arg(null, path); // Any parent? if (args.length > 1) { // Set the parent path. var parent = args.splice(0, args.length - 1).join('/'); menu_item.parent = parent; // Make sure the parent exists. if (drupalgap.menu_links[parent]) { // Now tell the parent about this child. If the parent doesn't yet have // any children, setup the children array on the parent. if (typeof drupalgap.menu_links[parent].children === 'undefined') { drupalgap.menu_links[parent].children = []; } drupalgap.menu_links[parent].children.push(path); // Now tell any siblings about this item, and tell this item about any // siblings. if (typeof menu_item.siblings === 'undefined') { menu_item.siblings = []; } for (var index in drupalgap.menu_links[parent].children) { if (!drupalgap.menu_links[parent].children.hasOwnProperty(index)) { continue; } var sibling = drupalgap.menu_links[parent].children[index]; if (sibling != path && drupalgap.menu_links[sibling]) { if ( typeof drupalgap.menu_links[sibling].siblings === 'undefined' ) { drupalgap.menu_links[sibling].siblings = []; } drupalgap.menu_links[sibling].siblings.push(path); menu_item.siblings.push(sibling); } } } } } catch (error) { console.log('drupalgap_menu_router_build_relationships - ' + error); } } /** * Show the jQueryMobile loading message. * @see http://stackoverflow.com/a/16277865/763010 */ function drupalgap_loading_message_show() { try { // Backwards compatibility for versions prior to 7.x-1.6-alpha if (drupalgap.loading === 'undefined') { drupalgap.loading = false; } // Return if the loading message is already shown. if (drupalgap.loading || drupalgap_toast_is_shown()) { return; } var options = drupalgap_loader_options(); if (arguments[0]) { options = arguments[0]; } // Show the loading message. //$.mobile.loading('show', options); //drupalgap.loading = true; setTimeout(function() { $.mobile.loading('show', options); drupalgap.loading = true; }, 1); } catch (error) { console.log('drupalgap_loading_message_show - ' + error); } } /** * Hide the jQueryMobile loading message. */ function drupalgap_loading_message_hide() { try { if (drupalgap_toast_is_shown()) { return; } setTimeout(function() { $.mobile.loading('hide'); drupalgap.loading = false; drupalgap.loader = 'loading'; }, 100); } catch (error) { console.log('drupalgap_loading_message_hide - ' + error); } } /** * Returns the jQM loader options based on the current mode and settings.js. * @return {Object} */ function drupalgap_loader_options() { try { var mode = drupalgap.loader; var text = t('Loading') + '...'; var textVisible = true; if (mode == 'saving') { var text = t('Saving') + '...'; } var options = { text: text, textVisible: textVisible }; if (drupalgap.settings.loader && drupalgap.settings.loader[mode]) { options = $.extend(true, options, drupalgap.settings.loader[mode]); if (options.text) { options.text = t(options.text); } } return options; } catch (error) { console.log('drupalgap_loader_options - ' + error); } } /** * Sets a message to display to the user. Optionally pass in a second argument * to specify the message type: status, warning, error * @param {String} message */ function drupalgap_set_message(message) { try { if (empty(message)) { return; } var type = 'status'; if (arguments[1]) { type = arguments[1]; } var msg = { message: message, type: type }; drupalgap.messages.push(msg); } catch (error) { console.log('drupalgap_set_message - ' + error); } } /** * Sets the current messages. * @param {Array} messages */ function drupalgap_set_messages(messages) { try { drupalgap.messages = messages; } catch (error) { console.log('drupalgap_set_messages - ' + error); } } /** * Returns the current messages. * @return {Array} */ function drupalgap_get_messages() { try { return drupalgap.messages; } catch (error) { console.log('drupalgap_get_messages - ' + error); } } /** * Clears the messages from the current page. Optionally pass in a page id to * clear messages from a particular page. */ function drupalgap_clear_messages() { try { var page_id = arguments[0]; if (empty(page_id)) { page_id = drupalgap_get_page_id(); } $('#' + page_id + ' div.messages').remove(); } catch (error) { console.log('drupalgap_clear_messages - ' + error); } } /** * Alerts a message to the user using PhoneGap's alert. It is important to * understand this is an async function, so code will continue to execute while * the alert is displayed to the user. * You may optionally pass in a second argument as a JSON object with the * following properties: * alertCallback - the function to call after the user presses OK * title - the title to use on the alert box, defaults to 'Alert' * buttonName - the text to place on the button, default to 'OK' * @param {String} message */ function drupalgap_alert(message) { try { var options = null; if (arguments[1]) { options = arguments[1]; } var alertCallback = function() { }; var title = t('Alert'); var buttonName = t('OK'); if (options) { if (options.alertCallback) { alertCallback = options.alertCallback; } if (options.title) { title = options.title; } if (options.buttonName) { buttonName = options.buttonName; } } if ( drupalgap.settings.mode != 'phonegap' || typeof navigator.notification === 'undefined' ) { alert(message); alertCallback(); } else { navigator.notification.alert(message, alertCallback, title, buttonName); } } catch (error) { console.log('drupalgap_alert - ' + error); } } /** * Displays a confirmation message to the user using PhoneGap's confirm. It is * important to understand this is an async function, so code will continue to * execute while the confirmation is displayed to the user. * You may optionally pass in a second argument as a JSON object with the * following properties: * confirmCallback - the function to call after the user presses a button, the * button's label is passed to this function. * title - the title to use on the alert box, defaults to 'Confirm' * buttonLabels - the text to place on the OK, and Cancel buttons, separated * by comma. * @param {String} message * @return {Boolean} */ function drupalgap_confirm(message) { try { var options = null; if (arguments[1]) { options = arguments[1]; } var confirmCallback = function(button) { }; var title = t('Confirm'); var buttonLabels = [t('OK'), t('Cancel')]; if (options) { if (options.confirmCallback) { confirmCallback = options.confirmCallback; } if (options.title) { title = options.title; } if (options.buttonLabels) { buttonLabels = options.buttonLabels; } } // The phonegap confirm dialog doesn't seem to work in Ripple, so just use // the default one, and it definitely doesn't work in a web app, so // otherwise just use the default confirm. if ( typeof parent.window.ripple === 'function' || drupalgap.settings.mode == 'web-app' ) { var r = confirm(message); if (r == true) { confirmCallback(1); } // OK button. else { confirmCallback(2); } // Cancel button. } else { navigator.notification.confirm( message, confirmCallback, title, buttonLabels ); } return false; } catch (error) { console.log('drupalgap_confirm - ' + error); } } /** * Show a non intrusive alert message. You may optionally pass in an * integer value as the second argument to specify how many milliseconds * to wait before closing the message. Likewise, you can pass in a * third argument to specify how long to wait before opening the * message. * @param {string} html - The html to display. */ function drupalgap_toast(html) { try { var open = arguments[2] ? arguments[2] : 750; var close = arguments[1] ? arguments[1] : 1500; setTimeout(function() { drupalgap.toast.shown = true; $.mobile.loading('show', { textVisible: true, html: html }); var interval = setInterval(function () { $.mobile.loading('hide'); drupalgap.toast.shown = false; clearInterval(interval); }, close); }, open); } catch (error) { console.log('drupalgap_toast - ' + error); } } /** * Returns true if the toast is currently shown, false otherwise. * @returns {Boolean} */ function drupalgap_toast_is_shown() { return drupalgap.toast.shown; } /** * Prompt's the user to input some text. * @param {String} msg The message to prompt about * @param {Object} options * {Function} onPrompt - the callback function to invoke. Sends an object in phonegap mode, or a string in web-app mode. * {String} title - the title of the box * {Array} buttonLabels - the button labels, separated by comma, defaults to ['Ok', 'Exit']] * {String} defaultText - the default text to place in the box */ function drupalgap_prompt(msg, options) { var onPrompt = options.onPrompt ? options.onPrompt : function() {}; var title = options.title ? options.title : drupalgap.settings.title; var buttonLabels = options.buttonLabels ? options.buttonLabels : [t('Ok'), t('Exit')]; var defaultText = options.defaultText ? options.defaultText : ''; var done = function(result) { if (typeof result === 'object' && result != null) { onPrompt(result); } else { var send = { buttonIndex: 2, input1: '' }; if (result != null) { send.buttonIndex = 1; send.input1 = result; } onPrompt(send); } }; if (navigator && navigator.notification) { navigator.notification.prompt(msg, done, title, buttonLabels, defaultText); } else { done(prompt(msg, defaultText)); } } /** * This will return the query string arguments for the page. You may optionally * pass in a key to get its value, pass in a key then a value to set the key * equal to the value, and you may optionally pass in a third argument to use * a specific page id, otherwise DrupalGap will automatically use the * appropriate page id. * @return {String|NULL} */ function _GET() { try { // Set up defaults. var get = false; var set = false; var key = null; var value = null; // Are we setting? If so, grab the value and key to set. if (typeof arguments[1] !== 'undefined') { set = true; value = arguments[1]; if (typeof arguments[0] !== 'undefined') { key = arguments[0]; } else { console.log('WARNING: _GET - missing key for value (' + value + ')'); return null; } } // Are we getting a certain value? If so, grab the key to get. else if (typeof arguments[0] !== 'undefined') { get = true; key = arguments[0]; } // Otherwise we are getting the whole page. else { get = true; } // Now perform the get or set... // Get. if (get) { // If a page id was provided use it, otherwise use the current page's id. var id = null; if (typeof arguments[2] !== 'undefined') { id = arguments[2]; } else { id = drupalgap_get_page_id(); } // Now that we know the page id, lets return the value if a key was // provided, otherwise return the whole query string object for the page. if (typeof _dg_GET[id] !== 'undefined') { if (!key) { return _dg_GET[id]; } else if (typeof _dg_GET[id][key] !== 'undefined') { return _dg_GET[id][key]; } return null; } } // Set. else if (set) { // If we were given a path, use its page id as the property index, other // wise we'll use the current page (which is different than the // destination page!). var id = null; if (typeof arguments[2] !== 'undefined') { id = drupalgap_get_page_id(arguments[2]); } else { id = drupalgap_get_page_id(); } // If the id hasn't been instantiated, do so. Then set the key and value // onto it. if (typeof _dg_GET[id] === 'undefined') { _dg_GET[id] = {}; } if (value) { _dg_GET[id][key] = value; } } return null; } catch (error) { console.log('_GET - ' + error); } } /** * Each time we use drupalgap_goto to change a page, this function is called on * the pagebeforehange event. If we're not moving backwards, or navigating to * the same page, this will preproccesses the page, then processes it. */ $(document).on('pagebeforechange', function(e, data) { try { // If we're moving backwards, reset drupalgap.back and return. if (drupalgap && drupalgap.back) { drupalgap.back = false; return; } // If the jqm active page url is the same as the page id of the current // path, return. if ( drupalgap_jqm_active_page_url() == drupalgap_get_page_id(drupalgap_path_get()) ) { return; } // We only want to process the page we are going to, not the page we are // coming from. When data.toPage is a string that is our destination page. if (typeof data.toPage === 'string') { // If drupalgap_goto() determined that it is necessary to prevent the // default page from reloading, then we'll skip the page // processing and reset the prevention boolean. if (drupalgap && !drupalgap.page.process) { drupalgap.page.process = true; } else if (drupalgap) { // Pre process, then process the page. template_preprocess_page(drupalgap.page.variables); template_process_page(drupalgap.page.variables); } } } catch (error) { console.log('pagebeforechange - ' + error); } }); /** * Implementation of template_preprocess_page(). * @param {Object} variables */ function template_preprocess_page(variables) { try { // Set up default attributes for the page's div container. if (typeof variables.attributes === 'undefined') { variables.attributes = {}; } // @todo - is this needed? // @UPDATE - this should be used, but these page attributes are ignored // by drupalgap_add_page_to_dom()! variables.attributes['data-role'] = 'page'; module_invoke_all('preprocess_page', variables); // Place the variables into drupalgap.page drupalgap.page.variables = variables; } catch (error) { console.log('template_preprocess_page - ' + error); } } /** * Implementation of template_process_page(). * @param {Object} variables */ function template_process_page(variables) { try { var drupalgap_path = drupalgap_path_get(); // Execute the active menu handler to assemble the page output. We need to // do this before we render the regions below. drupalgap.output = menu_execute_active_handler(); // For each region, render it, then replace the placeholder in the page's // html with the rendered region. var page_id = drupalgap_get_page_id(drupalgap_path); var page = $('#' + page_id); var page_html = $(page).html(); if (!page_html) { return; } for (var index in drupalgap.theme.regions) { if (!drupalgap.theme.regions.hasOwnProperty(index)) { continue; } var region = drupalgap.theme.regions[index]; var _region = {}; $.extend(true, _region, region); page_html = page_html.replace( '{:' + region.name + ':}', drupalgap_render_region(_region) ); } $(page).html(page_html); module_invoke_all('post_process_page', variables); } catch (error) { console.log('template_process_page - ' + error); } } /** * Given a path, this will return the id for the page's div element. * For example, a string path of 'foo/bar' would result in an id of 'foo_bar'. * If no path is provided, it will return the current page's id. * @param {String} path * @return {String} */ function drupalgap_get_page_id(path) { try { if (!path) { path = drupalgap_path_get(); } var id = path.toLowerCase().replace(/\//g, '_').replace(/-/g, '_'); return id; } catch (error) { console.log('drupalgap_get_page_id - ' + error); } } /** * Given a page id, the theme's hook_TYPE_tpl_html() string, and the menu link object * (all bundled in options) this takes the page template html and adds it to the * DOM. It doesn't actually render the page, that is taken care of by the * pagebeforechange handler. * @param {Object} options */ function drupalgap_add_page_to_dom(options) { try { // Prepare the default page attributes, then merge in any customizations // from the hook_menu() item, then inject the attributes into the // placeholder. We have to manually add our default class name after the // extend until this issue is resolved: // https://github.com/signalpoint/DrupalGap/issues/321 var attributes = { id: options.page_id, 'data-role': 'page' }; attributes = $.extend(true, attributes, options.menu_link.options.attributes); attributes['class'] += ' ' + drupalgap_page_class_get(drupalgap.router_path); module_invoke_all('add_page_to_dom_alter', attributes, options); options.html = options.html.replace( /{:drupalgap_page_attributes:}/g, drupalgap_attributes(attributes) ); // Add the html to the page and the page id to drupalgap.pages. $('body').append(options.html); drupalgap.pages.push(options.page_id); } catch (error) { console.log('drupalgap_add_page_to_dom - ' + error); } } /** * Attempts to remove given page from the DOM, will not remove the current page. * You may force the removal by passing in a second argument as a JSON object * with a 'force' property set to true. You may pass in a third argument to * specify the current page, otherwise it will default to what DrupalGap thinks * is the current page. No matter what, the current page (specified or not) * can't be removed from the DOM, because jQM always needs one page in the DOM. * @param {String} page_id */ function drupalgap_remove_page_from_dom(page_id) { try { var current_page_id = null; if (typeof arguments[2] !== 'undefined') { current_page_id = arguments[2]; } else { current_page_id = drupalgap_get_page_id(drupalgap_path_get()); } var options = {}; if (typeof arguments[1] !== 'undefined') { options = arguments[1]; } if (current_page_id != page_id || options.force) { var currentPage = $('#' + current_page_id); // Preserve and re-apply style to current page, @see https://github.com/signalpoint/DrupalGap/issues/837 var style = $(currentPage).attr('style'); $('#' + page_id).empty().remove(); if (style) { $(currentPage).attr('style', style); } var page_index = drupalgap.pages.indexOf(page_id); if (page_index > -1) { drupalgap.pages.splice(page_index, 1); } // We'll remove the query string, unless we were instructed to leave it. if ( typeof _dg_GET[page_id] !== 'undefined' && (typeof options.leaveQuery === 'undefined' || !options.leaveQuery) ) { delete _dg_GET[page_id]; } // Remove any embedded view for the page. views_embedded_view_delete(page_id); } else { console.log('WARNING: drupalgap_remove_page_from_dom() - not removing ' + 'the current page (' + page_id + ') from the DOM!'); } } catch (error) { console.log('drupalgap_remove_page_from_dom - ' + error); } } /** * Removes all pages from the DOM except the current one. */ function drupalgap_remove_pages_from_dom() { try { var current_page_id = drupalgap_get_page_id(drupalgap_path_get()); var pages = drupalgap.pages.slice(0); for (var index in pages) { if (!pages.hasOwnProperty(index)) { continue; } var page_id = pages[index]; if (current_page_id != page_id) { drupalgap_remove_page_from_dom(page_id, null, current_page_id); } } // Reset drupalgap.pages to only contain the current page id. drupalgap.pages = [current_page_id]; // Reset the drupalgap.views.ids array. drupalgap.views.ids = []; // Reset the jQM page events. drupalgap.page.jqm_events = []; // Reset the back path. drupalgap.back_path = []; } catch (error) { console.log('drupalgap_remove_pages_from_dom - ' + error); } } /** * Given a router path, this will return the CSS class name that can be used for * the page container. * @param {String} router_path The page router path. * @return {String} A css class name. */ function drupalgap_page_class_get(router_path) { try { // Replace '/' and '%' with underscores, then trim any trailing underscores. var class_name = router_path.replace(/[\/%]/g, '_'); while (class_name.lastIndexOf('_') == class_name.length - 1) { class_name = class_name.substr(0, class_name.length - 1); } return class_name; } catch (error) { console.log('drupalgap_page_class_get - ' + error); } } /** * Returns true if the given page id's page div already exists in the DOM. * @param {String} page_id * @return {Boolean} */ function drupalgap_page_in_dom(page_id) { try { var pages = $("body div[data-role$='page']"); var page_in_dom = false; if (pages && pages.length > 0) { for (var index in pages) { if (!pages.hasOwnProperty(index)) { continue; } var page = pages[index]; if (($(page).attr('id')) == page_id) { page_in_dom = true; break; } } } return page_in_dom; } catch (error) { console.log('drupalgap_page_in_dom - ' + error); } } /** * Returns true if the current page is the front page, false otherwise. * @return {Boolean} */ function drupalgap_is_front_page() { try { return drupalgap_path_get() == drupalgap.settings.front; } catch (error) { console.log('drupalgap_is_front_page - ' + error); } } /** * Returns the URL of the active jQuery Mobile page. * @return {String} */ function drupalgap_jqm_active_page_url() { try { // WARNING: when the app first loads, this value may be much different than // you expect. It certainly is not the front page path, because on Android // for example it returns '/android_asset/www/index.html'. Also, when the // app first loads, activePage is null, so just return an empty string. if (!$.mobile.activePage) { return ''; } return $.mobile.activePage.data('url'); } catch (error) { console.log('drupalgap_jqm_active_page_url - ' + error); } } /** * Renders the html string of the page content that is stored in * drupalgap.output. * @return {String} */ function drupalgap_render_page() { try { module_invoke_all('page_build', drupalgap.output); return drupalgap_render(drupalgap.output); } catch (error) { console.log('drupalgap_render_page - ' + error); } } /** * Given a region, this renders it and all the blocks in it. The blocks are * specified in the settings.js file, they are bundled under a region, which in * turn is bundled under a theme name. Returns an empty string if it fails. * @param {Object} region * @return {String} */ function drupalgap_render_region(region) { try { // @TODO - this function is getting huge. Break it up into many more // manageable functions. // Make sure there are blocks specified for this theme in settings.js. if (!drupalgap.settings.blocks[drupalgap.settings.theme]) { var msg = 'WARNING: drupalgap_render_region() - there are no blocks ' + 'for the "' + drupalgap.settings.theme + '" theme in the settings.js ' + 'file!'; console.log(msg); return ''; } // Grab the current path. var current_path = drupalgap_path_get(); // Let's render the region... var region_html = ''; region_html += _drupalgap_region_render_zone('_prefix', region, current_path); // Verify the region has any blocks. var hasBlocks = drupalgap.settings.blocks[drupalgap.settings.theme][region.name]; if (hasBlocks) { var blockCount = 0; $.each(drupalgap.settings.blocks[drupalgap.settings.theme][region.name], function(index, block) { if (!in_array(index, ['_prefix', '_suffix'])) { blockCount++; } }); hasBlocks = blockCount; } // If the region has blocks specified for it in the theme in settings.js... if (hasBlocks) { // If a class attribute hasn't yet been provided, set a default, then // append a system class name for the region onto its attributes array. if (!region.attributes['class']) { region.attributes['class'] = ''; } region.attributes['class'] += ' region_' + region.name + ' '; // Open the region container. region_html += '
    '; // If there are any links attached to this region, render them first. var region_link_count = 0; var region_link_popup_count = 0; if (region.links && region.links.length > 0) { // Let's first iterate over all of the region links and keep counts of // any links that use the ui-btn-left and ui-btn-right class attribute. // This will allow us to properly wrap region links in a control group. var ui_btn_left_count = 0; var ui_btn_right_count = 0; for (var index in region.links) { if (!region.links.hasOwnProperty(index)) { continue; } var link = region.links[index]; var data = menu_region_link_get_data(link); if (!drupalgap_check_visibility('region', data)) { continue; } region_link_count++; var css_class = drupalgap_link_get_class(link); if (css_class) { var side = menu_region_link_get_side(css_class); if (side == 'left') { ui_btn_left_count++; } else if (side == 'right') { ui_btn_right_count++; } } } // We need to separately render each side of the header (left, right). // That allows us to properly wrap the links with a control group if // it is needed. var region_link_html = ''; var ui_btn_left_html = ''; var ui_btn_right_html = ''; for (var i = 0; i < region.links.length; i++) { // Grab the link and its data. var region_link = region.links[i]; var data = menu_region_link_get_data(region_link); // Check link's region visiblity settings. Links will not be rendered // on certain system pages. // @TODO - this additional call to drupalgap_check_visibility() here // may be expensive, consider setting aside the results from the call // above, and using them here. if (drupalgap_check_visibility('region', data)) { // Don't render the link on certain system pages. if (in_array(current_path, ['offline', 'error', 'user/logout'])) { continue; } // If this is a popup region link, set the jQM attributes to make // this link function as a popup (dropdown) menu. Set the default // link icon, if it isn't set. var link_text = region_link.title; var link_path = region_link.path; if (data.options.popup) { region_link_popup_count++; // If the link text isn't set, and the data icon pos isn't set, // set it the data icon pos so the button and icon are rendered // properly. if ( (!link_text || empty(link_text)) && typeof data.options.attributes['data-iconpos'] === 'undefined' ) { data.options.attributes['data-iconpos'] = 'notext'; } // If data-rel, data-icon, data-role aren't set, set them. if ( typeof data.options.attributes['data-rel'] === 'undefined' ) { data.options.attributes['data-rel'] = 'popup'; } if ( typeof data.options.attributes['data-icon'] === 'undefined' ) { data.options.attributes['data-icon'] = 'bars'; } if ( typeof data.options.attributes['data-role'] === 'undefined' ) { data.options.attributes['data-role'] = 'button'; } // Popup menus need a dynamic href value on the link, so we // always overwrite it. link_path = null; data.options.attributes['href'] = '#' + menu_container_id(data.options.popup_delta); } else { // Set the data-role to a button, if one isn't already set. if (typeof data.options.attributes['data-role'] === 'undefined') { data.options.attributes['data-role'] = 'button'; } } // If it has notext for the icon position, force the text to be // an nbsp. if (data.options.attributes['data-iconpos'] == 'notext') { link_text = ' '; } // Render the link on the proper side. var css_class = drupalgap_link_get_class(region_link); var side = menu_region_link_get_side(css_class); var link_html = l(link_text, link_path, data.options); if (side == 'left') { ui_btn_left_html += link_html; } else if (side == 'right') { ui_btn_right_html += link_html; } } } // If there was more than one link on a side, wrap it in a control // group, and remove the ui-btn class from the links. if (ui_btn_left_count > 1) { var attrs = { 'data-type': 'horizontal', 'data-role': 'controlgroup', 'class': 'ui-btn-left' }; ui_btn_left_html = '
    ' + ui_btn_left_html.replace(/ui-btn-left/g, '') + '
    '; } if (ui_btn_right_count > 1) { var attrs = { 'data-type': 'horizontal', 'data-role': 'controlgroup', 'class': 'ui-btn-right' }; ui_btn_right_html = '
    ' + ui_btn_right_html.replace(/ui-btn-right/g, '') + '
    '; } // Finally render the ui sides on the region. region_html += ui_btn_left_html + ui_btn_right_html; } // Render each block in the region. Determine how many visible blocks are // in the region. var block_counts = { block_count: 0, block_menu_count: 0 }; var blocks = drupalgap.settings.blocks[drupalgap.settings.theme][region.name]; for (var block_delta in blocks) { if (!blocks.hasOwnProperty(block_delta)) { continue; } var block_settings = blocks[block_delta]; // Ignore region _prefix and _suffix. if (block_delta == '_prefix' || block_delta == '_suffix') { continue; } // Render the block. region_html += drupalgap_block_render( region, current_path, block_delta, block_settings, block_counts ); } // If this was a header or footer, and there were only region links // rendered, place an empty header in the region. if ( in_array(region.attributes['data-role'], ['header', 'footer']) && ( block_counts.block_count == 0 && region_link_count > 0 || block_counts.block_count - block_counts.block_menu_count == 0 ) || ( region_link_count > 0 && region_link_popup_count >= block_counts.block_menu_count && block_counts.block_count == 0 ) ) { // Show an empty header if we're not collapsing on an empty region. if ( typeof region.collapse_on_empty === 'undefined' || region.collapse_on_empty === false ) { region_html += '

     

    '; } } // Close the region container. region_html += '
    '; } region_html += _drupalgap_region_render_zone('_suffix', region, current_path); return region_html; } catch (error) { console.log('drupalgap_render_region - ' + error); } } /** * Renders the given zone (_prefix, _suffix) if any for a region. * @param {String} zone * @param {Object} region * @param {String} current_path * @return {String} */ function _drupalgap_region_render_zone(zone, region, current_path) { try { var html = ''; var theme_name = drupalgap.settings.theme; if (typeof drupalgap.settings.blocks[theme_name][region.name] === 'undefined' ) { return html; } var region_settings = drupalgap.settings.blocks[theme_name][region.name]; if (typeof region_settings[zone] === 'undefined') { return html; } var blocks = region_settings[zone]; for (var block_delta in blocks) { if (!blocks.hasOwnProperty(block_delta)) { continue; } var block_settings = blocks[block_delta]; html += drupalgap_block_render( region, current_path, block_delta, block_settings ); } return html; } catch (error) { console.log('_drupalgap_region_render_zone - ' + error); } } /** * Given a key (typically a block delta), this will generate a unique ID that * can be used for the panel. It will be fused with the current page id. * @param {String} key * @return {String} */ function drupalgap_panel_id(key) { try { return key + '_' + drupalgap_get_page_id(); } catch (error) { console.log('drupalgap_panel_id - ' + error); } } function drupalgap_render(content) { var output_type = $.type(content); var html = ''; // If the output came back as a string, we can render it as is. If the // output came back as on object, render each element in it through the // theme system. if (output_type === 'string') { html = content; } else if (output_type === 'object') { // The page came back as a render object. Let's define the names of // variables that are reserved for theme processing. var render_variables = ['theme', 'view_mode', 'language']; if (content.markup) { return content.markup; } if (content.theme && !drupalgap.theme_registry[content.theme]) { return theme(content.theme, content); } // Is there a theme value specified in the content and the registry? if (content.theme && drupalgap.theme_registry[content.theme]) { var themeName = drupalgap.settings.theme; var tplHtmlFunctionName = themeName + '_' + content.theme + '_tpl_html'; template_file_html = ''; if (function_exists(tplHtmlFunctionName)) { template_file_html = window[tplHtmlFunctionName](); } else { var template = drupalgap.theme_registry[content.theme]; var template_file_name = content.theme.replace(/_/g, '-') + '.tpl.html'; var template_file_path = template.path + '/' + template_file_name; console.log('@deprecated: ' + template_file_path + ' - use ' + tplHtmlFunctionName + '() in ' + themeName + '.js instead, see: http://docs.drupalgap.org/7/Themes/Create_a_Custom_Theme'); template_file_html = drupalgap_file_get_contents(template_file_path); } // What variable placeholders are present in the template file? var placeholders = drupalgap_get_placeholders_from_html(template_file_html); if (placeholders) { // Replace each placeholder with html. for (var index in placeholders) { if (!placeholders.hasOwnProperty(index)) { continue; } var placeholder = placeholders[index]; var _html = ''; if (content[placeholder]) { // Grab the element variable from the content. var element = content[placeholder]; // If it is markup, render it as is, if it is themeable, then theme it. if (content[placeholder].markup) { _html = content[placeholder].markup; } else if (content[placeholder].theme) { _html = theme(content[placeholder].theme, element); } // Now remove the variable from the content. delete content[placeholder]; } // Now replace the placeholder with the html, even if it was empty. template_file_html = template_file_html.replace('{:' + placeholder + ':}', _html); } } // Finally add the rendered template file to the html. html += template_file_html; } else { var weighted = {}; var weightedCount = 0; for (var index in content) { if (!content.hasOwnProperty(index)) { continue; } var piece = content[index]; var _type = typeof piece; if (_type === 'object' && piece !== null) { if (piece.theme && drupalgap.theme_registry[piece.theme]) { continue; } var weight = typeof piece.weight !== 'undefined' ? piece.weight : 0; if (typeof weighted[weight] === 'undefined') { weighted[weight] = []; } weighted[weight].push(drupalgap_render(piece)); weightedCount++; } else if (_type === 'array') { for (var i = 0; i < piece.length; i++) { html += drupalgap_render(piece[i]); } } else if (_type === 'string') { html += piece; } } if (weightedCount) { for (var weight in weighted) { if (!weighted.hasOwnProperty(weight)) { continue; } for (var i = 0; i < weighted[weight].length; i++) { html += weighted[weight][i]; } } } } } // Now that we are done assembling the content into an html string, we can // return it. return html; } /** * Returns the path to the current DrupalGap theme, false otherwise. * @return {String|Boolean} */ function path_to_theme() { try { if (drupalgap.theme_path) { return drupalgap.theme_path; } else { console.log('path_to_theme - drupalgap.theme_path is not set!'); return false; } } catch (error) { console.log('path_to_theme - ' + error); } } /** * Implementation of theme(). * @param {String} hook * @param {Object} variables * @return {String} */ function theme(hook, variables) { try { // If there is HTML markup present, just return it as is. Otherwise, run // the theme hook and send along the variables. if (!variables) { variables = {}; } if (typeof variables.access !== 'undefined' && !variables.access) { return ''; } if (variables.markup) { return variables.markup; } var content = ''; if (!hook) { return content; } // First see if the current theme implements the hook, if it does use it, if // it doesn't fallback to the core theme implementation of the hook. var theme_function = drupalgap.settings.theme + '_' + hook; if (!function_exists(theme_function)) { theme_function = 'theme_' + hook; // Fail safely an informative message if a bogus hook was passed in. if (!function_exists(theme_function)) { var caller = null; if (arguments.callee.caller) { caller = arguments.callee.caller.name; } var msg = 'WARNING: ' + theme_function + '() does not exist.'; if (caller) { msg += ' Called by: ' + caller + '().' } console.log(msg); return content; } } // If no attributes are coming in, look to variables.options.attributes // as a secondary option, otherwise setup an empty JSON object for them. if ( typeof variables.attributes === 'undefined' || !variables.attributes ) { if (variables.options && variables.options.attributes) { variables.attributes = variables.options.attributes; } else { variables.attributes = {}; } } // If there is no class name, set an empty one. if (!variables.attributes['class']) { variables.attributes['class'] = ''; } var fn = window[theme_function]; content = fn.call(null, variables); return content; } catch (error) { console.log('theme - ' + error); } } /** * Themes a button. * @param {Object} variables * @return {String} */ function theme_button(variables) { try { variables.attributes['data-role'] = 'button'; var html = '' + variables.text + ''; return html; } catch (error) { console.log('theme_button_link - ' + error); } } /** * Themes a button link. * @param {Object} variables * @return {String} */ function theme_button_link(variables) { try { variables.attributes['data-role'] = 'button'; return theme_link(variables); } catch (error) { console.log('theme_button_link - ' + error); } } /** * Themes a collapsible widget. * @param {Object} variables * @return {String} */ function theme_collapsible(variables) { try { variables.attributes['data-role'] = 'collapsible'; var header_type = 'h2'; if (variables.header_type) { header_type = variables.header_type; } var header_attributes = {}; if (variables.header_attributes) { header_attributes = variables.header_attributes; } var html = '
    ' + '<' + header_type + ' ' + drupalgap_attributes(header_attributes) + '>' + variables.header + '' + variables.content + '
    '; return html; } catch (error) { console.log('theme_collapsible - ' + error); } } /** * Themes a collapsibleset widget. * @param {Object} variables * @return {String} */ function theme_collapsibleset(variables) { try { variables.attributes['data-role'] = 'collapsible-set'; var html = '
    '; for (var index in variables.items) { if (!variables.items.hasOwnProperty(index)) { continue; } var item = variables.items[index]; html += theme('collapsible', item); } html += '
    '; return html; } catch (error) { console.log('theme_collapsibleset - ' + error); } } /** * Themes a controlgroup widget. * @param {Object} variables * @return {String} */ function theme_controlgroup(variables) { try { variables.attributes['data-role'] = 'controlgroup'; var html = '
    '; for (var index in variables.items) { if (!variables.items.hasOwnProperty(index)) { continue; } var item = variables.items[index]; html += item; } html += '
    '; return html; } catch (error) { console.log('theme_controlgroup - ' + error); } } /** * Themes a header widget. * @param {Object} variables * @return {String} */ function theme_header(variables) { try { variables.attributes['data-role'] = 'header'; if (typeof variables.type == 'undefined') { variables.type = 'h2'; } var typeAttrs = variables.type_attributes ? ' ' + drupalgap_attributes(variables.type_attributes) : ''; var html = '
    ' + '<' + variables.type + typeAttrs + '>' + variables.text + '
    '; return html; } catch (error) { console.log('theme_header - ' + error); } } /** * Implementation of theme_image(). * @param {Object} variables * @return {String} */ function theme_image(variables) { try { // Turn the path, alt and title into attributes if they are present. if (variables.path) { variables.attributes.src = variables.path; } if (variables.alt) { variables.attributes.alt = variables.alt; } if (variables.title) { variables.attributes.title = variables.title; } // Render the image. return ''; } catch (error) { console.log('theme_image - ' + error); } } /** * Implementation of theme_audio(). * @param {Object} variables * @return {String} */ function theme_audio(variables) { try { // Turn the path, alt and title into attributes if they are present. if (variables.path) { variables.attributes.src = variables.path; } if (variables.alt) { variables.attributes.alt = variables.alt; } if (variables.title) { variables.attributes.title = variables.title; } // Render the audio player. return ''; } catch (error) { console.log('theme_audio - ' + error); } } /** * Implementation of theme_video(). * @param {Object} variables * @return {String} */ function theme_video(variables) { try { // Turn the path, alt and title into attributes if they are present. if (variables.path) { variables.attributes.src = variables.path; } if (variables.alt) { variables.attributes.alt = variables.alt; } if (variables.title) { variables.attributes.title = variables.title; } // Add the 'webkit-playsinline' attribute on iOS devices if no one made a // decision about it being there or not. if ( typeof device !== 'undefined' && device.platform == 'iOS' && typeof variables.attributes['webkit-playsinline'] === 'undefined' ) { variables.attributes['webkit-playsinline'] = ''; } // Render the video player. return ''; } catch (error) { console.log('theme_video - ' + error); } } /** * Implementation of theme_image_style(). * @param {Object} variables * @return {String} */ function theme_image_style(variables) { try { variables.path = image_style_url(variables.style_name, variables.path); return theme_image(variables); } catch (error) { console.log('theme_image - ' + error); } } /** * Theme's an item from an MVC collection. * @param {Object} variables * @return {String} */ function theme_item(variables) { try { var html = ''; for (var field in variables.item) { if (!variables.item.hasOwnProperty(field)) { continue; } var value = variables.item[field]; html += '

    ' + variables.model.fields[field].title + '

    ' + '

    ' + value + '

    '; } return html; } catch (error) { console.log('theme_item - ' + error); } } /** * Implementation of theme_item_list(). * @param {Object} variables * @return {String} */ function theme_item_list(variables) { try { // We'll theme an empty list unordered list by default, if there is a type // of list specified we'll use that, and if there are some items we'll // theme them too. var type = 'ul'; if (variables.type) { type = variables.type; } var html = ''; if (variables.title) { html += '

    ' + variables.title + '

    '; } html += '<' + type + ' ' + drupalgap_attributes(variables.attributes) + '>'; if (variables.items && variables.items.length > 0) { var listview = typeof variables.attributes['data-role'] !== 'undefined' && variables.attributes['data-role'] == 'listview'; for (var index = 0; index < variables.items.length; index++) { var item = variables.items[index]; if (typeof item === 'string') { var icon = null; html += ''; } else if (typeof item === 'object') { var attributes = item.attributes ? item.attributes : {}; var content = item.content ? item.content : ''; html += '
  • ' + drupalgap_render(content) + '
  • '; } } } html += ''; return html; } catch (error) { console.log('theme_item_list - ' + error); } } /** * Identical to theme_item_list, except this turns the list into a jQM listview. * @param {Object} variables * @return {String} */ function theme_jqm_item_list(variables) { try { if (variables.attributes) { if ( variables.attributes['data-role'] && variables.attributes['data-role'] != 'listview' ) { } else { variables.attributes['data-role'] = 'listview'; } } else { variables.attributes['data-role'] = 'listview'; } return theme_item_list(variables); } catch (error) { console.log('theme_jqm_item_list - ' + error); } } /** * Implementation of theme_link(). * @param {Object} variables * @return {String} */ function theme_link(variables) { try { var text = ''; if (variables.text) { text = variables.text; } if (typeof variables.path !== 'undefined' && variables.path != null) { // If the path begins with a hashtag, just render the link as is with the // hashtag for the href. if (variables.path.indexOf('#') == 0) { variables.attributes['href'] = variables.path; return '' + text + ''; } // By default our onclick will use a drupalgap_goto(). If we have any // incoming link options, then modify the link accordingly. var onclick = 'drupalgap_goto(\'' + variables.path + '\');'; if (variables.options) { // Use an InAppBrowser? if (variables.options.InAppBrowser) { onclick = "window.open('" + variables.path + "', '_blank', 'location=yes');"; } else { // Prepare the path. variables.path = _drupalgap_goto_prepare_path(variables.path); // All other options need to be extracted into a JSON string for the // onclick handler. var goto_options = ''; for (var option in variables.options) { if (!variables.options.hasOwnProperty(option)) { continue; } var value = variables.options[option]; if (option == 'attributes') { continue; } if (typeof value === 'string') { value = "'" + value + "'"; } goto_options += option + ':' + value + ','; } onclick = 'drupalgap_goto(\'' + variables.path + '\', ' + '{' + goto_options + '});'; } } // Is this link active? if (variables.path == drupalgap_path_get()) { if (variables.attributes['class'].indexOf('ui-btn-active') == -1) { variables.attributes['class'] += ' ui-btn-active '; } if (variables.attributes['class'].indexOf('ui-state-persist') == -1) { variables.attributes['class'] += ' ui-state-persist '; } } // Finally, return the link. return '' + text + ''; } else { // The link has no path, so just render the text and attributes. if (typeof variables.attributes.href === 'undefined') { variables.attributes.href = '#'; } return '' + text + ''; } } catch (error) { console.log('theme_link - ' + error); } } /** * Themes the logout button. * @param {Object} variables * @return {String} */ function theme_logout(variables) { try { return bl( t('Logout'), 'user/logout', { attributes: { 'data-icon': 'action', 'data-iconpos': 'right' } } ); } catch (error) { console.log('theme_logout - ' + error); } } /** * Themes a popup widget. * @param {Object} variables * @return {String} */ function theme_popup(variables) { try { variables.attributes['data-role'] = 'popup'; var button_attributes = {}; if (variables.button_attributes) { button_attributes = variables.button_attributes; } button_attributes.href = '#' + variables.attributes.id; button_attributes['data-rel'] = 'popup'; var html = bl(variables.button_text, null, { attributes: button_attributes }) + '
    ' + variables.content + '
    '; return html; } catch (error) { console.log('theme_popup - ' + error); } } /** * Implementation of theme_submit(). * @param {Object} variables * @return {String} */ function theme_submit(variables) { try { return ''; } catch (error) { console.log('theme_submit - ' + error); } } /** * Implementation of theme_table(). * @param {Object} variables * @return {String} */ function theme_table(variables) { try { var html = ''; if (variables.header) { html += ''; for (var index in variables.header) { if (!variables.header.hasOwnProperty(index)) { continue; } var column = variables.header[index]; if (column.data) { html += ''; } } html += ''; } html += ''; if (variables.rows) { for (var row_index in variables.rows) { if (!variables.rows.hasOwnProperty(row_index)) { continue; } var row = variables.rows[row_index]; html += ''; if (row) { for (var column_index in row) { if (!row.hasOwnProperty(column_index)) { continue; } var column = row[column_index]; html += ''; } } html += ''; } } return html + '
    ' + column.data + '
    ' + column + '
    '; } catch (error) { console.log('theme_table - ' + error); } } /** * Theme a jQueryMobile table. * @param {Object} variables * @return {String} */ function theme_jqm_table(variables) { try { variables.attributes['data-role'] = 'table'; variables.attributes['data-mode'] = 'reflow'; return theme_table(variables); } catch (error) { console.log('theme_jqm_table - ' + error); } } /** * An interal callback used to handle the setting of the page title during the * pageshow event. * @param {*} page_arguments */ function _drupalgap_page_title_pageshow(page_arguments) { try { var router_path = drupalgap_router_path_get(); // Set the page title. First we'll see if the hook_menu() item has a // title variable set, then check for a title_callback function. var title_arguments = []; if (typeof drupalgap.menu_links[router_path].title !== 'undefined') { drupalgap_set_title(drupalgap.menu_links[router_path].title); } if ( typeof drupalgap.menu_links[router_path].title_callback !== 'undefined' ) { var function_name = drupalgap.menu_links[router_path].title_callback; if (function_exists(function_name)) { // Grab the title callback function. var fn = window[function_name]; // Place the internal success callback handler on the front of the // title arguments. title_arguments.unshift(_drupalgap_page_title_pageshow_success); // Are there any additional arguments to send to the title callback? if (drupalgap.menu_links[router_path].title_arguments) { // For each title argument, if the argument is an integer, grab the // corresponding arg(#), otherwise just push the arg onto the title // arguments. var args = arg(null, drupalgap_path_get()); var _title_arguments = drupalgap.menu_links[router_path].title_arguments; for (var index in _title_arguments) { if (!_title_arguments.hasOwnProperty(index)) { continue; } var object = _title_arguments[index]; if (is_int(object) && args[object]) { title_arguments.push(args[object]); } else { title_arguments.push(object); } } } // Call the title callback function with the title arguments. drupalgap_set_title( fn.apply( null, Array.prototype.slice.call(title_arguments) ) ); } } else { _drupalgap_page_title_pageshow_success(drupalgap_get_title()); } } catch (error) { console.log('_drupalgap_page_title_pageshow - ' + error); } } /** * An internal function used to set the page title when the page title callback * function is successful. * @param {String} title */ function _drupalgap_page_title_pageshow_success(title) { try { var id = system_title_block_id(drupalgap_path_get()); $('h1#' + id).html(title); } catch (error) { console.log('_drupalgap_page_title_pageshow_success - ' + error); } } function theme_jqm_grid(variables) { if (!variables.items || !variables.items.length) { return ''; } var html = ''; // Determine columns, and jqm grid type and css class.. var columns = jqm_grid_verify_columns(variables); var grid = jqm_grid_get_type(columns); variables.attributes.class += ' ui-grid-' + grid + ' '; var open = '
    '; var close = '
    '; var open_row = '
    '; // This class will be replaced dynamically. var close_row = '
    '; html += open; for (var i = 0; i < variables.items.length; i++) { var className = jqm_grid_get_item_class(i, columns); var openRow = jqm_grid_set_item_row_class(open_row, className); html += openRow + variables.items[i] + close_row; } html += close; return html; } function jqm_grid_verify_columns(variables) { var columns = variables.columns ? variables.columns : 2; var msg = null; if (columns < 2) { msg = columns + ' columns is not enough, a minimum of 2 is needed'; columns = 2; } if (columns > 5) { msg = columns + ' columns is too many enough, a maximum of 5 is allowed'; columns = 5; } variables.columns = columns; if (msg) { console.log('jqm_grid_verify_columns - ' + msg); } return variables.columns; } function jqm_grid_get_type(columns) { var grid = null; switch (columns) { case 2: grid = 'a'; break; case 3: grid = 'b'; break; case 4: grid = 'c'; break; case 5: grid = 'd'; break; } return grid; } function jqm_grid_get_item_class(index, columns) { var className = null; switch (index % columns) { case 0: className = 'ui-block-a'; break; case 1: className = 'ui-block-b'; break; case 2: className = 'ui-block-c'; break; case 3: className = 'ui-block-d'; break; case 4: className = 'ui-block-e'; break; } return className } function jqm_grid_set_item_row_class(open_row, className) { var openRow = JSON.parse(JSON.stringify(open_row)); // Make a copy of the string. if (className) { openRow = openRow.replace('ui-block', className); } return openRow; } /** * Given a form element, this will return true if access to the element is * permitted, false otherwise. * @param {Object} element * @return {Boolean} */ function drupalgap_form_element_access(element) { try { var access = true; if (element.access == false) { access = false; } return access; } catch (error) { console.log('drupalgap_form_element_access - ' + error); } } /** * Given a form element type, this will return the name of the module that * implements the hook_field_widget_form() for the element. Keep in mind for now * some of the module names don't exist, and are actually implemented inside * the field module. If no module is found, it returns false. * @param {String} type * @return {String} */ function drupalgap_form_element_get_module_name(type) { try { var module = false; switch (type) { case 'checkbox': case 'radios': case 'select': module = 'options'; break; case 'image': module = 'image'; break; } return module; } catch (error) { console.log('drupalgap_form_element_get_module_name - ' + error); } } /** * Given a form element name and the form_id, this generates an html id * attribute value to be used in the DOM. An optional third argument is a * string language code to use. An optional fourth argument is an integer delta * value to use on field elements. * @param {String} name * @param {String} form_id * @return {String} */ function drupalgap_form_get_element_id(name, form_id) { try { if (name == null || name == '') { return ''; } var id = 'edit-' + form_id.toLowerCase().replace(/_/g, '-') + '-' + name.toLowerCase().replace(/_/g, '-'); // Any language code to append to the id? if (arguments[2]) { id += '-' + arguments[2]; } // Any delta value to append to the id? if (typeof arguments[3] !== 'undefined') { id += '-' + arguments[3] + '-value'; } return id; } catch (error) { console.log('drupalgap_form_get_element_id - ' + error); } } /** * Given an element name, this will return the class name to use on the * element's container. * @param {String} name * @return {String} */ function drupalgap_form_get_element_container_class(name) { try { return 'form-item field-name-' + name.replace(/_/g, '-'); } catch (error) { console.log('drupalgap_form_get_element_container_class - ' + error); } } /** * Renders all the input elements in a form. * @param {Object} form * @return {String} */ function _drupalgap_form_render_elements(form) { try { var content = ''; var content_sorted = ''; var content_weighted = []; // For each form element, if the element objects name property isn't set, // set it, then render the element if access is permitted. While rendering // the elements, set them aside according to their widget weight so they // can be appended to the content string in the correct order later. for (var name in form.elements) { if (!form.elements.hasOwnProperty(name)) { continue; } var element = form.elements[name]; if (!element.name) { element.name = name; } if (drupalgap_form_element_access(element)) { if ( element.is_field && typeof element.field_info_instance.widget.weight !== 'undefined' ) { var weight = element.field_info_instance.widget.weight; while (typeof content_weighted[weight] !== 'undefined') { weight += .1; } content_weighted['' + weight] = _drupalgap_form_render_element(form, element); } else { // Extract the bundle. Note, on comments the bundle is prefixed with // 'comment_node_' so we need to remove that to correctly map to the // potential extra fields data. var bundle = null; if (form.bundle) { bundle = form.bundle; if ( form.entity_type == 'comment' && form.bundle.indexOf('comment_node_') != -1 ) { bundle = form.bundle.replace('comment_node_', ''); } } // This is not a field, if it has it's own weight use it, or see if // there is a weight in field_info_extra_fields, otherwise just // append it to the element content. // Elements with a weight defined. if (typeof element.weight !== 'undefined') { if (content_weighted[element.weight]) { var msg = 'WARNING: _drupalgap_form_render_elements - the ' + 'weight of ' + element.weight + ' for ' + element.name + ' is already in use by ' + content_weighted[element.weight].name; console.log(msg); // Just render it. var _content = _drupalgap_form_render_element(form, element); if (typeof _content !== 'undefined') { content += _content; } } else { content_weighted[element.weight] = _drupalgap_form_render_element(form, element); } } // Extra fields. else if ( form.entity_type && bundle && typeof drupalgap.field_info_extra_fields[bundle][name] !== 'undefined' && typeof drupalgap.field_info_extra_fields[bundle][name].weight !== 'undefined' ) { var weight = drupalgap.field_info_extra_fields[bundle][name].weight; if (content_weighted[weight]) { var msg = 'WARNING: _drupalgap_form_render_elements - the ' + 'weight of ' + weight + ' for ' + element.name + ' is ' + 'already in use by ' + content_weighted[weight].name; console.log(msg); // Just render it. var _content = _drupalgap_form_render_element(form, element); if (typeof _content !== 'undefined') { content += _content; } } else { content_weighted[weight] = _drupalgap_form_render_element(form, element); } } // No weight, just render it. else { var _content = _drupalgap_form_render_element(form, element); if (typeof _content !== 'undefined') { content += _content; } } } } } // Prepend the weighted elements to the content. if (!empty(content_weighted)) { for (var weight in content_weighted) { content_sorted += content_weighted[weight] + '\n'; } // Attach sorted content. content = content_sorted + '\n' + content; } // Add any form buttons to the form elements html, if access to the button // is permitted. if (form.buttons && form.buttons.length != 0) { for (var name in form.buttons) { if (!form.buttons.hasOwnProperty(name)) { continue; } var button = form.buttons[name]; if (drupalgap_form_element_access(button)) { var attributes = { type: 'button', id: drupalgap_form_get_element_id(name, form.id) }; if (button.attributes) { $.extend(attributes, button.attributes); } content += ''; } } } return content; } catch (error) { console.log('_drupalgap_form_render_elements - ' + error); } } /** * Renders an input element for a form. * @param {Object} form * @param {Object} element * @return {String} */ function _drupalgap_form_render_element(form, element) { try { var html = ''; if (!element) { return html; } // Extract the element name. var name = element.name; // Grab the language. var language = language_default(); // We'll assume the element has no items (e.g. title, nid, vid, etc), unless // we determine later that this element is a field, then it'll have items. var items = false; // If this element is a field, extract the items from the language code and // determine what module and hook will handle the items. If the element is // not a field, just flatten it into a single item collection and determine // which module handles this element type. Keep in mind not all the modules // actually exist, and we've placed implementations into the field module. var module = false; var field_widget_form_function_name = false; var field_widget_form_function = false; if (element.is_field) { items = element[language]; module = element.field_info_instance.widget.module; } else { items = {0: element}; module = drupalgap_form_element_get_module_name(element.type); } if (module) { field_widget_form_function_name = module + '_field_widget_form'; if (function_exists(field_widget_form_function_name)) { field_widget_form_function = window[field_widget_form_function_name]; } else { console.log( 'WARNING: _drupalgap_form_render_element() - ' + field_widget_form_function_name + '() does not exist for the "' + element.type + '" form element!' ); } } // If there were no items, just return. if (!items || items.length == 0) { return html; } // Generate default variables. var variables = { attributes: {} }; // Grab the info instance and info field for the field, then attach them // both to the variables object so all theme functions will have access // to that data. variables.field_info_field = element.field_info_field; variables.field_info_instance = element.field_info_instance; // Render the element item(s). Remember the final delta value for later. var delta = 0; var item_html = ''; var item_label = ''; var render_item = null; for (var delta in items) { if (!items.hasOwnProperty(delta)) { continue; } var item = items[delta]; // We'll render the item, unless we prove otherwise. render_item = true; // Overwrite the variable's attributes id with the item's id. variables.attributes.id = item.id; // Attach the item as the element onto variables. variables.element = item; // Create an array for the item's children if it doesn't exist already. // This is used by field widget forms to extend form elements. if (!items[delta].children) { items[delta].children = []; } // Generate the label for field items on delta zero only. Keep in mind // rendered labels, with an element title_placeholder set to true, // will not be appended to the result html later. if (element.is_field && delta == 0) { item.title = element.title; item_label = theme('form_element_label', {'element': item}); } // If the element's title is set to be a placeholder, set the // placeholder attribute equal to the title on the current item, unless // someone already set it. If it is a required element, mark it as such. if ( delta == 0 && typeof element.title_placeholder !== 'undefined' && element.title_placeholder && typeof variables.attributes['placeholder'] === 'undefined' ) { var placeholder = element.title; // @TODO show a better required marker for placeholders. /*if (element.required) { placeholder += ' ' + theme('form_required_marker', { }); }*/ variables.attributes['placeholder'] = placeholder; } // If there wasn't a default value provided, set one. Then set the default value into the variables' attributes, // if it wasn't already set, otherwise set it to the item's value. if (!item.default_value) { item.default_value = ''; } variables.attributes.value = item.default_value; if ( typeof item.value !== 'undefined' && (typeof variables.attributes.value === 'undefined' || empty(variables.attributes.value)) ) { variables.attributes.value = item.value; } // Call the hook_field_widget_form() if necessary. Merge any changes // to the item back into this item. if (field_widget_form_function) { field_widget_form_function.apply( null, [ form, null, element.field_info_field, element.field_info_instance, language, items, delta, element ]); item = $.extend(true, item, items[delta]); // If the item type got lost, replace it. if (!item.type && element.type) { item.type = element.type; } } // Merge element attributes into the variables object. if (item.options && item.options.attributes) { variables.attributes = $.extend( true, variables.attributes, item.options.attributes ); } // Render the element item, unless it wasn't supported. Before rendering, clear out any default values so they // aren't stale for the next delta item. item_html += _drupalgap_form_render_element_item( form, element, variables, item ); if (typeof variables.default_value !== 'undefined') { delete variables.default_value; } if (typeof variables.default_value_label !== 'undefined') { delete variables.default_value_label; } if (typeof variables.value !== 'undefined') { delete variables.value; } if (typeof item_html === 'undefined') { render_item = false; break; } } // Are we skipping the render of the item? if (!render_item) { return ''; } // Show the 'Add another item' button on unlimited value fields. /*if (element.field_info_field && element.field_info_field.cardinality == -1) { var add_another_item_variables = { text: 'Add another item', attributes: { 'class': 'drupalgap_form_add_another_item', onclick: "javascript:_drupalgap_form_add_another_item('" + form.id + "', '" + element.name + "', " + delta + ')' } }; html += theme('button', add_another_item_variables); }*/ // Is this element wrapped? We won't wrap hidden inputs by default, unless // someone is overriding it. var wrapped = true; if (typeof element.wrapped !== 'undefined' && !element.wrapped) { wrapped = false; } if (element.type == 'hidden') { wrapped = false; if (element.wrapped) { wrapped = true; } } // If there is an element prefix, place it in the html. if (element.prefix) { html += element.prefix; } // Open the element container. var container_attributes = { 'class': drupalgap_form_get_element_container_class(name) }; if (wrapped) { html += '
    '; } // Add a label to the element, except submit and hidden elements. Any field // labels have already been rendered, other element labels must be manually // rendered here. Don't attach the label if the element's title_placeholder // is set to true. if (element.type != 'submit' && element.type != 'hidden') { if ( typeof element.title_placeholder !== 'undefined' && element.title_placeholder ) { /* Skip label for placeholders. */ } else { if (element.is_field) { html += item_label; } else { html += theme('form_element_label', {'element': element}); } } } // Add the item html if it isn't empty. Place the description "before" or "after" the item, exclude it if set // to "none". if (item_html != '') { if (element.description && element.type != 'hidden') { var descriptionDisplay = element.description_display ? element.description_display : 'after'; var descriptionHtml = '
    ' + t(element.description) + '
    '; switch (descriptionDisplay) { case 'before': html += descriptionHtml + item_html; break; case 'after': html += item_html + descriptionHtml; break; case 'none': html += item_html; break; } } else { html += item_html; } } // Close the element container. if (wrapped) { html += '
    '; } // If there is an element suffix, place it in the html. if (element.suffix) { html += element.suffix; } // Return the element html. return html; } catch (error) { console.log('_drupalgap_form_render_element - ' + error); } } /** * Given a form, an element, the variables for a theme function, and the element * item, this will return the html rendering of the element item. * @param {Object} form * @param {Object} element * @param {Object} variables * @param {Object} item * @return {*} */ function _drupalgap_form_render_element_item(form, element, variables, item) { try { var html = ''; // Depending on the element type, if necessary, adjust the variables and/or // theme function to be used, then render the element by calling its theme // function. // @TODO - this block of code should be moved into their respective // implementations of hook_field_widget_form(). switch (item.type) { case 'text': item.type = 'textfield'; break; case 'list_text': case 'list_float': case 'list_integer': item.type = 'select'; break; } // Set the theme function. var theme_function = item.type; // If the element is disabled, add the 'disabled' attribute. if (element.disabled) { variables.attributes.disabled = ''; } // Make any preprocess modifications to the elements so they will map // cleanly to their theme function. // @todo A hook_field_widget_form() should be used instead here. if (item.type == 'submit') { variables.attributes.onclick = '_drupalgap_form_submit(\'' + form.id + '\');'; if (!variables.attributes['data-theme']) { variables.attributes['data-theme'] = 'b'; } if (typeof variables.attributes.type === 'undefined') { variables.attributes.type = 'button'; } if (typeof variables.attributes['class'] === 'undefined') { variables.attributes['class'] = ''; } variables.attributes['class'] += ' dg_form_submit_button '; } // Merge the item into variables. $.extend(true, variables, item); // If a value isn't set on variables, try to set it with the default value // on the item. if (typeof variables.value === 'undefined' || variables.value == null) { if (typeof item.default_value !== 'undefined') { variables.value = item.default_value; } } // Run the item through the theme system if a theme function exists, or try // to use the item markup, or let the user know the field isn't supported. if (function_exists('theme_' + theme_function)) { html += theme(theme_function, variables); } else { if (item.markup || item.markup == '') { html += item.markup; } else { // @todo - the reason for this warning sometimes happens because the // item.type is lost with $.extend in _drupalgap_form_render_element(). // @update - if an item doesn't have a type, it gets set by the parent // element, so we should now always have a type available here. var msg = 'Field ' + item.type + ' not supported.'; console.log('WARNING: _drupalgap_form_render_element_item() - ' + msg); dpm(item); return null; } } // Render any item children. If the child has markup, just use the html, // otherwise run the child through theme(). if (item.children && item.children.length > 0) { for (var i = 0; i < item.children.length; i++) { if (item.children[i].markup) { html += item.children[i].markup; } else if (item.children[i].type || item.children[i].theme) { var theme_type = item.children[i].type; if (!theme_type) { theme_type = item.children[i].theme; } // Is there a title for a label? if (item.children[i].title) { html += theme('form_element_label', { element: item.children[i] }); } // Render the child with the theme system. if (item.children[i].prefix) { html += item.children[i].prefix; } html += theme(theme_type, item.children[i]); if (item.children[i].suffix) { html += item.children[i].suffix; } } else { console.log( 'WARNING: _drupalgap_form_render_element_item - ' + 'failed to render child ' + i + ' for ' + element.name ); } } } return html; } catch (error) { console.log('_drupalgap_form_render_element_item - ' + error); } } /** * Given an element name, the form, a language code and a delta value, this * will return default values that can be used to place an item element into a * Forms API object. * @param {String} name * @param {Object} form * @param {String} language * @param {Number} delta * @return {Object} */ function drupalgap_form_element_item_create(name, form, language, delta) { try { // Generate the id for this element field item and set it and // some default options onto the element item. var id = drupalgap_form_get_element_id(name, form.id, language, delta); return { id: id, options: { attributes: { id: id } }, required: form.elements[name].required }; } catch (error) { console.log('drupalgap_form_element_item_create - ' + error); } } /** * * @param {Object} form * @param {Object} form_state * @param {Object} element * @param {String} language * @param {Number} delta * @return {Array} */ function _drupalgap_form_element_items_widget_arguments(form, form_state, element, language, delta) { try { var widget_arguments = []; widget_arguments.push(form); // form widget_arguments.push(form_state); // form state widget_arguments.push(element.field_info_field); // field widget_arguments.push(element.field_info_instance); // instance widget_arguments.push(language); // language widget_arguments.push(form.elements[element.name][language]); // items widget_arguments.push(delta); // delta widget_arguments.push(element); // element return widget_arguments; } catch (error) { console.log('_drupalgap_form_element_items_widget_arguments - ' + error); } } /** * Internal function used to dynamically add another element item to a form for * unlimited value fields. * @param {String} form_id * @param {String} name * @param {Number} delta */ function _drupalgap_form_add_another_item(form_id, name, delta) { try { // Locate the last item, load the form, extract the element from // the form, generate default variables for the new item, determine the next // delta value. var selector = '.' + drupalgap_form_get_element_container_class(name) + ' .drupalgap_form_add_another_item'; var add_another_item_button = $(selector); var form = drupalgap_form_local_storage_load(form_id); var language = language_default(); var item = drupalgap_form_element_item_create( name, form, language, delta + 1 ); form.elements[name][language][delta + 1] = item; var element = form.elements[name]; var variables = { attributes: {}, field_info_field: element.field_info_field, field_info_instance: element.field_info_instance }; var field_widget_form_function = element.field_info_instance.widget.module + '_field_widget_form'; window[field_widget_form_function].apply( null, _drupalgap_form_element_items_widget_arguments( form, null, element, language, delta + 1 ) ); drupalgap_form_local_storage_save(form); $(add_another_item_button).before( _drupalgap_form_render_element_item(form, element, variables, item) ); } catch (error) { console.log('_drupalgap_form_add_another_item - ' + error); } } /** * Returns a 'Cancel' button object that can be used on most forms. * @return {Object} */ function drupalgap_form_cancel_button() { return { 'title': t('Cancel'), attributes: { onclick: 'javascript:drupalgap_back();' } }; } /** * Given a jQuery selector to a form, this will clear all the elements on * the UI. * @see http://stackoverflow.com/a/6364313/763010 */ function drupalgap_form_clear(form_selector) { try { $(':input', form_selector) .not(':button, :submit, :reset, :hidden') .val('') .removeAttr('checked') .removeAttr('selected'); } catch (error) { console.log('drupalgap_form_clear - ' + error); } } /** * Given a form id, this will assemble and return the default form JSON object. * @param {String} form_id * @return {Object} */ function drupalgap_form_defaults(form_id) { try { var form = {}; // Set the form id, elements, buttons, options and attributes. form.id = form_id; form.elements = {}; form.buttons = {}; form.options = { attributes: { 'class': '' } }; // Create a prefix and suffix. form.prefix = ''; form.suffix = ''; // Create empty arrays for the form's validation and submission handlers, // then add the default call back functions to their respective array, if // they exist. form.validate = []; form.submit = []; var validate_function_name = form.id + '_validate'; if (function_exists(validate_function_name)) { form.validate.push(validate_function_name); } var submit_function_name = form.id + '_submit'; if (function_exists(submit_function_name)) { form.submit.push(submit_function_name); } // Finally, return the form. return form; } catch (error) { console.log('drupalgap_form_defaults - ' + error); } } /** * Given a drupalgap form, this renders the form html and returns it. * @param {Object} form * @return {String} */ function drupalgap_form_render(form) { try { // @TODO - we may possibly colliding html element ids!!! For example, I // think the node edit page gets an id of "node_edit" and possibly so does // the node edit form, which also may get an id of "node_edit". We may want // to prefix both the template page and form ids with prefixes, e.g. // drupalgap_page_* and drupalgap_form_*, but adding these prefixes could // get annoying for css selectors used in jQuery and CSS. What to do? // If no form id is provided, warn the user. if (!form.id) { return '

    drupalgap_form_render() - missing form id!

    ' + JSON.stringify(form); } // If the form already exists in the DOM, remove it. if ($('form#' + form.id).length) { $('form#' + form.id).remove(); } // Render the prefix and suffix and wrap them in their own div. var prefix = form.prefix; if (!empty(prefix)) { prefix = '
    ' + prefix + '
    '; } var suffix = form.suffix; if (!empty(suffix)) { suffix = '
    ' + suffix + '
    '; } // Render the form's input elements. var form_elements = _drupalgap_form_render_elements(form); var form_attributes = drupalgap_attributes(form.options.attributes); // Return the form html. var form_html = '
    ' + prefix + '
    ' + form_elements + suffix + '
    '; return form_html; } catch (error) { console.log('drupalgap_form_render - ' + error); } } /** * Given a form element name and an error message, this attaches the error * message to the drupalgap.form_errors array, keyed by the form element name. * @param {String} name * @param {String} message */ function drupalgap_form_set_error(name, message) { try { drupalgap.form_errors[name] = message; } catch (error) { console.log('drupalgap_form_set_error - ' + error); } } /** * Given a form id, this will render the form and return the html for the form. * Any additional arguments will be sent along to the form. * @param {String} form_id * @return {String} */ function drupalgap_get_form(form_id) { try { var html = ''; var form = drupalgap_form_load.apply( null, Array.prototype.slice.call(arguments) ); if (form) { // Render the form. html = drupalgap_form_render(form); } else { var msg = 'drupalgap_get_form - ' + t('failed to get form') + ' (' + form_id + ')'; drupalgap_alert(msg); } return html; } catch (error) { console.log('drupalgap_get_form - ' + error); } } /** * Given a form id, this will delete the form from local storage. * If the form isn't in local storage, this returns false. * @param {String} form_id * @return {Object} */ function drupalgap_form_local_storage_delete(form_id) { try { var result = window.localStorage.removeItem( drupalgap_form_id_local_storage_key(form_id) ); return result; } catch (error) { console.log('drupalgap_form_local_storage_delete - ' + error); } } /** * Given a form id, this will load the form from local storage and return it. * If the form isn't in local storage, this returns false. * @param {String} form_id * @return {Object} */ function drupalgap_form_local_storage_load(form_id) { try { var form = false; form = window.localStorage.getItem( drupalgap_form_id_local_storage_key(form_id) ); if (!form) { form = false; } else { form = JSON.parse(form); } return form; } catch (error) { console.log('drupalgap_form_local_storage_load - ' + error); } } /** * Given a form, this will save the form to local storage, overwriting any * previously saved forms. * @param {Object} form */ function drupalgap_form_local_storage_save(form) { try { window.localStorage.setItem( drupalgap_form_id_local_storage_key(form.id), JSON.stringify(form) ); } catch (error) { console.log('drupalgap_form_local_storage_save - ' + error); } } /** * Given a form id, this will return the local storage key used by DrupalGap * to save the assembled form to the device's local storage. * @param {String} form_id * @return {String} */ function drupalgap_form_id_local_storage_key(form_id) { return 'drupalgap_form_' + form_id; } /** * Given a form id, this will return the form JSON object assembled by the * form's call back function. If the form fails to load, this returns false. * @param {String} form_id * @return {Object} */ function drupalgap_form_load(form_id) { try { var form = drupalgap_form_defaults(form_id); // The form's call back function will be equal to the form id. var function_name = form_id; if (function_exists(function_name)) { // Grab the form's function. var fn = window[function_name]; // Determine the language code. var language = language_default(); // Build the form arguments by iterating over each argument then adding // each to to the form arguments, afterwards remove the argument at index // zero because that is the form id. var form_arguments = []; for (var index in arguments) { if (!arguments.hasOwnProperty(index)) { continue; } var argument = arguments[index]; form_arguments.push(argument); } form_arguments.splice(0, 1); // Attach the form arguments to the form object. form.arguments = form_arguments; // If there were no arguments to pass along, call the function directly to // retrieve the form, otherwise call the function and pass along any // arguments to retrieve the form. if (form_arguments.length == 0) { form = fn(form, null); } else { // We must consolidate the form, form_state and arguments into one array // and then pass it along to the form builder. var consolidated_arguments = []; var form_state = null; consolidated_arguments.push(form); consolidated_arguments.push(form_state); for (var index in form_arguments) { if (!form_arguments.hasOwnProperty(index)) { continue; } var argument = form_arguments[index]; consolidated_arguments.push(argument); } form = fn.apply( null, Array.prototype.slice.call(consolidated_arguments) ); } // Set default values across elements in preparation for alteration. _drupalgap_form_load_set_element_defaults(form, language); // Give modules an opportunity to alter the form. module_invoke_all('form_alter', form, null, form_id); // Set default values across elements again in case any elements were added in preparation for alteration. _drupalgap_form_load_set_element_defaults(form, language); // Place the assembled form into local storage so _drupalgap_form_submit // will have access to the assembled form. drupalgap_form_local_storage_save(form); Drupal.cache_expiration.forms[form_id] = 1; window.localStorage.setItem('cache_expiration', JSON.stringify(Drupal.cache_expiration)); } else { var error_msg = 'drupalgap_form_load - ' + t('no callback function') + ' (' + function_name + ') ' + t('available for form') + ' (' + form_id + ')'; drupalgap_alert(error_msg); } return form; } catch (error) { console.log('drupalgap_form_load - ' + error); } } /** * An internal function used to set default values onto all form elements' properties, such as an id and name. * @param form * @param language * @private */ function _drupalgap_form_load_set_element_defaults(form, language) { try { // Set empty options and attributes properties on each form element if the // element does not yet have any. This allows others to more easily modify // options and attributes on an element without having to worry about // testing for nulls and creating empty properties first. for (var name in form.elements) { if (!form.elements.hasOwnProperty(name)) { continue; } var element = form.elements[name]; //console.log(name); //console.log(element); // If we haven't already, check to see f this element is a field, load its field_info_field and // field_info_instance onto the element. if (typeof element.is_field === 'undefined') { var element_is_field = false; var field_info_field = drupalgap_field_info_field(name); if (field_info_field) { element_is_field = true; form.elements[name].field_info_field = field_info_field; form.elements[name].field_info_instance = drupalgap_field_info_instance( form.entity_type, name, form.bundle ); } form.elements[name].is_field = element_is_field; } // Set the name property on the element if it isn't already set. if (!form.elements[name].name) { form.elements[name].name = name; } // If the element is a field, we'll append a language code and delta // value to the element id, along with the field items appended // onto the element using the language code and delta values. var id = null; if (element_is_field) { // What's the number of allowed values (cardinality) on this field? // A cardinality of -1 means the field has unlimited values. var cardinality = parseInt(element.field_info_field.cardinality); if (cardinality == -1) { cardinality = 1; // we'll just add one element for now, until we // figure out how to handle the 'add another // item' feature. } // Initialize the item collections language code if it hasn't been. if (!form.elements[name][language]) { form.elements[name][language] = {}; } // Prepare the item(s) for this element. for (var delta = 0; delta < cardinality; delta++) { // Prepare some item defaults. var item = drupalgap_form_element_item_create( name, form, language, delta ); // If the delta for this item hasn't been created on the element, // create it using the default item values. Otherwise, merge the // default values into the pre existing item on the element. if (!form.elements[name][language][delta]) { form.elements[name][language][delta] = item; } else { $.extend(true, form.elements[name][language][delta], item); } } } else { if (element.name == 'user_roles') { console.log('defaults', element); } // This element is not a field, setup default options if none // have been provided. Then set the element id. if (!element.options) { form.elements[name].options = {attributes: {}}; } else if (!element.options.attributes) { form.elements[name].options.attributes = {}; } id = drupalgap_form_get_element_id(name, form.id); form.elements[name].id = id; form.elements[name].options.attributes.id = id; } } } catch (error) { console.log('_drupalgap_form_load_set_element_defaults - ' + error); } } /** * Optionally use this function as an HTML DOM onkeypress handler, and it will * attempt to listen for the enter key being pressed and submit the form at that * time. * @param {String} form_id * @param {Object} event * @return {Boolean} */ function drupalgap_form_onkeypress(form_id, event) { try { var event = event ? event : window.event; if (!event) { return; } var charCode = event.which || event.keyCode; if (charCode != '13') { return; } $('#' + form_id + ' button.dg_form_submit_button').click(); event.preventDefault(); return false; } catch (error) { console.log('drupalgap_form_onkeypress - ' + error); } } /** * Handles a drupalgap form's submit button click. * @param {String} form_id * @return {*} */ function _drupalgap_form_submit(form_id) { try { // Load the form from local storage. var form = drupalgap_form_local_storage_load(form_id); if (!form) { var msg = '_drupalgap_form_submit - ' + t('failed to load form') + ': ' + form_id; drupalgap_alert(msg); return false; } // Assemble the form state values. var form_state = drupalgap_form_state_values_assemble(form); // Clear out previous form errors. drupalgap.form_errors = {}; // Build the form validation wrapper function. var form_validation = function() { try { // Call the form's validate function(s), if any. for (var index in form.validate) { if (!form.validate.hasOwnProperty(index)) { continue; } var function_name = form.validate[index]; var fn = window[function_name]; fn.apply(null, Array.prototype.slice.call([form, form_state])); } // Call drupalgap form's api validate. _drupalgap_form_validate(form, form_state); // If there were validation errors, show the form errors and stop the // form submission. Otherwise submit the form. if (!jQuery.isEmptyObject(drupalgap.form_errors)) { var html = ''; for (var name in drupalgap.form_errors) { if (!drupalgap.form_errors.hasOwnProperty(name)) { continue; } var message = drupalgap.form_errors[name]; html += message + '\n\n'; } drupalgap_alert(html); } else { form_submission(); } } catch (error) { console.log('_drupalgap_form_submit - form_validation - ' + error); } }; // Build the form submission wrapper function. var form_submission = function() { try { // Call the form's submit function(s), if any. for (var index in form.submit) { if (!form.submit.hasOwnProperty(index)) { continue; } var function_name = form.submit[index]; var fn = window[function_name]; fn.apply(null, Array.prototype.slice.call([form, form_state])); } // Remove the form from local storage. // @todo - we can't do this here because often times a form's submit // handler makes asynchronous calls (i.e. user login) and although the // form validated, server side may say the input was invalid, so the // user will still be on the form, except we already removed the form. //drupalgap_form_local_storage_delete(form_id); } catch (error) { console.log('_drupalgap_form_submit - form_submission - ' + error); } }; // Get ready to validate and submit the form, but first... // If this is an entity form, and there is an image field on the form, we // need to asynchronously process the image field, then continue onward // with normal form validation and submission. if (form.entity_type && image_fields_present_on_entity_type(form.entity_type, form.bundle) ) { _image_field_form_process(form, form_state, { success: form_validation }); } else { // There were no image fields on the form, proceed normally with form // validation, which will in turn process the submission if there are no // validation errors. form_validation(); } } catch (error) { console.log('_drupalgap_form_submit - ' + error); } } /** * An internal function used by the DrupalGap forms api to validate all the * elements on a form. * @param {Object} form * @param {Object} form_state */ function _drupalgap_form_validate(form, form_state) { try { for (var name in form.elements) { if (!form.elements.hasOwnProperty(name)) { continue; } var element = form.elements[name]; if (name == 'submit') { continue; } if (element.required) { var valid = true; var value = null; if (element.is_field) { value = form_state.values[name][language_default()][0]; } else { value = form_state.values[name]; } // Check for empty values. if (empty(value)) { valid = false; } // Validate a required select list. else if ( element.type == 'select' && element.required && value == '' ) { valid = false; } else if (element.type == 'checkboxes' && element.required) { var has_value = false; for (var key in form_state.values[name]) { if (!form_state.values[name].hasOwnProperty(key)) { continue; } var value = form_state.values[name][key]; if (value) { has_value = true; break; } } if (!has_value) { valid = false; } } if (!valid) { var field_title = name; if (element.title) { field_title = element.title; } drupalgap_form_set_error( name, t('The') + ' ' + field_title + ' ' + t('field is required') + '.' ); } } } } catch (error) { console.log('_drupalgap_form_validate - ' + error); } } /** * Given a form, this function iterates over the form's elements and assembles * each element and value and places them into the form state's values. This * is similar to $form_state['values'] in Drupal. * @param {Object} form * @return {Object} */ function drupalgap_form_state_values_assemble(form) { try { var lng = language_default(); var form_state = { values: {} }; for (var name in form.elements) { if (!form.elements.hasOwnProperty(name)) { continue; } var element = form.elements[name]; if (name == 'submit') { continue; } // Always skip the form 'submit'. var id = null; if (element.is_field) { form_state.values[name] = {}; form_state.values[name][lng] = {}; var allowed_values = element.field_info_field.cardinality; if (allowed_values == -1) { allowed_values = 1; // Convert unlimited value field to one for now... } for (var delta = 0; delta < allowed_values; delta++) { id = drupalgap_form_get_element_id(name, form.id, lng, delta); form_state.values[name][lng][delta] = _drupalgap_form_state_values_assemble_get_element_value( id, element ); } } else { id = drupalgap_form_get_element_id(name, form.id); form_state.values[name] = _drupalgap_form_state_values_assemble_get_element_value( id, element ); } } // Attach the form state to drupalgap.form_states keyed by the form id. drupalgap.form_states[form.id] = form_state; return form_state; } catch (error) { console.log('drupalgap_form_state_values_assemble - ' + error); } } /** * * @param {String} id * @param {Object} element * @return {String|Number} */ function _drupalgap_form_state_values_assemble_get_element_value(id, element) { try { // If a value_callback is specified on the element, call that function to // retrieve the element's value. Ohterwise, we'll use the default techniques // implemented to extract a value for the form state. if (element.value_callback && function_exists(element.value_callback)) { var fn = window[element.value_callback]; return fn(id, element); } // Figure out the value, depending on the element type. var value = null; var selector = ''; if (element.type == 'radios') { selector = 'input:radio[name="' + id + '"]:checked'; } else { selector = '#' + id; } switch (element.type) { case 'checkbox': var _checkbox = $(selector); if ($(_checkbox).is(':checked')) { value = 1; } else { value = 0; } break; case 'checkboxes': // Iterate over each option, and see if it is checked, then pop it onto // the value array. It's possible for values to be updated dynamically // by a developer (i.e. through a pageshow handler), so it isn't safe to // look at the options in local storage. Instead we'll directly iterate // over the element option(s) in the DOM. value = {}; var options = $('label[for="' + id + '"]').siblings('.ui-checkbox'); $.each(options, function(index, option) { var checkbox = $(option).children('input'); var _value = $(checkbox).attr('value'); if ($(checkbox).is(':checked')) { value[_value] = _value; } else { value[_value] = 0; } }); break; case 'list_boolean': var _checkbox = $(selector); if ($(_checkbox).is(':checked')) { value = $(_checkbox).attr('on'); } else { value = $(_checkbox).attr('off'); } break; case 'list_text': // Radio buttons. if ( element.field_info_instance && element.field_info_instance.widget.type == 'options_buttons' ) { selector = 'input:radio[name="' + id + '"]:checked'; } break; } if (value === null) { value = $(selector).val(); } if (typeof value === 'undefined') { value = null; } return value; } catch (error) { console.log( '_drupalgap_form_state_values_assemble_get_element_value - ' + error ); } } /** * When a service call results in an error, this function is used to extract the * request's response form errors into a human readble string and returns it. If * there are no form errors, it will return false. * @param {Object} form * @param {Object} form_state * @param {Object} xhr * @param {String} status * @param {String} message * @return {String|Boolean} */ function _drupalgap_form_submit_response_errors(form, form_state, xhr, status, message) { try { var responseText = JSON.parse(xhr.responseText); if (typeof responseText === 'object' && responseText.form_errors) { var msg = ''; for (var element_name in responseText.form_errors) { if (!responseText.form_errors.hasOwnProperty(element_name)) { continue; } var error_msg = responseText.form_errors[element_name]; if (error_msg != '') { // The element name tends to come back weird, e.g. // "field_art_type][und", so let's trim anything at and after // the first "]". var pos = element_name.indexOf(']'); if (pos != -1) { element_name = element_name.substr(0, pos); } var label = element_name; if ( form.elements[element_name] && form.elements[element_name].title ) { label = form.elements[element_name].title; } msg += $('
    (' + label + ') - ' + error_msg + '
    ').text() + '\n'; } } if (msg != '') { return msg; } } return false; } catch (error) { console.log('_drupalgap_form_submit_response_errors - ' + error); } } /** * Themes a checkbox input. * @param {Object} variables * @return {String} */ function theme_checkbox(variables) { try { variables.attributes.type = 'checkbox'; // Check the box? if (variables.checked) { variables.attributes.checked = 'checked'; } var output = ''; return output; } catch (error) { console.log('theme_checkbox - ' + error); } } /** * Themes checkboxes input. * @param {Object} variables * @return {String} */ function theme_checkboxes(variables) { try { var html = ''; variables.attributes.type = 'checkboxes'; for (var value in variables.options) { if (!variables.options.hasOwnProperty(value)) { continue; } var label = variables.options[value]; if (value == 'attributes') { continue; } // Skip attributes. var _label = value; if (!empty(label)) { _label = label; } var checkbox = { value: value, attributes: { name: variables.name + '[' + value + ']', 'class': variables.name, value: value } }; if (variables.value && variables.value[value]) { checkbox.checked = true; } html += ''; } // Check the box? /*if (variables.checked) { variables.attributes.checked = 'checked'; }*/ return html; } catch (error) { console.log('theme_checkbox - ' + error); } } /** * Themes a email input. * @param {Object} variables * @return {String} */ function theme_email(variables) { try { variables.attributes.type = 'email'; var output = ''; return output; } catch (error) { console.log('theme_email - ' + error); } } /** * Themes a file input. * @param {Object} variables * @return {String} */ function theme_file(variables) { try { variables.attributes.type = 'file'; var output = ''; return output; } catch (error) { console.log('theme_file - ' + error); } } /** * Themes a form element label. * @param {Object} variables * @return {String} */ function theme_form_element_label(variables) { try { var element = variables.element; if (empty(element.title)) { return ''; } // Any elements with a title_placeholder set to true // By default, use the element id as the label for, unless the element is // a radio, then use the name. var label_for = ''; if (element.id) { label_for = element.id; } else if (element.attributes && element.attributes['for']) { label_for = element.attributes['for']; } if (element.type == 'radios') { label_for = element.name; } // Render the label. var html = ''; return html; } catch (error) { console.log('theme_form_element_label - ' + error); } } /** * Themes a marker for a required form element label. * @param {Object} variables * @return {String} */ function theme_form_required_marker(variables) { return '*'; } /** * Themes a number input. * @param {Object} variables * @return {String} */ function theme_number(variables) { try { variables.attributes.type = 'number'; return ''; } catch (error) { console.log('theme_number - ' + error); } } /** * Themes a hidden input. * @param {Object} variables * @return {String} */ function theme_hidden(variables) { try { variables.attributes.type = 'hidden'; if (!variables.attributes.value && variables.value != null) { variables.attributes.value = variables.value; } return ''; } catch (error) { console.log('theme_hidden - ' + error); } } /** * Themes a password input. * @param {Object} variables * @return {String} */ function theme_password(variables) { try { variables.attributes.type = 'password'; return ''; } catch (error) { console.log('theme_password - ' + error); } } /** * Themes radio buttons. * @param {Object} variables * @return {String} */ function theme_radios(variables) { try { var radios = ''; if (variables.options) { variables.attributes.type = 'radio'; // Determine an id prefix to use. var id = 'radio'; if (variables.attributes.id) { id = variables.attributes.id; delete variables.attributes.id; } // Set the radio name equal to the id if one doesn't exist. if (!variables.attributes.name) { variables.attributes.name = id; } // Init a delta value so each radio button can have a unique id. var delta = 0; for (var value in variables.options) { if (!variables.options.hasOwnProperty(value)) { continue; } var label = variables.options[value]; if (value == 'attributes') { continue; } // Skip the attributes. var checked = ''; if (variables.value && variables.value == value) { checked = ' checked="checked" '; } var input_id = id + '_' + delta.toString(); var input_label = ''; radios += '' + input_label; delta++; } } return radios; } catch (error) { console.log('theme_radios - ' + error); } } /** * Themes a range input. * @param {Object} variables * @return {String} */ function theme_range(variables) { try { variables.attributes.type = 'range'; if (typeof variables.attributes.value === 'undefined') { variables.attributes.value = variables.value; } var output = ''; return output; } catch (error) { console.log('theme_range - ' + error); } } /** * Themes a search input. * @param {Object} variables * @return {String} */ function theme_search(variables) { try { variables.attributes.type = 'search'; var output = ''; return output; } catch (error) { console.log('theme_search - ' + error); } } /** * Themes a select list input. * @param {Object} variables * @return {String} */ function theme_select(variables) { try { var options = ''; if (variables.options) { for (var value in variables.options) { if (!variables.options.hasOwnProperty(value)) { continue; } var label = variables.options[value]; if (value == 'attributes') { continue; } // Skip the attributes. // Is the option selected? var selected = ''; if (typeof variables.value !== 'undefined') { if ( ($.isArray(variables.value) && in_array(value, variables.value)) || variables.value == value ) { selected = ' selected '; } } // Render the option. options += ''; } } return ''; } catch (error) { console.log('theme_select - ' + error); } } /** * Themes a telephone input. * @param {Object} variables * @return {String} */ function theme_tel(variables) { try { variables.attributes['type'] = 'tel'; return ''; } catch (error) { console.log('theme_tel - ' + error); } } /** * Themes a text input. * @param {Object} variables * @return {String} */ function theme_textfield(variables) { try { variables.attributes.type = 'text'; return ''; } catch (error) { console.log('theme_textfield - ' + error); } } /** * Themes a textarea input. * @param {Object} variables * @return {String} */ function theme_textarea(variables) { try { var output = '
    '; return output; } catch (error) { console.log('theme_textarea - ' + error); } } /** * Implements hook_add_page_to_dom_alter(). */ function hook_add_page_to_dom_alter(attributes, options) { // Use this hook to make alterations to a page's attributes or options before it is added to the DOM. // @param {Object} attributes // The page attributes on a page before it is added to the DOM. // @param {Object} options // The page options object containing the rendered html, the menu link object, and the internal page id. // Add a custom class to the page indicating the device size. Place // empty spaces around the class name so we don't collide with others. var size = null; var width = $(window).width(); if (width <= 414) { size = 'small'; } else if (width < 1024) { size = 'medium'; } else { size = 'large'; } attributes.class += ' ' + size + ' '; // Add a custom class to a particular route. if (drupalgap_router_path_get() == 'channel/%') { attributes.class += ' foo-bar '; } } /** * When a form submission for an entity is assembling the entity json object to * send to the server, some form element fields need to be assembled in unique * ways to match the entity's structure in Drupal. Modules that implement fields * can use this hook to properly assemble the item value (by delta) and return * it. * @param {Object} entity_type * @param {String} bundle * @param {String} form_state_value * @param {Object} field * @param {Object} instance * @param {String} langcode * @param {Number} delta * @param {Object} field_key Set the 'value' string property on this object to * use a custom property name on the field value. * Defaults to 'value'. * Set the 'use_key' boolean property on this object * to false to not use a key when assembling the * result into the field. Defaults to true. * Set the 'use_wrapper' boolean property on this * object to false to not use the default wrapper * placed around the result object. Defaults to true. * Set the 'use_delta' boolean property to false when * a delta value is not needed. Defaults to true. * @param {Object} form * * @return {*} */ function hook_assemble_form_state_into_field(entity_type, bundle, form_state_value, field, instance, langcode, delta, field_key, form) { try { // Listed below are example use cases. Each show how to assemble the result, // and what the resulting field object will look like when assembled by the // DrupalGap Forms API. We'll use a field machine name of 'field_my_field' // in all of the examples. // Optional helpful values. // Grab the form element's name, the form id, and the element's id. // var element_name = field_key.name; // var form_id = field_key.form_id; // var element_id = field_key.element_id; // Example 1 - Here's a simple example using all the defaults. var result = { foo: 'bar' }; return result; // This in turn gets assembled by the DG FAPI. // { // /* ... other entity values and fields ... */ // field_my_field: { // und: [ // { value: { foo: "bar" } } // ] // } // } // Example 2 - Setting the 'value' property. field_key.value = "foo"; var result = "bar"; return result; // { // /* ... other entity values and fields ... */ // field_my_field: { // und: [ // { foo: "bar" } // ] // } // } // Example 3 - Setting the 'use_key' to false. field_key.use_key = false; var result = "bar"; return result; // { // /* ... other entity values and fields ... */ // field_my_field: { // und: ["bar"] // } // } // Example 4 - Setting the 'use_delta' to false. field_key.use_delta = false; var result = "bar"; return result; // { // /* ... other entity values and fields ... */ // field_my_field: { // und: ["bar"] // } // } // Example 5 - Setting the 'use_delta' and 'use_wrapper' to false. field_key.use_delta = false; field_key.use_wrapper = false; var result = "bar"; return result; // { // /* ... other entity values and fields ... */ // field_my_field: { // und: "bar" // } // } } catch (error) { console.log('hook_assemble_form_state_into_field - ' + error); } } /** * When the app is first loading up, DrupalGap checks to see if the device has a connection, if it does then this hook * is called. If DrupalGap doesn't have a connection, then hook_device_offline() is called. Implementations of * hook_deviceready() need to return true if they'd like DrupalGap to continue, or return false if you'd like DrupalGap * to NOT continue. If DrupalGap continues, it will perform a System Connect resource call then go to the App's front * page. This is called during DrupalGap's "deviceready" implementation for PhoneGap. Note, the Drupal.user object is * not initialized at this point, and will always be an anonymous user. */ function hook_deviceready() {} /** * When someone calls drupalgap_has_connection(), this hook has an opportunity to set drupalgap.online to true or false. * The value of drupalgap.online is returned to anyone who calls drupalgap_has_connection(), including DrupalGap core. */ function hook_device_connection() { // If it is Saturday, take the app offline and force the user to go outside and play. var d = new Date(); if (d.getDay() == 6) { drupalgap.online = false; } } /** * Called during app startup if the device does not have a connection. Note, the Drupal.user object is ot initialized at * this point, and will always be an anonymous user. */ function hook_device_offline() { // Even though we're offline, let's just go to the front page. drupalgap_goto(''); } /** * Take action when the user presses the "back" button. This includes the soft, * hardware and browser back buttons. The browser back button is only available * in web app mode, the hardware back button is typically only on compiled * Android devices, whereas the soft back button actually appears within the UX * of the app. * @param {String} from * @param {String} to * @see http://docs.drupalgap.org/7/Widgets/Buttons/Back_Button */ function hook_drupalgap_back(from, to) { // When the user navigates from the front page to the login page, show them // a message (a toast). if (from == drupalgap.settings.front && to == 'user/login') { drupalgap_toast('Please login to continue'); } } /** * Each time a page is navigated to within the app, drupalgap_goto() is called. * Use this hook to do some preprocessing before drupalgap_goto() gets started. * @param {String} path The current page path. */ function hook_drupalgap_goto_preprocess(path) { try { // Pre process the front page. if (path == drupalgap.settings.front) { drupalgap_alert(t('Preprocessing the front page!')); } } catch (error) { console.log('hook_drupalgap_goto_preprocess - ' + error); } } /** * Each time a page is navigated to within the app, drupalgap_goto() is called. * Use this hook to do some post processing after drupalgap_goto() has finished. * @param {String} path The current page path. */ function hook_drupalgap_goto_post_process(path) { try { // Post process the front page. if (path == drupalgap.settings.front) { drupalgap_alert(t('Post processing the front page!')); } } catch (error) { console.log('hook_drupalgap_goto_post_process - ' + error); } } /** * Called after a successful services API call to a Drupal site. Do not call * any services from within your implementation, you may run into an infinite * loop in your code. See http://drupalgap.org/project/force_authentication for * example usage. * @deprecated - use hook_services_postprocess() instead. */ function hook_services_success(url, data) { } /** * A hook used to declare custom block information. */ function hook_block_info() {} /** * A hook used to render custom blocks. */ function hook_block_view(delta, region) { } /** * A hook used to handle a 404 in the app. */ function hook_404(router_path) {} /** * Implements hook_entity_pre_build_content(). */ function hook_entity_pre_build_content(entity, entity_type, bundle) { // Change some weights on nodes with a date field. if (entity_type == 'node' && typeof entity.field_date !== 'undefined') { entity.body.weight = 0; entity.field_date.weight = 1; } } /** * Implements hook_entity_post_build_content(). */ function hook_entity_post_build_content(entity, entity_type, bundle) { } /** * Implements hook_entity_pre_render_content(). * Called before drupalgap_entity_render_content() assembles the entity.content * string. Use this to make modifications to an entity before its' content is rendered. */ function hook_entity_pre_render_content(entity, entity_type, bundle) { try { // Remove access to the date field on all nodes. if (entity_type == 'node' && typeof entity.field_date !== 'undefined') { entity.field_date.access = false; } } catch (error) { console.log('hook_entity_pre_render_content - ' + error); } } /** * Called after drupalgap_entity_render_content() assembles the entity.content * string. Use this to make modifications to the HTML output of the entity's * content before it is displayed. */ function hook_entity_post_render_content(entity, entity_type, bundle) { try { if (entity.type == 'article') { entity.content += '

    ' + t('Example text on every article!') + '

    '; } } catch (error) { console.log('hook_entity_post_render_content - ' + error); } } /** * Implements hook_entity_view_alter(). * Called immediately before a page is rendered and injected into its waiting * container. Use this hook to modifications to the build object by adding or * editing render arrays (widgets) on the build object. */ function hook_entity_view_alter(entity_type, entity_id, mode, build) { try { if (entity_type == 'user' && mode == 'view') { if (entity_id == Drupal.user.uid) { build['foo'] = { markup: '

    Extra stuff when viewing own user profile...

    ' }; build['volume'] = { theme: 'range', attributes: { min: '0', max: '11', value: '11', 'data-theme': 'b' } }; } else { build['bar'] = { markup: '

    Viewing some other profile...

    ' }; } } } catch (error) { console.log('hook_entity_view_alter - ' + error); } } /** * Implements hook_field_info_instance_add_to_form(). * Used by modules that provide custom fields to operate on a form or its * elements before the form gets saved to local storage. This allows extra * data be attached to the form that way things like hook_field_widget_form(), * which takes place at render time, can have access to any extra data it may * need. * @param {String} entity_type * @param {String} bundle * @param {Object} form * @param {Object} entity * @param {Object} element */ function hook_field_info_instance_add_to_form(entity_type, bundle, form, entity, element) { try { // Attach a value_callback to the element so we can manually build its form // state value. element.value_callback = 'example_field_value_callback'; } catch (error) { console.log('hook_field_info_instance_add_to_form - ' + error); } } /** * Implements hook_field_formatter_view(). * @param {String} entity_type * @param {Object} entity * @param {Object} field * @param {Object} instance * @param {String} langcode * @param {Object} items * @param {Object} display */ function hook_field_formatter_view(entity_type, entity, field, instance, langcode, items, display) { try { // Use this hook to render a field's content on an entity. Use dpm() to // inspect the incoming arguments. The arguments contain field display // settings from Drupal. //console.log(entity_type); //console.log(entity); //console.log(field); //console.log(instance); //console.log(langcode); //console.log(items); //console.log(display); // Iterate over each item, and place a widget onto the render array. var content = {}; for (var delta in items) { if (!items.hasOwnProperty(delta)) { continue; } var item = items[delta]; content[delta] = { markup: '

    ' + t('Hello!') + '

    ' }; } return content; } catch (error) { console.log('hook_field_formatter_view - ' + error); } } /** * Implements hook_field_widget_form(). * @param {Object} form * @param {Object} form_state * @param {Object} field * @param {Object} instance * @param {String} langcode * @param {Object} items * @param {Number} delta * @param {Object} element */ function hook_field_widget_form(form, form_state, field, instance, langcode, items, delta, element) { try { // Use this hook to provide field widgets for form element items. This hook // is called for each delta value on the field. Make modifications to the // items collection using the provided delta value. The object contained // within is a standard DrupalGap Forms API object, so you may assemble the // field (and any children widgets) as needed. // Very simple example, make the widget for the field a text field. items[delta].type = 'textfield'; } catch (error) { console.log('hook_field_widget_form - ' + error); } } /** * Called after a form element is assembled. Use it to alter a form element. */ //function hook_form_element_alter(form, element, variables) { } /** * Implements hook_entity_post_render_field(). * Called after drupalgap_entity_render_field() assembles the field content * string. Use this to make modifications to the HTML output of the entity's * field before it is displayed. The field content will be inside of * reference.content, so to make modifications, change reference.content. For * more info: http://stackoverflow.com/questions/518000/is-javascript-a-pass-by-reference-or-pass-by-value-language */ function hook_entity_post_render_field(entity, field_name, field, reference) { if (field_name == 'field_my_image') { reference.content += '

    ' + entity.title + '

    '; } } /** * Implements hook_form_alter(). * This hook is used to make alterations to existing forms. */ function hook_form_alter(form, form_state, form_id) { // Change the description of the name element on the user login form if (form_id == 'user_login_form') { form.elements['name'].description = t('Enter your login name'); } } /** * Implements hook_image_path_alter(). * Called after drupalgap_image_path() assembles the image path. Use this hook * to make modifications to the image path. Return the modified path, or false * to allow the default path to be generated. */ function hook_image_path_alter(src) { } /** * Implements hook_install(). * This hook is used by modules that need to execute custom code when the module * is loaded. Note, the Drupal.user object is not initialized at this point, and * always appears to be an anonymous user. */ function hook_install() { } /** * Implements hook_locale(). * Used to declare language code .json files that should be loaded by DrupalGap. * @see http://drupalgap.org/translate */ function hook_locale() { // Tell DrupalGap to load our custom Spanish and Italian language files // located here: // app/modules/custom/my_module/locale/es.json // app/modules/custom/my_module/locale/it.json return ['es', 'it']; } /** * Implements hook_menu() * This hook is used to declare menu paths for custom pages. */ function hook_menu() { try { var items = {}; items['hello_world'] = { title: t('Hello World'), page_callback: 'my_module_hello_world_page' }; return items; } catch (error) { console.log('hook_menu - ' + error); } } function hook_mvc_model() { var models = {}; return models; } function hook_mvc_view() {} function hook_mvc_controller() {} /** * Implements hook_node_page_view_alter_TYPE(). * @param {Object} node The fully loaded node object. * @param {Object} options The options object, containing the success callback. */ function hook_node_page_view_alter_TYPE(node, options) { try { // Use this hook to completely take over the content that is shown when a // user views a certain content type's page within the app. Pass your // content (render array or html string) to the sucess callback provided in // options to have it automatically injected in the page. var content = {}; content['my_markup'] = { markup: '

    '+t('Click below to see the node!')+'

    ' }; content['my_collapsible'] = { theme: 'collapsible', header: node.title, content: node.content }; options.success(content); } catch (error) { console.log('hook_node_page_view_alter_TYPE() - ' + error); } } /** * Implements hook_page_build(). * @param {Object} output The page build output object. */ function hook_page_build(output) { try { // Remove all titles from article node pages. if (output.node && output.node.type == 'article') { delete output.title; } } catch (error) { console.log('hook_page_build - ' + error); } } /** * Implements hook_preprocess_page(). * Take action before the page is processed and shown to the user. * @param {Object} variables The page variables. */ function hook_preprocess_page(variables) { try { } catch (error) { console.log('hook_preprocess_page - ' + error); } } /** * Implements hook_post_process_page(). * Take action after the page is processed and shown to the user. * @param {Object} variables The page variables. */ function hook_post_process_page(variables) { try { } catch (error) { console.log('hook_post_process_page - ' + error); } } /** * Implements hook_views_exposed_filter(). * @param {Object} form * @param {Object} form_state * @param {Object} element * @param {Object} filter * @param {Object} field */ function hook_views_exposed_filter(form, form_state, element, filter, field) { try { // This hook is used to assemble the form element for an exposed filter. // Make modifications to the element JSON object to control how it is used // on the form. The element comes prepopulated with some basic values. The // majority of the data you need to assemble the field should be contained // within the filter and field JSON objects, so use dpm() to insepct them // and assemble your filter's element. //dpm(filter); //dpm(field); } catch (error) { console.log('hook_views_exposed_filter - ' + error); } } /** * Implements hook_menu(). * @return {Object} */ function comment_menu() { var items = { 'comment/%': { title: t('Comment'), page_callback: 'comment_page_view', page_arguments: [1], pageshow: 'comment_page_view_pageshow', title_callback: 'comment_page_title', title_arguments: [1] }, 'comment/%/view': { title: t('View'), type: 'MENU_DEFAULT_LOCAL_TASK', weight: -10 }, 'comment/%/edit': { title: t('Edit'), page_callback: 'entity_page_edit', pageshow: 'entity_page_edit_pageshow', page_arguments: ['comment_edit', 'comment', 1], weight: 0, type: 'MENU_LOCAL_TASK', access_callback: 'comment_access', access_arguments: [1], options: { reloadPage: true } } }; return items; } /** * Given a comment, this determines if the current user has access to it. * Returns true if so, false otherwise. * @param {Object} comment * @return {Boolean} */ function comment_access(comment) { try { if (comment.uid == Drupal.user.uid && user_access('edit own comments') || user_access('administer comments')) { return true; } else { return false; } } catch (error) { console.log('comment_access - ' + error); } } /** * Given a node id, this will return the id to use on the comments wrapper. * @param {Number} nid * @return {String} */ function comments_container_id(nid) { return 'comments_container_' + nid; } /** * Given a comment id, this will return the id to use on the comment wrapper. * @param {Number} cid * @return {String} */ function comment_container_id(cid) { return 'comment_container_' + cid; } /** * Given a node id, this will return the id to use on the html list for the * comments. * @param {Number} nid * @return {String} * @deprecated Use comments_container_id() instead. */ function comment_list_id(nid) { try { return comments_container_id(nid); } catch (error) { console.log('comment_list_id - ' + error); } } /** * Page callback for comment/%. * @param {Number} cid * @return {Object} */ function comment_page_view(cid) { try { if (cid) { var content = { container: _drupalgap_entity_page_container('comment', cid, 'view') }; return content; } else { drupalgap_error(t('No comment id provided!')); } } catch (error) { console.log('comment_page_view - ' + error); } } /** * jQM pageshow handler for comment/% pages. * @param {Number} cid */ function comment_page_view_pageshow(cid) { try { comment_load(cid, { success: function(comment) { var item = theme('comment', { comment: comment }); var content = theme('jqm_item_list', {items: [item]}); _drupalgap_entity_page_container_inject( 'comment', cid, 'view', content ); } }); } catch (error) { console.log('comment_page_view_pageshow - ' + error); } } /** * The title call back function for the comment view page. * @param {Function} callback * @param {Number} cid */ function comment_page_title(callback, cid) { try { // Try to load the comment subject, then send it back to the given callback. var title = ''; var comment = comment_load(cid, { success: function(comment) { if (comment && comment.subject) { title = comment.subject; } callback.call(null, title); } }); } catch (error) { console.log('comment_page_title - ' + error); } } /** * The comment edit form. * @param {Object} form * @param {Object} form_state * @param {Object} comment * @param {Object} node * @return {Object} */ function comment_edit(form, form_state, comment, node) { try { // If there is no comment object coming in, make an empty one with a node // id. Note, once the form.js submit handler is aware of its own entity and // can pass it along to this function (and others) when loading a form // during the submission process, this little chunk of code will no longer // be needed. if (!comment) { comment = {'nid': arg(1)}; } // Determine the comment bundle from the node type. var node_type = null; if (node && node.type) { node_type = node.type; } else { node_type = comment.node_type.replace('comment_node_', ''); } var bundle = 'comment_node_' + node_type; // Setup form defaults. form.id += '_' + comment.nid; // Append the node id the comment form id. form.entity_type = 'comment'; form.bundle = bundle; form.action = 'node/' + comment.nid; // Add the entity's core fields to the form. drupalgap_entity_add_core_fields_to_form( 'comment', bundle, form, comment ); // Add the fields for this content type to the form. drupalgap_field_info_instances_add_to_form( 'comment', bundle, form, comment ); // Add submit to form. form.elements.submit = { 'type': 'submit', 'value': t('Save') }; // Add cancel and delete button to form if we're editing a comment. Also // figure out a form title to use in the prefix. var form_title = t('Add comment'); if (comment && comment.cid) { form_title = t('Edit comment'); form.buttons['cancel'] = drupalgap_form_cancel_button(); form.buttons['delete'] = drupalgap_entity_edit_form_delete_button('comment', comment.cid); } // Add a prefix. form.prefix += '

    ' + form_title + '

    '; return form; } catch (error) { console.log('comment_edit - ' + error); } } /** * The comment edit submit function. * @param {Object} form * @param {Object} form_state */ function comment_edit_submit(form, form_state) { try { var comment = drupalgap_entity_build_from_form_state(form, form_state); drupalgap_entity_form_submit(form, form_state, comment); } catch (error) { console.log('comment_edit_submit - ' + error); } } /** * Implements hook_services_postprocess(). * @param {Object} options * @param {Object} result */ function comment_services_postprocess(options, result) { try { if (options.service == 'comment' && options.resource == 'create') { // If we're on the node view page, inject the comment into the comment // listing (if it is in the DOM), then scroll the page to the newly // inserted/rendered comment, then clear the form input. var path = drupalgap_path_get(); var router_path = drupalgap_get_menu_link_router_path(path); if (router_path == 'node/%') { var nid = arg(1); var container_id = comments_container_id(nid); var container = $('#' + container_id); if ( typeof container.length !== 'undefined' && container.length == 0 ) { return; } node_load(nid, { reset: true, success: function(node) { comment_load(result.cid, { success: function(comment) { $(container).append( theme('comment', { comment: comment }) ).trigger('create'); scrollToElement('#' + container_id + ' #' + comment_container_id(comment.cid), 500); var form_selector = '#' + drupalgap_get_page_id() + ' #comment_edit' + '_' + comment.nid; drupalgap_form_clear(form_selector); } }); } }); } } } catch (error) { console.log('comment_services_postprocess - ' + error); } } /** * Theme's a comment container. * @param {Object} variables * @return {String} */ function theme_comments(variables) { try { // Set the container id and append default attributes. variables.attributes.id = comments_container_id(variables.node.nid); variables.attributes['class'] += 'comments comments-node-' + variables.node.type; variables.attributes['data-role'] = 'collapsible-set'; // Open the container. var html = '
    '; // Show a comments title if there are any comments. if (variables.node.comment_count > 0) { html += '

    Comments

    '; } // If the comments are already rendered, show them. if (variables.comments) { html += variables.comments; } // Close the container and return the html. html += '
    '; return html; } catch (error) { console.log('theme_comments - ' + error); } } /** * Theme's a comment. * @param {Object} variables * @return {String} */ function theme_comment(variables) { try { var comment = variables.comment; // Set the container id and append default attributes. variables.attributes.id = comment_container_id(comment.cid); variables.attributes['class'] += 'comment '; variables.attributes['data-role'] = 'collapsible'; variables.attributes['data-collapsed'] = 'false'; var html = '
    '; var comment_content = ''; // Any user picture? // @TODO - the user picture doesn't use an image style here, it uses the // original picture uploaded by the user, which can be varying sizes. var picture = ''; if (comment.picture_uri) { picture += theme( 'image', { path: drupalgap_image_path(comment.picture_uri) } ); } // Comment date. var created = new Date(comment.created * 1000); created = created.toLocaleDateString() + ' at ' + created.toLocaleTimeString(); // Append comment extra fields and content. The user info will be rendered // as a list item link. var author = picture + '

    ' + comment.name + '

    ' + '

    ' + created + '

    '; author = l(author, 'user/' + comment.uid); comment_content += '

    ' + comment.subject + '

    ' + '' + comment.content; html += comment_content; // Add an edit link if necessary. if ( user_access('administer comments') || (user_access('edit own comments') && comment.uid == Drupal.user.uid) ) { html += theme('button_link', { text: t('Edit'), path: 'comment/' + comment.cid + '/edit', attributes: { 'data-icon': 'gear' } }); } // Close the container and return the html. html += '
    '; return html; } catch (error) { console.log('theme_comment - ' + error); } } /** * Implements hook_menu(). * @return {Object} */ function contact_menu() { try { var items = {}; items['contact'] = { title: t('Contact'), page_callback: 'drupalgap_get_form', page_arguments: ['contact_site_form'], pageshow: 'contact_site_form_pageshow', access_arguments: ['access site-wide contact form'] }; items['user/%/contact'] = { title: t('User contact'), page_callback: 'drupalgap_get_form', page_arguments: ['contact_personal_form', 1], pageshow: 'contact_personal_form_pageshow', access_arguments: ['access user contact forms'], weight: 10, type: 'MENU_LOCAL_TASK' }; return items; } catch (error) { console.log('contact_menu - ' + error); } } /** * The contact index service resource. * @param {Object} options */ function contact_index(options) { try { options.method = 'GET'; options.path = 'contact.json'; options.service = 'contact'; options.resource = 'index'; Drupal.services.call(options); } catch (error) { console.log('contact_index - ' + error); } } /** * The contact site service resource. * @param {Object} options */ function contact_site(options) { try { options.method = 'POST'; options.path = 'contact/site.json'; options.service = 'contact'; options.resource = 'site'; Drupal.services.call(options); } catch (error) { console.log('contact_site - ' + error); } } /** * The contact personal service resource. * @param {Object} options */ function contact_personal(options) { try { options.method = 'POST'; options.path = 'contact/personal.json'; options.service = 'contact'; options.resource = 'personal'; Drupal.services.call(options); } catch (error) { console.log('contact_personal - ' + error); } } /** * The site contact form. * @param {Object} form * @param {Object} form_state * @return {Object} */ function contact_site_form(form, form_state) { try { form.elements.name = { title: t('Your name'), type: 'textfield', required: true }; form.elements.mail = { title: t('Your e-mail address'), type: 'email', required: true }; form.elements.subject = { title: t('Subject'), type: 'textfield', required: true }; form.elements.cid = { title: t('Category'), type: 'select', required: true }; form.elements.message = { title: t('Message'), type: 'textarea', required: true }; form.elements.copy = { title: t('Send yourself a copy?'), type: 'checkbox', default_value: 0, access: false }; form.elements.submit = { type: 'submit', value: t('Send message') }; // If the user is logged in, set the default values. if (Drupal.user.uid != 0) { form.elements.name.default_value = Drupal.user.name; form.elements.name.disabled = true; form.elements.mail.default_value = Drupal.user.mail; form.elements.mail.disabled = true; form.elements.copy.access = true; } return form; } catch (error) { console.log('contact_site_form - ' + error); } } /** * The pageshow callback for the contact site form. */ function contact_site_form_pageshow() { try { contact_index({ success: function(results) { var select = $('#edit-contact-site-form-cid'); if (!results || !results.length) { return; } for (var index in results) { if (!results.hasOwnProperty(index)) { continue; } var result = results[index]; var selected = result.selected == 1 ? 'selected' : ''; var option = ''; $(select).append(option); } $(select).selectmenu('refresh'); if (results.length == 1) { $(select).hide(); } } }); } catch (error) { console.log('contact_site_form_pageshow - ' + error); } } /** * The site wide contact form submit handler. * @param {Ojbect} form * @param {Ojbect} form_state */ function contact_site_form_submit(form, form_state) { var data = { name: form_state.values['name'], mail: form_state.values['mail'], subject: form_state.values['subject'], cid: form_state.values['cid'], message: form_state.values['message'], copy: form_state.values['copy'] }; contact_site({ data: JSON.stringify(data), success: function(result) { if (result[0]) { drupalgap_alert(t('Your message has been sent!')); } else { drupalgap_alert( t('There was a problem sending your message!'), { title: t('Error') } ); } drupalgap_form_clear(); }, error: function(xhr, status, message) { if (message) { message = JSON.parse(message); if (message.form_errors) { var errors = ''; for (var element in message.form_errors) { if (!message.form_errors.hasOwnProperty(element)) { continue; } var error = message.form_errors[element]; errors += error + '\n'; } if (errors != '') { drupalgap_alert(errors); } } } } }); } /** * The personal contact form. * @param {Object} form * @param {Object} form_state * @param {Number} recipient * @return {Object} */ function contact_personal_form(form, form_state, recipient) { try { // @TODO - when providing a personal contact form, make sure the user has // their personal contact form enabled. form.elements.name = { title: t('Your name'), type: 'textfield', required: true }; form.elements.mail = { title: t('Your e-mail address'), type: 'email', required: true }; form.elements.to = { type: 'hidden', required: true }; var container_id = contact_personal_form_to_container_id(recipient); form.elements.to_display = { title: 'To', markup: '
    ' }; form.elements.subject = { title: t('Subject'), type: 'textfield', required: true }; form.elements.message = { title: t('Message'), type: 'textarea', required: true }; form.elements.copy = { title: t('Send yourself a copy?'), type: 'checkbox', default_value: 0, access: false }; form.elements.submit = { type: 'submit', value: t('Send message') }; // If the user is logged in, set the default values. if (Drupal.user.uid != 0) { form.elements.name.default_value = Drupal.user.name; form.elements.name.disabled = true; form.elements.mail.default_value = Drupal.user.mail; form.elements.mail.disabled = true; form.elements.copy.access = true; } return form; } catch (error) { console.log('contact_personal_form - ' + error); } } /** * The pageshow callback for the personal contact form. * @param {Object} form * @param {Number} recipient */ function contact_personal_form_pageshow(form, recipient) { try { user_load(recipient, { success: function(account) { // Make sure the user has their contact form enabled. if (!account.data.contact) { $('#' + drupalgap_get_page_id() + ' #drupalgap_form_errors').html( "
    " + t("Sorry, this user's contact form is disabled.") + '
    ' ); return; } // Populate hidden value for the 'to' field. var container_id = contact_personal_form_to_container_id(recipient); $('#' + container_id).html(l(account.name, 'user/' + account.uid)); var hidden_selector = '#' + drupalgap_get_page_id() + ' #edit-contact-personal-form-to'; $(hidden_selector).val(account.name); } }); } catch (error) { console.log('contact_personal_form_pageshow - ' + error); } } /** * The personal contact form submit handler. * @param {Ojbect} form * @param {Ojbect} form_state */ function contact_personal_form_submit(form, form_state) { var data = { name: form_state.values['name'], mail: form_state.values['mail'], to: form_state.values['to'], subject: form_state.values['subject'], message: form_state.values['message'], copy: form_state.values['copy'] }; contact_personal({ data: JSON.stringify(data), success: function(result) { if (result[0]) { drupalgap_alert(t('Your message has been sent!')); } else { drupalgap_alert( t('There was a problem sending your message!'), { title: t('Error') } ); } drupalgap_form_clear(); }, error: function(xhr, status, message) { if (message) { message = JSON.parse(message); if (message.form_errors) { var errors = ''; for (var element in message.form_errors) { if (!message.form_errors.hasOwnProperty(element)) { continue; } var error = message.form_errors[element]; errors += error + '\n'; } if (errors != '') { drupalgap_alert(errors); } } } } }); } /** * Returns the div container id to use on the "to" markup on the personal * contact form. * @param {Number} recipient * @return {String} */ function contact_personal_form_to_container_id(recipient) { return 'contact_personal_form_user_' + recipient; } /** * Given an entity type and optional bundle, this will return the view mode machine name to use. * It defaults to "drupalgap", but can be configured. * @param {String} entity_type * @param {String} bundle * @returns {string} * @see http://docs.drupalgap.org/7/Entities/Display_Modes */ function drupalgap_entity_view_mode(entity_type, bundle) { var view_mode = 'drupalgap'; if (typeof drupalgap.settings.view_modes !== 'undefined') { if (entity_type && bundle) { if ( drupalgap.settings.view_modes[entity_type] && drupalgap.settings.view_modes[entity_type][bundle] && drupalgap.settings.view_modes[entity_type][bundle].view_mode ) { view_mode = drupalgap.settings.view_modes[entity_type][bundle].view_mode; } } else if (entity_type) { if ( drupalgap.settings.view_modes[entity_type] && drupalgap.settings.view_modes[entity_type].view_mode ) { view_mode = drupalgap.settings.view_modes[entity_type].view_mode; } } } return view_mode; } /** * Given an entity type, id and optional context, this will return a container id to be used * when constructing a placeholder to load/display/edit an entity. * @param {String} entity_type * @param {Number} entity_id * @param {String} context An optional context to use, e.g. "edit" * @returns {string} */ function drupalgap_get_entity_container_id(entity_type, entity_id, context) { var id = 'dg-entity-container-' + entity_type + '-' + entity_id; if (context) { id += '-' + context; } return id; } /** * A page_callback function used to build an empty placeholder for an entity and inline * JavaScript to retrieve the entity for display. * @param {String} handler * @param {String} entity_type * @param {Number} entity_id * @returns {string} */ function drupalgap_get_entity(handler, entity_type, entity_id, context) { return '
    ' + drupalgap_jqm_page_event_script_code({ jqm_page_event_callback: 'drupalgap_get_entity_pageshow', jqm_page_event_args: JSON.stringify({ handler: handler, entity_type: entity_type, entity_id: entity_id }) }); } /** * A pageshow function used to retrieve an entity, pass it along to its handler for rendering, * and then inject the handler's render array into the waiting placeholder. * @param {Object} options */ function drupalgap_get_entity_pageshow(options) { var context = drupalgap.menu_links[drupalgap_router_path_get()].page_arguments.length == 4 ? drupalgap.menu_links[drupalgap_router_path_get()].page_arguments[3] : null; entity_load(options.entity_type, options.entity_id, { success: function(entity) { var id = drupalgap_get_entity_container_id(options.entity_type, options.entity_id, context); $('#' + id).html(drupalgap_render(window[options.handler](entity))).trigger('create'); if (drupalgap.page.options.success) { drupalgap.page.options.success(entity); } }, error: drupalgap.page.options.error ? drupalgap.page.options.error : null }); } /** * A page_callback function used to build an empty placeholder for an entity and inline * JavaScript to retrieve the entity for editing. * @param {String} handler * @param {String} entity_type * @param {Number} entity_id * @returns {string} */ function drupalgap_get_entity_form(handler, entity_type, entity_id, context) { context = !context ? 'edit' : context; return '
    ' + drupalgap_jqm_page_event_script_code({ jqm_page_event_callback: 'drupalgap_get_entity_form_pageshow', jqm_page_event_args: JSON.stringify({ handler: handler, entity_type: entity_type, entity_id: entity_id }) }); } /** * A pageshow function used to retrieve an entity, pass it along to a form builder, and then * inject the form into the waiting placeholder. * @param {Object} options */ function drupalgap_get_entity_form_pageshow(options) { var context = drupalgap.menu_links[drupalgap_router_path_get()].page_arguments.length == 4 ? drupalgap.menu_links[drupalgap_router_path_get()].page_arguments[3] : 'edit'; var done = function(entity) { var id = drupalgap_get_entity_container_id(options.entity_type, options.entity_id, context); $('#' + id).html(drupalgap_get_form(options.handler, entity)).trigger('create'); if (drupalgap.page.options.success) { drupalgap.page.options.success(entity); } }; if (options.entity_id == 'add') { done({}); } else { entity_load(options.entity_type, options.entity_id, { success: done, error: drupalgap.page.options.error ? drupalgap.page.options.error : null }); } } /** * Implements hook_install(). */ function entity_install() { entity_clean_local_storage(); } /** * Given an entity type, bundle name, form and entity, this will add the * entity's core fields to the form via the DrupalGap forms api. * @param {String} entity_type * @param {String} bundle * @param {Object} form * @param {Object} entity */ function drupalgap_entity_add_core_fields_to_form(entity_type, bundle, form, entity) { try { // Grab the core fields for this entity type and bundle. var fields = drupalgap_entity_get_core_fields(entity_type, bundle); // Iterate over each core field in the entity and add it to the form. If // there is a value present in the entity, then set the field's form element // default value equal to the core field value. for (var name in fields) { if (!fields.hasOwnProperty(name)) { continue; } var field = fields[name]; var default_value = field.default_value; if (entity && entity[name]) { default_value = entity[name]; } form.elements[name] = field; form.elements[name].default_value = default_value; } } catch (error) { console.log('drupalgap_entity_add_core_fields_to_form - ' + error); } } /** * Deprecated! Given an entity type, the bundle, the entity (assembled from form * state values) and any options, this assembles the ?data= string for the * entity service resource call URLs. * @param {String} entity_type * @param {String} bundle * @param {Object} entity * @param {Object} options */ function drupalgap_entity_assemble_data(entity_type, bundle, entity, options) { try { console.log('WARNING: drupalgap_entity_assemble_data() has been ' + 'deprecated! Now just call e.g. node_save() for auto assembly.'); return; } catch (error) { console.log('drupalgap_entity_assemble_data - ' + error); } } /** * Returns the 'Delete' button object that is used on entity edit forms. * @param {String} entity_type * @param {Number} entity_id * @return {Object} */ function drupalgap_entity_edit_form_delete_button(entity_type, entity_id) { return { title: t('Delete'), attributes: { onclick: "javascript:drupalgap_entity_edit_form_delete_confirmation('" + entity_type + "', " + entity_id + ');' } }; } /** * Given an entity type and id, this will display a confirmation dialogue and * will subsequently delete the entity if the user confirms the dialogue box. * The Services module retains Drupal user permissions so users without proper * permissions will not be able to delete the entities from the server. * @param {String} entity_type * @param {Number} entity_id * @return {*} */ function drupalgap_entity_edit_form_delete_confirmation(entity_type, entity_id) { try { var confirm_msg = t('Delete this content, are you sure? This action cannot be undone...'); drupalgap_confirm(confirm_msg, { confirmCallback: function(button) { if (button == 2) { return; } // Change the jQM loader mode to deleting. drupalgap.loader = 'deleting'; // Set up the api call arguments and success callback. var call_arguments = {}; call_arguments.success = function(result) { // Remove the entities page from the DOM, if it exists. var entity_page_path = entity_type + '/' + entity_id; var entity_page_id = drupalgap_get_page_id(entity_page_path); if (drupalgap_page_in_dom(entity_page_id)) { drupalgap_remove_page_from_dom(entity_page_id); } // Remove the entity from local storage. // @todo - this should be moved to jDrupal. window.localStorage.removeItem( entity_local_storage_key(entity_type, entity_id) ); // Go to the front page, unless a form action path was specified. var form = drupalgap_form_local_storage_load('node_edit'); var destination = form.action ? form.action : ''; drupalgap_goto(destination, { reloadPage: true, form_submission: true }); }; // Call the delete function. var name = services_get_resource_function_for_entity( entity_type, 'delete' ); var fn = window[name]; fn(entity_id, call_arguments); } }); } catch (error) { console.log('drupalgap_entity_edit_form_delete_confirmation - ' + error); } } /** * Given an entity, this will render the content of the entity and place it in * the entity JSON object as the 'content' property. * @param {String} entity_type * @param {Object} entity */ function drupalgap_entity_render_content(entity_type, entity) { try { entity.content = ''; // Figure out the bundle. var bundle = entity.type; if (entity_type == 'comment') { bundle = entity.bundle; } else if (entity_type == 'taxonomy_term') { bundle = entity.vocabulary_machine_name; } // Load the field info for this entity and bundle combo. var field_info = drupalgap_field_info_instances(entity_type, bundle); if (!field_info) { return; } // Give modules a chance to pre build the content. module_invoke_all('entity_pre_build_content', entity, entity_type, bundle); // Render each field on the entity, using the drupalgap or default display. var field_weights = {}; var field_displays = {}; for (var field_name in field_info) { if (!field_info.hasOwnProperty(field_name)) { continue; } var field = field_info[field_name]; // Determine which display mode to use. The default mode will be used if the drupalgap display mode is not // present, unless a view mode has been specified in settings.js then we'll use that config for the current // entity/bundle combo. If a module isn't listed on a custom display, use the default display's module. if (!field.display) { break; } var display = field.display['default']; var view_mode = drupalgap_entity_view_mode(entity_type, bundle); if (field.display[view_mode]) { display = field.display[view_mode]; if (typeof display.module === 'undefined' && typeof field.display['default'].module !== 'undefined' ) { display.module = field.display['default'].module; } } // Skip hidden fields. if (display.type == 'hidden') { continue; } // Save the field display and weight. Use the weight from the field's render element if it's available, // otherwise fallback to the weight mentioned in the display. field_displays[field_name] = display; field_weights[field_name] = entity[field_name] && typeof entity[field_name].weight !== 'undefined' ? entity[field_name].weight : display.weight; } // Give modules a chance to alter the build content. module_invoke_all('entity_post_build_content', entity, entity_type, bundle); // Extract the field weights and sort them. var extracted_weights = []; for (var field_name in field_weights) { if (!field_weights.hasOwnProperty(field_name)) { continue; } var weight = field_weights[field_name]; extracted_weights.push(weight); } extracted_weights.sort(function(a, b) { return a - b; }); // Give modules a chance to pre alter the content. module_invoke_all('entity_pre_render_content', entity, entity_type, bundle); // For each sorted weight, locate the field with the corresponding weight, // then render it's field content. var completed_fields = []; for (var weight_index in extracted_weights) { if (!extracted_weights.hasOwnProperty(weight_index)) { continue; } var target_weight = extracted_weights[weight_index]; for (var field_name in field_weights) { if (!field_weights.hasOwnProperty(field_name) || typeof entity[field_name] === 'undefined') { continue; } if (typeof entity[field_name].access !== 'undefined' && !entity[field_name].access) { continue; } var weight = field_weights[field_name]; if (target_weight == weight) { if (completed_fields.indexOf(field_name) == -1) { completed_fields.push(field_name); entity.content += drupalgap_entity_render_field( entity_type, entity, field_name, field_info[field_name], field_displays[field_name] ); break; } } } } // Give modules a chance to alter the content. module_invoke_all('entity_post_render_content', entity, entity_type, bundle); // Update this entity in local storage so the content property sticks. if (entity_caching_enabled(entity_type, bundle)) { _entity_set_expiration_time(entity_type, entity); _entity_local_storage_save( entity_type, entity[entity_primary_key(entity_type)], entity ); } } catch (error) { console.log('drupalgap_entity_render_content - ' + error); } } /** * Given an entity_type, the entity, a field name, and the field this will * render the field using the appropriate hook_field_formatter_view(). * @param {String} entity_type * @param {Object} entity * @param {String} field_name * @param {Object} field * @param {*} display * @return {String} */ function drupalgap_entity_render_field(entity_type, entity, field_name, field, display) { try { var content = ''; // Determine module that implements the hook_field_formatter_view, // then determine the hook's function name, then render the field content. // If there wasn't a module specified in the display, look to the module // specified in the field widget. If we still don't find it, then just // return. var module = display['module']; if (!module) { if (!field.widget.module) { var msg = 'drupalgap_entity_render_field - ' + 'unable to locate the module for the field (' + field_name + ')'; console.log(msg); return content; } else { module = field.widget.module; } } var function_name = module + '_field_formatter_view'; if (function_exists(function_name)) { // Grab the field formatter function, then grab the field items // from the entity, then call the formatter function and append its result // to the entity's content. var fn = window[function_name]; var items = null; // Check to see if translated content based on app's language setting // is present or not. If yes, then use that language as per setting. // Determine the language code. Note, multi lingual sites may have a // language code on the entity, but still have 'und' on the field, so // fall back to 'und' if the field's language code doesn't match the // entity's language code. var default_lang = language_default(); var language = entity.language; if (entity[field_name]) { if (entity[field_name][default_lang]) { items = entity[field_name][default_lang]; } else if (entity[field_name][language]) { items = entity[field_name][language]; } else if (entity[field_name]['und']) { items = entity[field_name]['und']; language = 'und'; } else { items = entity[field_name]; } } // @TODO - We've been sending 'field' as the instance // (drupalgap_field_info_instance), and the 'instance' as the field // (drupalgap_field_info_field). This is backwards, and should be // reversed. All contrib modules with field support will need to be // udpated to reflect this. Lame. var elements = fn( entity_type, entity, field, /* This is actually the instance, doh! (I think) */ drupalgap_field_info_field(field_name), language, items, display ); for (var delta in elements) { if (!elements.hasOwnProperty(delta)) { continue; } var element = elements[delta]; // If the element has markup, render it as is, if it is // themeable, then theme it. var element_content = ''; if (element.markup) { element_content = element.markup; } else if (element.theme) { element_content = theme(element.theme, element); } content += element_content; } } else { console.log( 'WARNING: drupalgap_entity_render_field - ' + function_name + '() ' + 'does not exist! (' + field_name + ')' ); } // Render the field label, if necessary. if (content != '' && display['label'] != 'hidden') { var label = '

    ' + field.label + '

    '; // Place the label above or below the field content. label = '
    ' + label + '
    '; switch (display['label']) { case 'below': content += label; break; case 'above': default: content = label + content; break; } } // Finally, wrap the rendered field in a div, and set the field name as the // class name on the wrapper. content = '
    ' + content + '
    '; // Give modules a chance to alter the field content. var reference = {'content': content}; module_invoke_all( 'entity_post_render_field', entity, field_name, field, reference ); if (reference.content != content) { return reference.content; } return content; } catch (error) { console.log('drupalgap_entity_render_field - ' + error); } } /** * Given a form and form_state, this will assemble an entity from the form_state * values and return the entity as a JSON object. * @param {Object} form * @param {Object} form_state * @return {Object} */ function drupalgap_entity_build_from_form_state(form, form_state) { try { var entity = {}; var language = language_default(); for (var name in form_state.values) { if (!form_state.values.hasOwnProperty(name)) { continue; } var value = form_state.values[name]; // Skip elements with restricted access. if ( typeof form.elements[name].access !== 'undefined' && !form.elements[name].access ) { continue; } // Determine wether or not this element is a field. If it is, determine // it's module and field assembly hook. var is_field = false; var module = false; var hook = false; if (form.elements[name].is_field) { is_field = true; module = form.elements[name].field_info_field.module; hook = module + '_assemble_form_state_into_field'; if (!function_exists(hook)) { hook = false; } } // Retrieve the potential key for the element, if we don't get one // then it is a flat field that should be attached as a property to the // entity. Otherwise attach the key and value to the entity. var key = drupalgap_field_key(name); // e.g. value, fid, tid, nid, etc. if (key) { // Determine how many allowed values for this field. var allowed_values = form.elements[name].field_info_field.cardinality; // Convert unlimited value fields to one, for now... if (allowed_values == -1) { allowed_values = 1; } // Make sure there is at least one value before creating the form // element on the entity. if (typeof value[language][0] === 'undefined') { continue; } // Create an empty object to house the field on the entity. entity[name] = {}; // Some fields do not use a delta value in the service call, so we // prepare for that here. // @todo - Do all options_select widgets really have no delta value? // Or is it only single value fields that don't have it? We need to // test this. var use_delta = true; if ( form.elements[name].type == 'taxonomy_term_reference' || form.elements[name].field_info_instance.widget.type == 'options_select' ) { use_delta = false; entity[name][language] = {}; } else { entity[name][language] = []; } // Now iterate over each delta on the form element, and add the value // to the entity. for (var delta = 0; delta < allowed_values; delta++) { if (typeof value[language][delta] !== 'undefined') { // @TODO - the way values are determined here is turning into // spaghetti code. Every form element needs its own // value_callback, just like Drupal's FAPI. Right now DG has // something similar going on with the use of // hook_assemble_form_state_into_field(). So replace any spaghetti // below with a value_callback. Provide a deprecated hook warning // for any fields not haven't caught up yet, and fallback to the // hook for a while. // @UPDATE - Actually, the DG FAPI // hook_assemble_form_state_into_field() is a good idea, and // should be used by all field form elements, then in // drupalgap_field_info_instances_add_to_form(), that function // should use the value_callback idea to properly map entity data // to the form element's value. // Extract the value. var field_value = value[language][delta]; // By default, we'll assume we'll be attaching this element item's // value according to a key (usually 'value' is the default key // used by Drupal fields). However, we'll give modules that // implement hook_assemble_form_state_into_field() an opportunity // to specify no usage of a key if their item doesn't need one. // The geofield module is an example of field that doesn't use a // key. The use_wrapper flag allows others to completely override // the use of a wrapper around the field value, e.g. taxonomy term // reference autocomplete. We'll attach any other helpful // variables here as well (element name, form id, etc). var field_key = { value: 'value', use_key: true, use_wrapper: true, use_delta: use_delta, name: name, form_id: form.id, element_id: form.elements[name][language][delta].id }; // If this element is a field, give the field's module an // opportunity to assemble its own value, otherwise we'll just // use the field value extracted above. if (is_field && hook) { var fn = window[hook]; field_value = fn(form.entity_type, form.bundle, field_value, form.elements[name].field_info_field, form.elements[name].field_info_instance, language, delta, field_key, form ); } // If someone updated the key, use it. if (key != field_key.value) { key = field_key.value; } // If we don't need a delta value, place the field value using the // key, if posible. If we're using a delta value, push the key // and value onto the field to indicate the delta. if (!field_key.use_delta) { if (!field_key.use_wrapper) { entity[name][language] = field_value; } else { if ($.isArray(entity[name][language])) { console.log( 'WARNING: drupalgap_entity_build_from_form_state - ' + 'cannot use key (' + key + ') on field (' + name + ') ' + 'language code array, key will be ignored.' ); entity[name][language].push(field_value); } else { entity[name][language][key] = field_value; } } } else { if (field_key.use_key) { var item = {}; item[key] = field_value; entity[name][language].push(item); } else { entity[name][language].push(field_value); } } // If the field value was null, we won't send along the field, so // just remove it. Except for list_boolean fields, they need a // null value to set the field value to false. if ( field_value === null && typeof entity[name] !== 'undefined' && form.elements[name].type != 'list_boolean' ) { if (is_field) { if (delta == 0) { delete entity[name]; } else if (typeof entity[name][language][delta] !== 'undefined') { delete entity[name][language][delta]; } } else { delete entity[name]; } } // If we had an optional select list, and no options were // selected, delete the empty field from the assembled entity. // @TODO - will this cause multi value issues? if ( is_field && !use_delta && form.elements[name].field_info_instance.widget.type == 'options_select' && !form.elements[name].required && field_value === '' && typeof entity[name] !== 'undefined' ) { delete entity[name]; } } } } else if (typeof value !== 'undefined') { entity[name] = value; } } return entity; } catch (error) { console.log('drupalgap_entity_build_from_form_state - ' + error); } } /** * Given a form, form_state and entity, this will call the appropriate service * resource to create or update the entity. * @param {Object} form * @param {Object} form_state * @param {Object} entity * @return {*} */ function drupalgap_entity_form_submit(form, form_state, entity) { try { // Grab the primary key name for this entity type. var primary_key = entity_primary_key(form.entity_type); // Determine if we are editing an entity or creating a new one. var editing = false; if (entity[primary_key] && entity[primary_key] != '') { editing = true; } // Let's set up the api call arguments. var call_arguments = {}; // Setup the success call back to go back to the entity page view. call_arguments.success = function(result) { try { // If no one has provided a form.action to submit this form to, // by default we'll try to redirect to [entity-type]/[entity-id] to view // the entity. For taxonomy, we replace the underscore with a forward // slash in the path. var destination = form.action; if (!destination) { var prefix = form.entity_type; if (prefix == 'taxonomy_vocabulary' || prefix == 'taxonomy_term') { prefix = prefix.replace('_', '/'); } destination = prefix + '/' + result[primary_key]; } // Is there a destination URL query parameter overwriting the action? if (_GET('destination')) { destination = _GET('destination'); } // Set up the default goto options, and use any options provided by the // form. var goto_options = { form_submission: true }; if (form.action_options) { goto_options = $.extend({}, goto_options, form.action_options); } // Finally goto our destination. drupalgap_goto(destination, goto_options); } catch (error) { console.log('drupalgap_entity_form_submit - success - ' + error); } }; // Setup the error call back. call_arguments.error = function(xhr, status, message) { try { // If there were any form errors, display them in an alert. var msg = _drupalgap_form_submit_response_errors(form, form_state, xhr, status, message); if (msg) { drupalgap_alert(msg); } } catch (error) { console.log('drupalgap_entity_form_submit - error - ' + error); } }; // Change the jQM loader mode to saving. drupalgap.loader = 'saving'; // Depending on if we are creating a new entity, or editing an existing one, // call the appropriate service resource. var crud = 'create'; if (editing) { crud = 'update'; // Remove the entity from local storage. // @todo This should be moved to jDrupal. window.localStorage.removeItem( entity_local_storage_key(form.entity_type, entity[primary_key]) ); } var fn = window[ services_get_resource_function_for_entity(form.entity_type, crud) ]; fn(entity, call_arguments); } catch (error) { console.log('drupalgap_entity_form_submit - ' + error); } } /** * Given an entity type, this returns its core fields as forms api elements. * @param {String} entity_type * @param {String} bundle * @return {Object} */ function drupalgap_entity_get_core_fields(entity_type, bundle) { try { // @todo - was this function what we were tyring to accomplish with the // early entity_info hook imitations? // @todo - And why is this function not populated dynamically via Drupal? var fields = {}; switch (entity_type) { case 'comment': var content_type = bundle.replace('comment_node_', ''); // Add each schema field to the field collection. var base_table = drupalgap.entity_info[entity_type].schema_fields_sql['base table']; for (var index in base_table) { if (!base_table.hasOwnProperty(index)) { continue; } var name = base_table[index]; var field = { type: 'hidden', required: false, default_value: '', title: ucfirst(name) }; fields[name] = field; } // Make the node id required. fields['nid'].required = true; // Only anonymous users can fill out the name field, authenticated users // have their name auto filled and disabled. fields['name'].type = 'textfield'; if (Drupal.user.uid != 0) { fields['name'].default_value = Drupal.user.name; fields['name'].disabled = true; } // If the 'Allow comment title' is enabled on the content type, show // the comment subject field. if (drupalgap.content_types_list[content_type].comment_subject_field) { fields['subject'].type = 'textfield'; } // Depending on this content type's comment settings, let's make // modifications to the form elements. // admin/structure/types/manage/article // 0 = Anonymous posters may not enter their contact information // 1 = Anonymous posters may leave their contact information // 2 = Anonymous posters must leave their contact information var comment_anonymous = drupalgap.content_types_list[content_type].comment_anonymous; switch (comment_anonymous) { case '0': delete(fields['mail']); delete(fields['homepage']); break; case '1': break; case '2': fields['mail'].required = true; fields['homepage'].required = true; break; } // Only anonymous users get the mail and homepage fields. if (Drupal.user.uid == 0) { if (fields['mail']) { fields['mail'].type = 'textfield'; } if (fields['homepage']) { fields['homepage'].type = 'textfield'; } } break; case 'node': fields.nid = { 'type': 'hidden', 'required': false, 'default_value': '' }; fields.title = { 'type': 'textfield', 'title': t('Title'), 'required': true, 'default_value': '', 'description': '' }; fields.type = { 'type': 'hidden', 'required': true, 'default_value': '' }; fields.language = { 'type': 'hidden', 'required': true, 'default_value': language_default() }; break; case 'user': fields.uid = { 'type': 'hidden', 'required': false, 'default_value': '' }; fields.name = { 'type': 'textfield', 'title': t('Username'), 'required': true, 'default_value': '', 'description': '' }; fields.mail = { 'type': 'email', 'title': t('E-mail address'), 'required': true, 'default_value': '', 'description': '' }; fields.picture = { 'type': 'image', 'widget_type': 'imagefield_widget', 'title': t('Picture'), 'required': false, 'value': t('Add Picture') }; break; case 'taxonomy_term': fields = { 'vid': { 'type': 'hidden', 'required': true, 'default_value': '' }, 'tid': { 'type': 'hidden', 'required': false, 'default_value': '' }, 'name': { 'type': 'textfield', 'title': t('Name'), 'required': true, 'default_value': '' }, 'description': { 'type': 'textarea', 'title': t('Description'), 'required': false, 'default_value': '' } }; break; case 'taxonomy_vocabulary': fields = { 'vid': { 'type': 'hidden', 'required': false, 'default_value': '' }, 'name': { 'type': 'textfield', 'title': t('Name'), 'required': true, 'default_value': '' }, 'machine_name': { 'type': 'textfield', 'title': t('Machine Name'), 'required': true, 'default_value': '' }, 'description': { 'type': 'textarea', 'title': t('Description'), 'required': false, 'default_value': '' } }; break; default: console.log( 'drupalgap_entity_get_core_fields - entity type not supported yet (' + entity_type + ')' ); break; } return fields; } catch (error) { console.log('drupalgap_entity_get_core_fields - ' + error); } } /** * Given an entity_type, this returns the entity JSON info, if it exists, false * otherwise. You may optionally call this function with no arguments to * retrieve the JSON info for all entity types. See also * @see http://api.drupal.org/api/drupal/includes%21common.inc/function/entity_get_info/7 * @return {Object|Boolean} */ function drupalgap_entity_get_info() { try { if (arguments[0]) { var entity_type = arguments[0]; if (entity_type && drupalgap.entity_info[entity_type]) { return drupalgap.entity_info[entity_type]; } else { return false; } } return drupalgap.entity_info; } catch (error) { console.log('drupalgap_entity_get_info - ' + error); } } /** * @deprecated Since 7.x-1.7-alpha you should use entity_primary_key() instead. * Given an entity type, this returns the primary key identifier for it. * @param {String} entity_type * @return {String} */ function drupalgap_entity_get_primary_key(entity_type) { try { console.log( 'WARNING: drupalgap_entity_get_primary_key() is deprecated! ' + 'Use entity_primary_key() instead.' ); return entity_primary_key(entity_type); } catch (error) { console.log('drupalgap_entity_get_primary_key - ' + error); } } /** * Given an entity type, an entity id and a mode, this will return a render * object for the entity's page container. * @param {String} entity_type * @param {Number} entity_id * @param {String} mode * @return {Object} */ function _drupalgap_entity_page_container(entity_type, entity_id, mode) { try { var id = _drupalgap_entity_page_container_id(entity_type, entity_id, mode); var attrs = { id: id, 'class': entity_type + ' ' + entity_type + '-' + mode }; return { markup: '
    ' }; } catch (error) { console.log('_drupalgap_entity_page_container - ' + error); } } /** * Given an entity type, an entity id, and a mode, this will return the unique * id to be used for the entity's page container. * @param {String} entity_type * @param {Number} entity_id * @param {String} mode * @return {String} */ function _drupalgap_entity_page_container_id(entity_type, entity_id, mode) { return entity_type + '_' + entity_id + '_' + mode + '_container'; } /** * Given an entity type, id, mode and page build, this will render the page * build and inject it into the container on the page. * @param {String} entity_type * @param {Number} entity_id * @param {String} mode * @param {Object} build */ function _drupalgap_entity_page_container_inject(entity_type, entity_id, mode, build) { try { // Get the container id, set the drupalgap.output to the page build, then // inject the rendered page into the container. var id = _drupalgap_entity_page_container_id(entity_type, entity_id, mode); module_invoke_all('entity_view_alter', entity_type, entity_id, mode, build); drupalgap.output = build; $('#' + id).html(drupalgap_render_page()).trigger('create'); _drupalgap_entity_page_add_css_class_names(entity_type, entity_id, build); } catch (error) { console.log('_drupalgap_entity_page_container_inject - ' + error); } } /** * An internal function used to add css class names to an entity's jQM page container. * @param {String} entity_type * @param {Number} entity_id * @param {Object} build * @private */ function _drupalgap_entity_page_add_css_class_names(entity_type, entity_id, build) { try { var className = entity_type; var bundleName = entity_get_bundle(entity_type, build[entity_type]); if (bundleName) { className += '-' + bundleName; } className += ' ' + entity_type.replace(/_/g, '-') + '-' + entity_id; $('#' + drupalgap_get_page_id()).addClass(className); } catch (error) { console.log('_drupalgap_entity_page_add_css_class_names - ' + error); } } /** * The page callback for entity edit forms. * @param {String} form_id * @param {String} entity_type * @param {Number} entity_id * @return {Object} */ function entity_page_edit(form_id, entity_type, entity_id) { try { var content = { container: _drupalgap_entity_page_container( entity_type, entity_id, 'edit' ) }; return content; } catch (error) { console.log('entity_page_edit - ' + error); } } /** * The pageshow callback for entity edit forms. * @param {String} form_id * @param {String} entity_type * @param {Number} entity_id */ function entity_page_edit_pageshow(form_id, entity_type, entity_id) { try { entity_load(entity_type, entity_id, { success: function(entity) { _drupalgap_entity_page_container_inject( entity_type, entity_id, 'edit', drupalgap_get_form(form_id, entity) ); } }); } catch (error) { console.log('entity_page_edit_pageshow - ' + error); } } /** * Returns an entity type's primary title key. * @param {String} entity_type * @return {String} */ function entity_primary_key_title(entity_type) { try { var key; switch (entity_type) { case 'comment': key = 'subject'; break; case 'file': key = 'filename'; break; case 'node': key = 'title'; break; case 'taxonomy_term': key = 'name'; break; case 'taxonomy_vocabulary': key = 'name'; break; case 'user': key = 'name'; break; default: console.log( 'entity_primary_key_title - unsupported entity type (' + entity_type + ')' ); break; } return key; } catch (error) { console.log('entity_primary_key_title - ' + error); } } /** * Implements hook_services_request_pre_postprocess_alter(). * @param {Object} options * @param {*} result */ function entity_services_request_pre_postprocess_alter(options, result) { try { // If we're retrieving an entity, render the entity's content, if it isn't // already set. if ( options.resource == 'retrieve' && in_array(options.service, entity_types() )) { // @TODO - does this condition ever evaluate to true? if (typeof result.content !== 'undefined') { return; } drupalgap_entity_render_content(options.service, result); } // If we're indexing comments, render its content, if it isn't already set. else if (options.service == 'comment' && options.resource == 'index') { for (var index in result) { if (!result.hasOwnProperty(index)) { continue; } var object = result[index]; // @TODO - does this condition ever evaluate to true? if (typeof object.content !== 'undefined') { continue; } drupalgap_entity_render_content(options.service, result[index]); } } } catch (error) { console.log('entity_services_request_pre_postprocess_alter - ' + error); } } /** * Given a field name, this will return its field info or null if it doesn't exist. * @param {String} field_name * @return {Object} */ function drupalgap_field_info_field(field_name) { return drupalgap.field_info_fields && drupalgap.field_info_fields[field_name] ? drupalgap.field_info_fields[field_name] : null; } /** * Returns info on all fields or null if they don't exist. * @return {Object} */ function drupalgap_field_info_fields() { return drupalgap.field_info_fields ? drupalgap.field_info_fields : null; } /** * Given an entity type, field name, and bundle name this will return a JSON * object with data for the specified field name. * @param {String} entity_type * @param {String} field_name * @param {String} bundle_name * @return {Object} */ function drupalgap_field_info_instance(entity_type, field_name, bundle_name) { try { var instances = drupalgap_field_info_instances(entity_type, bundle_name); var warningPrefix = 'WARNING: drupalgap_field_info_instance - '; if (!instances) { console.log( warningPrefix + 'instance was null for entity (' + entity_type + ') bundle (' + bundle_name + ') using field (' + field_name + ')' ); return null; } if (!instances[field_name]) { console.log( warningPrefix + '"' + field_name + '" does not exist for entity (' + entity_type + ') bundle (' + bundle_name + ')' ); return null; } return instances[field_name]; } catch (error) { console.log('drupalgap_field_info_instance - ' + error); } } /** * Given an entity type and/or a bundle name, this returns the field info * instances for the entity or the bundle. * @param {String} entity_type * @param {String} bundle_name * @return {Object} */ function drupalgap_field_info_instances(entity_type, bundle_name) { try { var instances = drupalgap.field_info_instances; if (!instances) { return null; } var field_info_instances = null; // If there is no bundle, pull the fields out of the wrapper. // @TODO there appears to be a special case with commerce_products, in that // they aren't wrapped like normal entities (see the else statement when a // bundle name isn't present). Or do we have a bug here, and we shouldn't // be expecting the wrapper in the first place? if (!bundle_name) { field_info_instances = entity_type == 'commerce_product' ? instances[entity_type] : instances[entity_type][entity_type]; } else if (typeof instances[entity_type] !== 'undefined') { field_info_instances = instances[entity_type][bundle_name]; } return field_info_instances; } catch (error) { console.log('drupalgap_field_info_instances - ' + error); } } /** * Given an entity type, bundle, form and entity, this will add the * entity's fields to the given form. * @param {String} entity_type * @param {String} bundle * @param {Object} form * @param {Object} entity */ function drupalgap_field_info_instances_add_to_form(entity_type, bundle, form, entity) { try { // Grab the field info instances for this entity type and bundle. var fields = drupalgap_field_info_instances(entity_type, bundle); // If there is no bundle, pull the fields out of the wrapper. //if (!bundle) { fields = fields[entity_type]; } // Use the default language, unless the entity has one specified. var language = language_default(); if (entity && entity.language) { language = entity.language; } // Iterate over each field in the entity and add it to the form. If there is // a value present in the entity, then set the field's form element default // value equal to the field value. if (fields) { for (var name in fields) { if (!fields.hasOwnProperty(name)) { continue; } var field = fields[name]; // The user registration form is a special case, in that we only want // to place fields that are set to display on the user registration // form. Skip any fields not set to display. if (form.id == 'user_register_form' && !field.settings.user_register_form) { continue; } var field_info = drupalgap_field_info_field(name); if (field_info) { form.elements[name] = { type: field_info.type, title: field.label, required: field.required, description: field.description }; var default_value = field.default_value; var cardinality = parseInt(field_info.cardinality); if (cardinality == -1) { cardinality = 1; // we'll just add one element for now, until we // figure out how to handle the 'add another // item' feature. } if (entity && entity[name] && entity[name].length != 0) { // Make sure the field has some type of language code on it, or just skip it. An entity will sometimes have // a language code that a field doesn't have, so fall back to und on the field if the language code isn't // present. if (!entity[name][language]) { if (!entity[name].und) { continue; } language = 'und'; } if (!form.elements[name][language]) { form.elements[name][language] = {}; } for (var delta = 0; delta < cardinality; delta++) { // @TODO - is this where we need to use the idea of the // value_callback property present in Drupal's FAPI? That way // each element knows how to map the entity data to its element // value property. if ( entity[name][language][delta] && typeof entity[name][language][delta].value !== 'undefined' ) { default_value = entity[name][language][delta].value; } // If the default_value is null, set it to an empty string. if (default_value == null) { default_value = ''; } // Note, not all fields have a language code to use here, e.g. taxonomy term reference fields do not. form.elements[name][language][delta] = { value: default_value }; // Place the field item onto the element. if (entity[name][language][delta]) { form.elements[name][language][delta].item = entity[name][language][delta]; } } // Set the language back to the entity's language in case it was temporarily changed because of an un // translated field. if (entity && entity.language) { language = entity.language; } } // Give module's a chance to alter their own element during the form // build, that way element properties will be saved to local storage // and then available during hook_field_widget_form() and the form // submission process. var fn = field.widget.module + '_field_info_instance_add_to_form'; if (function_exists(fn)) { window[fn](entity_type, bundle, form, entity, form.elements[name]); } } } } } catch (error) { console.log('drupalgap_field_info_instances_add_to_form - ' + error); } } /** * Given a field name, this will return the key that should be used when * setting its value on an entity. If the field name is not a field, it returns * false. * @param {String} field_name * @return {String} */ function drupalgap_field_key(field_name) { try { // Determine the key to use for the value. By default, most fields // use 'value' as the key. var key = false; var field_info = drupalgap_field_info_field(field_name); if (field_info) { key = 'value'; // Images use fid as the key. if (field_info.module == 'image' && field_info.type == 'image') { key = 'fid'; } else if ( field_info.module == 'taxonomy' && field_info.type == 'taxonomy_term_reference' ) { key = 'tid'; } } return key; } catch (error) { console.log('drupalgap_field_key - ' + error); } } /** * Implements hook_field_formatter_view(). * @param {String} entity_type * @param {Object} entity * @param {Object} field * @param {Object} instance * @param {String} langcode * @param {Object} items * @param {*} display * @return {Object} */ function list_field_formatter_view(entity_type, entity, field, instance, langcode, items, display) { try { var element = {}; if (!empty(items)) { for (var delta in items) { if (!items.hasOwnProperty(delta)) { continue; } var item = items[delta]; var markup = ''; // list_default or list_key if (display.type == 'list_default') { markup = instance.settings.allowed_values[item.value]; // Single on/off checkboxes need an empty space as their markup so // just the label gets rendered. if ( instance.type == 'list_boolean' && field.widget.type == 'options_onoff' ) { markup = ' '; } } else { markup = item.value; } element[delta] = { markup: markup }; } } return element; } catch (error) { console.log('list_field_formatter_view - ' + error); } } /** * Implements hook_assemble_form_state_into_field(). * @param {Object} entity_type * @param {String} bundle * @param {String} form_state_value * @param {Object} field * @param {Object} instance * @param {String} langcode * @param {Number} delta * @param {Object} field_key * * @return {*} */ function list_assemble_form_state_into_field(entity_type, bundle, form_state_value, field, instance, langcode, delta, field_key) { try { var result = form_state_value; switch (field.type) { case 'list_boolean': // For single on/off checkboxes, if the checkbox is unchecked, then we // send a null value on the language code. We know the checkbox is // unchecked if the form_state_value is equal to the first allowed value // on the field. if (instance.widget.type == 'options_onoff') { var index = 0; var on = true; for (var value in field.settings.allowed_values) { if (!field.settings.allowed_values.hasOwnProperty(value)) { continue; } var label = field.settings.allowed_values[value]; if (form_state_value == value && index == 0) { on = false; break; } index++; } if (!on) { field_key.use_delta = false; field_key.use_wrapper = false; result = null; } } else { console.log( 'WARNING: list_assemble_form_state_into_field - unknown widget (' + field.type + ') on list_boolean' ); } break; case 'list_text': // For radio buttons on the user entity form, field values must be // "flattened", i.e. this field_foo: { und: [ { value: 123 }]}, should // be turned into field_foo: { und: 123 } if ( entity_type == 'user' && instance.widget.type == 'options_buttons' ) { field_key.use_delta = false; field_key.use_wrapper = false; } break; default: console.log( 'WARNING: list_assemble_form_state_into_field - unknown type (' + field.type + ')' ); break; } return result; } catch (error) { console.log('list_assemble_form_state_into_field - ' + error); } } /** * Implements hook_views_exposed_filter(). * @param {Object} form * @param {Object} form_state * @param {Object} element * @param {Object} filter * @param {Object} field */ function list_views_exposed_filter(form, form_state, element, filter, field) { try { //console.log('list_views_exposed_filter'); //console.log(form); //console.log(form_state); //console.log(element); //console.log(filter); //console.log(field); var widget = filter.options.group_info.widget; // List fields. if (widget == 'select') { // Set the element value if we have one in the filter. if (!empty(filter.value)) { element.value = filter.value[0]; } // Set the options, then depending on whether or not it is required, set // the default value accordingly. element.options = filter.value_options; if (!element.required) { element.options['All'] = '- ' + t('Any') + ' -'; if (typeof element.value === 'undefined') { element.value = 'All'; } } } else { console.log('WARNING: list_views_exposed_filter - unsupported widget:' + widget); } } catch (error) { console.log('list_views_exposed_filter - ' + error); } } /** * Implements hook_field_formatter_view(). * @param {String} entity_type * @param {Object} entity * @param {Object} field * @param {Object} instance * @param {String} langcode * @param {Object} items * @param {*} display * @return {Object} */ function number_field_formatter_view(entity_type, entity, field, instance, langcode, items, display) { try { var element = {}; // If items is a string, convert it into a single item JSON object. if (typeof items === 'string') { items = {0: {value: items}}; } if (!empty(items)) { var prefix = ''; if (!empty(field.settings.prefix)) { prefix = field.settings.prefix; } var suffix = ''; if (!empty(field.settings.suffix)) { suffix = field.settings.suffix; } for (var delta in items) { if (!items.hasOwnProperty(delta)) { continue; } var item = items[delta]; element[delta] = { markup: prefix + item.value + suffix }; } } return element; } catch (error) { console.log('number_field_formatter_view - ' + error); } } /** * Implements hook_field_widget_form(). * @param {Object} form * @param {Object} form_state * @param {Object} field * @param {Object} instance * @param {String} langcode * @param {Object} items * @param {Number} delta * @param {Object} element */ function number_field_widget_form(form, form_state, field, instance, langcode, items, delta, element) { try { switch (element.type) { case 'number_integer': case 'number_float': case 'number_decimal': case 'range': // Change the form element into a number, unless we're using a range // slider. Then set its min/max attributes along with the step. if (element.type != 'range') { items[delta].type = 'number'; } if (!empty(instance.settings.max)) { items[delta].options.attributes['min'] = instance.settings.min; } if (!empty(instance.settings.max)) { items[delta].options.attributes['max'] = instance.settings.max; } var step = 1; if (element.type == 'number_float') { step = 0.01; } if (element.type == 'number_decimal') { step = 0.01; } items[delta].options.attributes['step'] = step; break; default: console.log( 'number_field_widget_form - element type not supported (' + element.type + ')' ); break; } } catch (error) { console.log('number_field_widget_form - ' + error); } } /** * Implements hook_field_widget_form(). * @param {Object} form * @param {Object} form_state * @param {Object} field * @param {Object} instance * @param {String} langcode * @param {Object} items * @param {Number} delta * @param {Object} element * @return {*} */ function options_field_widget_form(form, form_state, field, instance, langcode, items, delta, element) { try { switch (element.type) { case 'checkbox': // If the checkbox has a default value of 1, check the box. if (items[delta].default_value == 1) { items[delta].checked = true; } break; case 'radios': break; case 'list_boolean': switch (instance.widget.type) { case 'options_onoff': // Switch an on/off boolean to a checkbox and place its on/off // values as attributes. Depending on the allowed values, we may // have to iterate over an array, or an object to get the on/off // values. items[delta].type = 'checkbox'; var off = null; var on = null; if ($.isArray(field.settings.allowed_values)) { for (var key in field.settings.allowed_values) { if (off === null) { off = key; } else { on = key; } } } else { for (var value in field.settings.allowed_values) { if (!field.settings.allowed_values.hasOwnProperty(value)) { continue; } var label = field.settings.allowed_values[value]; if (off === null) { off = value; } else { on = value; } } } items[delta].options.attributes['off'] = off; items[delta].options.attributes['on'] = on; // If the value equals the on value, then check the box. if ( typeof items[delta] !== 'undefined' && items[delta].value == on ) { items[delta].options.attributes['checked'] = 'checked'; } break; default: console.log( 'WARNING: options_field_widget_form list_boolean with ' + 'unsupported type (' + instance.widget.type + ')' ); break; } break; case 'select': case 'list_text': case 'list_float': case 'list_integer': if (instance) { switch (instance.widget.type) { case 'options_select': items[delta].type = 'select'; // If the select list is required, add a 'Select' option and set // it as the default. If it is optional, place a "none" option // for the user to choose from. var text = '- None -'; if (items[delta].required) { text = '- ' + t('Select a value') + ' -'; } items[delta].options[''] = text; if (empty(items[delta].value)) { items[delta].value = ''; } // If more than one value is allowed, turn it into a multiple // select list. if (field.cardinality != 1) { items[delta].options.attributes['data-native-menu'] = 'false'; items[delta].options.attributes['multiple'] = 'multiple'; } break; case 'options_buttons': // If there is one value allowed, we turn this into radio // button(s), otherwise they will become checkboxes. var type = 'checkboxes'; if (field.cardinality == 1) { type = 'radios'; } items[delta].type = type; break; default: console.log( 'WARNING: options_field_widget_form - unsupported widget (' + instance.widget.type + ')' ); return false; break; } // If there are any allowed values, place them on the options // list. Then check for a default value, and set it if necessary. if (field && field.settings.allowed_values) { for (var key in field.settings.allowed_values) { if (!field.settings.allowed_values.hasOwnProperty(key)) { continue; } var value = field.settings.allowed_values[key]; // Don't place values that are objects onto the options // (i.e. commerce taxonomy term reference fields). if (typeof value === 'object') { continue; } // If the value already exists in the options, then someone // else has populated the list (e.g. commerce), so don't do // any processing. if (typeof items[delta].options[key] !== 'undefined') { break; } // Set the key and value for the option. items[delta].options[key] = value; } if (instance.default_value && instance.default_value[delta] && typeof instance.default_value[delta].value !== 'undefined') { items[delta].value = instance.default_value[delta].value; if (items[delta].required) { delete items[delta].options['']; } if (items[delta].item && typeof items[delta].item.value !== 'undefined') { items[delta].value = items[delta].item.value; } } } } break; case 'taxonomy_term_reference': // Change the item type to a hidden input. items[delta].type = 'hidden'; // What vocabulary are we using? var machine_name = field.settings.allowed_values[0].vocabulary; var taxonomy_vocabulary = taxonomy_vocabulary_machine_name_load(machine_name); var widget_type = false; if (instance.widget.type == 'options_select') { widget_type = 'select'; } else { console.log( 'WARNING: options_field_widget_form() - ' + instance.widget.type + ' not yet supported for ' + element.type + ' form elements!' ); return false; } var widget_id = items[delta].id + '-' + widget_type; // If the select list is required, add a 'Select' option and set // it as the default. If it is optional, place a "none" option // for the user to choose from. var text = '- ' + t('None') + ' -'; if (items[delta].required) { text = '- ' + t('Select a value') + ' -'; } items[delta].children.push({ type: widget_type, attributes: { id: widget_id, onchange: "_theme_taxonomy_term_reference_onchange(this, '" + items[delta].id + "');" }, options: { '': text } }); // Attach a pageshow handler to the current page that will load the // terms into the widget. var options = { 'page_id': drupalgap_get_page_id(drupalgap_path_get()), 'jqm_page_event': 'pageshow', 'jqm_page_event_callback': '_theme_taxonomy_term_reference_load_items', 'jqm_page_event_args': JSON.stringify({ 'taxonomy_vocabulary': taxonomy_vocabulary, 'widget_id': widget_id }) }; // Pass the field name so the page event handler can be called for // each item. items[delta].children.push({ markup: drupalgap_jqm_page_event_script_code( options, field.field_name ) }); break; default: var msg = 'options_field_widget_form - unknown widget type: ' + element.type; console.log(msg); break; } } catch (error) { console.log('options_field_widget_form - ' + error); } } /** * Implements hook_field_formatter_view(). * @param {String} entity_type * @param {Object} entity * @param {Object} field * @param {Object} instance * @param {String} langcode * @param {Object} items * @param {*} display * @return {Object} */ function text_field_formatter_view(entity_type, entity, field, instance, langcode, items, display) { try { var element = {}; if (items.length) { for (var delta in items) { if (!items.hasOwnProperty(delta)) { continue; } // Grab the item, then grab the field value or its safe_value if we have it. var item = items[delta]; var value = typeof item.safe_value !== 'undefined' ? item.safe_value : item.value; // Any trim? if (display.type == 'text_summary_or_trimmed') { var length = display.settings.trim_length; value = value.length > length ? value.substring(0, length - 3) + "..." : value.substring(0, length); } element[delta] = { markup: value }; } } return element; } catch (error) { console.log('text_field_formatter_view - ' + error); } } /** * Implements hook_field_widget_form(). * @param {Object} form * @param {Object} form_state * @param {Object} field * @param {Object} instance * @param {String} langcode * @param {Object} items * @param {Number} delta * @param {Object} element */ function text_field_widget_form(form, form_state, field, instance, langcode, items, delta, element) { try { // Determine the widget type, then set the delta item's type property. var type = null; switch (element.type) { case 'search': type = 'search'; break; case 'text': type = 'textfield'; break; case 'textarea': case 'text_long': case 'text_with_summary': case 'text_textarea': type = 'textarea'; break; } items[delta].type = type; // If the item has a value and its attribute value hasn't yet been set, then set the attribute value. if ( typeof items[delta].value !== 'undefined' && typeof items[delta].options.attributes.value === 'undefined' ) { items[delta].options.attributes.value = items[delta].value; } } catch (error) { console.log('text_field_widget_form - ' + error); } } /** * Implements hook_field_formatter_view(). */ function file_field_formatter_view(entity_type, entity, field, instance, langcode, items, display) { try { //console.log(entity_type); //console.log(entity); //console.log(field); //console.log(instance); //console.log(langcode); //console.log(items); //console.log(display); // Iterate over each item, and place a widget onto the render array. var content = {}; for (var delta in items) { if (!items.hasOwnProperty(delta)) { continue; } var item = items[delta]; switch (display.type) { case 'file_table': // Instantiate the table only once. if (!content['file_table']) { content['file_table'] = { theme: 'jqm_table', header: [{ data: t('Attachment') }, { data: t('Size') }], rows: [], attributes: { border: 1 } }; } // Build the path to the file. var file_path = drupalgap_image_path(item.uri); // Android can't open .pdf files in the in app browser, so have // Google Docs open it instead. if (item.filemime == 'application/pdf' && typeof device !== 'undefined' && device.platform == 'Android' ) { file_path = 'https://docs.google.com/gview?embedded=true&url=' + file_path; } // Add the row to the table. content.file_table.rows.push([ l(item.filename, file_path, { InAppBrowser: true }), Math.round(item.filesize/1000).toFixed(2) + ' KB' ]); break; default: console.log('file_field_formatter_view() - unsupported display type: ' + display.type); break; } } return content; } catch (error) { console.log('file_field_formatter_view - ' + error); } } /** * Implements hook_field_formatter_view(). */ function file_entity_field_formatter_view(entity_type, entity, field, instance, langcode, items, display) { try { //console.log(entity_type); //console.log(entity); //console.log(field); //console.log(instance); //console.log(langcode); //console.log(items); //console.log(display); // Special case for media module. if (display.type == 'file_rendered') { return media_field_formatter_view(entity_type, entity, field, instance, langcode, items, display); } // Iterate over each item, and place a widget onto the render array. var content = {}; for (var delta in items) { if (!items.hasOwnProperty(delta)) { continue; } var item = items[delta]; switch (display.type) { default: console.log('file_entity_field_formatter_view() - unsupported display type: ' + display.type); break; } } return content; } catch (error) { console.log('file_entity_field_formatter_view - ' + error); } } // Holds onto the phonegap getPicture success image data. It is keyed by field // name, then delta value. var image_phonegap_camera_options = {}; /** * Implements hook_field_formatter_view(). /** * Implements hook_field_formatter_view(). * @param {String} entity_type * @param {Object} entity * @param {Object} field * @param {Object} instance * @param {String} langcode * @param {Object} items * @param {*} display * @return {Object} */ function image_field_formatter_view(entity_type, entity, field, instance, langcode, items, display) { try { var element = {}; // Toss on the default image if we one is specified and we have no items. // "In addition, any code which programmatically generates a link to an // image derivative without using the standard image_style_url() API // function will no longer work correctly if the image does not already // exist in the file system, since the necessary token will not be present // in the URL." @see http://drupal.stackexchange.com/a/76827/10645 if (empty(items) && instance.settings.default_image) { items = [{ uri: instance.settings.default_image_uri }]; } if (!empty(items)) { for (var delta in items) { if (!items.hasOwnProperty(delta)) { continue; } var item = items[delta]; var theme = empty(display.settings.image_style) ? 'image' : 'image_style'; var image = { theme: theme, alt: item.alt, title: item.title }; if (theme == 'image_style') { image.style_name = display.settings.image_style; image.path = item.uri; } else { image.path = drupalgap_image_path(item.uri); } element[delta] = image; } } return element; } catch (error) { console.log('image_field_formatter_view - ' + error); } } /** * Implements hook_field_widget_form(). * @param {Object} form * @param {Object} form_state * @param {Object} field * @param {Object} instance * @param {String} langcode * @param {Object} items * @param {Number} delta * @param {Object} element */ function image_field_widget_form(form, form_state, field, instance, langcode, items, delta, element) { try { // Change the item type to a hidden input to hold the file id. items[delta].type = 'hidden'; // If we're dealing with the user profile 'picture' it isn't a real field, // so we need to spoof some field settings to get the widget to render // properly. // @TODO the field label doesn't show up. if (form.id == 'user_profile_form' && element.name == 'picture') { field = { field_name: 'picture' }; } // If we already have an image for this item, show it. if (typeof items[delta].item !== 'undefined' && items[delta].item.fid) { // Set the hidden input's value equal to the file id. items[delta].value = items[delta].item.fid; // Show the image on the form, the file name as a link to the actual file, // the file size, and a remove button. var path = drupalgap_image_path(items[delta].item.uri); // @TODO - show the filesize. // @TODO - show the remove button. var html = theme('image', { path: path }) + '
    ' + l(items[delta].item.filename, path, { InAppBrowser: true }) + '
    '; /*theme('button_link', { text: 'Remove', path: null, attributes: { onclick: "_image_field_widget_form_remove_image()", 'data-icon': 'delete', 'data-iconpos': 'right' } });*/ //'
    (' + items[delta].item.filesize + ')
    '; // Add html to the item's children. items[delta].children.push({markup: html}); return; // No further processing required. } // Set the default button text, and if a value was provided, // overwrite the button text. var button_text = t('Take Photo'); if (items[delta].value) { button_text = items[delta].value; } var browse_button_text = t('Browse'); if (items[delta].value2) { browse_button_text = items[delta].value2; } // Place variables into document for PhoneGap image processing. var item_id_base = items[delta].id.replace(/-/g, '_'); var image_field_source = item_id_base + '_imagefield_source'; var imagefield_destination_type = item_id_base + '_imagefield_destination_type'; var imagefield_data = item_id_base + '_imagefield_data'; eval('var ' + image_field_source + ' = null;'); eval('var ' + imagefield_destination_type + ' = null;'); eval('var ' + imagefield_data + ' = null;'); // Build an imagefield widget with PhoneGap. Contains a message // div, an image item, a button to add an image, and a button to browse for // images. var browse_button_id = items[delta].id + '-browse-button'; var html = '
    ' + '
    ' + '' + '' + button_text + '' + '' + browse_button_text + '' + '
    '; // Open extra javascript declaration. html += ''; // Add html to the item's children. items[delta].children.push({markup: html}); } catch (error) { console.log('image_field_widget_form - ' + error); } } /** * On an entity edit form, this removes an image file from the server, then from * the form elements and user interface. */ function _image_field_widget_form_remove_image() { try { alert('_image_field_widget_form_remove_image'); } catch (error) { console.log('_image_field_widget_form_remove_image - ' + error); } } /** * Given an entity type and optional bundle name, this will return an array * containing any image field names present, false otherwise. * @param {String} entity_type * @param {String} bundle * @return {Object} */ function image_fields_present_on_entity_type(entity_type, bundle) { try { var results = []; var fields = drupalgap_field_info_instances(entity_type, bundle); if (!fields) { return false; } for (var name in fields) { if (!fields.hasOwnProperty(name)) { continue; } var field = fields[name]; if ( field.widget && field.widget.type && field.widget.type == 'image_image' ) { results.push(name); } } if (results.length == 0) { return false; } return results; } catch (error) { console.log('image_fields_present_on_entity_type - ' + error); } } /** * Implements hook_form_alter(). * @param {Object} form * @param {Object} form_state * @param {String} form_id */ function image_form_alter(form, form_state, form_id) { try { // Make potential alterations to any entity edit form that has an image // field element(s). if (form.entity_type) { var bundle = form.bundle; var image_fields = image_fields_present_on_entity_type(form.entity_type, bundle); if (image_fields) { // Attach the image field names to the form for later reference. form.image_fields = image_fields; // For each image field, create a place for it in the global var. if ($.isArray(image_fields)) { for (var index in image_fields) { if (!image_fields.hasOwnProperty(index)) { continue; } var name = image_fields[index]; image_phonegap_camera_options[name] = { 0: null }; } } } } } catch (error) { console.log('image_form_alter - ' + error); } } /** * Given an image style name and image uri, this will return the absolute URL * that can be used as a src value for an img element. * @param {String} style_name * @param {String} path * @return {String} */ function image_style_url(style_name, path) { try { // @TODO - bug: the trailing slash on public and private is breaking images // that don't live in a sub directory in sites/default/files. var src = Drupal.settings.site_path + Drupal.settings.base_path + path; if (src.indexOf('public://') != -1) { src = src.replace( 'public://', Drupal.settings.file_public_path + '/styles/' + style_name + '/public/' ); } else if (src.indexOf('private://') != -1) { src = src.replace( 'private://', Drupal.settings.file_private_path + '/styles/' + style_name + '/private/' ); } return src; } catch (error) { console.log('image_style_url - ' + error); } } /** * The success callback function used when handling PhoneGap's camera * getPicture() call. * @param {Object} options */ function _image_phonegap_camera_getPicture_success(options) { try { // Hold on to the image options in the global var. image_phonegap_camera_options[options.field_name] = {0: options}; // Hide the 'Add image' button and show the 'Upload' button. //$('#' + options.id + '-button').hide(); //$('#' + options.id + '_upload').show(); // Show the captured photo as a thumbnail. When the photo is loaded, resize // it to fit the content area, then show it. var image_element_id = options.id + '-imagefield'; var image = document.getElementById(image_element_id); image.src = 'data:image/jpeg;base64,' + image_phonegap_camera_options[options.field_name][0].image; image.onload = function() { var width = this.width; var height = this.height; var ratio = width / drupalgap_max_width(); var new_width = width / ratio; var new_height = height / ratio; image.width = new_width; image.height = new_height; $('#' + image_element_id).show(); }; } catch (error) { console.log('_image_phonegap_camera_getPicture_success - ' + error); } } /** * An internal function used to upload images to the server, retreive their file * id and then populate the corresponding form element's value with the file id. * @param {Object} form * @param {Object} form_state * @param {Object} options */ function _image_field_form_process(form, form_state, options) { try { // @TODO needs mutli value field support (delta) // @see https://www.drupal.org/node/2224803 var lng = language_default(); var processed_an_image = false; // For each image field on the form... for (var index in form.image_fields) { if (!form.image_fields.hasOwnProperty(index)) { continue; } var name = form.image_fields[index]; // Skip empty images and ones that already have their field id set. if (!image_phonegap_camera_options[name][0] || form_state.values[name][lng][0] != '') { continue; } // Create a unique file name using the UTC integer value. var d = new Date(); var image_file_name = Drupal.user.uid + '_' + d.valueOf() + '.jpg'; // Build the data for the file create resource. If it's private, adjust the filepath. var image_file_path = form.elements[name].field_info_instance.settings.file_directory; if (image_file_path !== "") { image_file_path += "/"; } var file = { file: { file: image_phonegap_camera_options[name][0].image, filename: image_file_name, filepath: 'public://' + image_file_path + image_file_name } }; if (!empty(Drupal.settings.file_private_path)) { file.file.filepath = 'private://' + image_file_path + image_file_name; } // Change the loader mode to saving, and save the file. drupalgap.loader = 'saving'; processed_an_image = true; file_save(file, { async: false, success: function(result) { try { // Set the hidden input and form state values with the file id. var element_id = drupalgap_form_get_element_id(name, form.id); $('#' + element_id).val(result.fid); form_state.values[name][lng][0] = result.fid; if (options.success) { options.success(); } } catch (error) { console.log('_image_field_form_process - success - ' + error); } } }); } // If no images were processed, we need to continue onward anyway. if (!processed_an_image && options.success) { options.success(); } } catch (error) { console.log('_image_field_form_validate - ' + error); } } /** * Implements hook_assemble_form_state_into_field(). * @param {Object} entity_type * @param {String} bundle * @param {String} form_state_value * @param {Object} field * @param {Object} instance * @param {String} langcode * @param {Number} delta * @param {Object} field_key * @return {*} */ function image_assemble_form_state_into_field(entity_type, bundle, form_state_value, field, instance, langcode, delta, field_key) { try { field_key.value = 'fid'; return form_state_value; } catch (error) { console.log('image_assemble_form_state_into_field - ' + error); } } /** * Implements hook_block_view(). * @param {String} delta * @param {Object} region * @return {String} */ function menu_block_view(delta, region) { // NOTE: When rendering a jQM data-role="navbar" you can't place an // empty list () in it, this will cause an error: // https://github.com/jquery/jquery-mobile/issues/5141 // So we must check to make sure we have any items before rendering the // menu since our theme_item_list implementation returns empty lists // for jQM pageshow async list item data retrieval and display. try { // Load the menu. var menu = drupalgap.menus[delta]; // Since menu link paths may have an 'access_callback' handler that needs // to make an async call to the server (e.g. local tasks), we'll utilize a // pageshow handler to render the menu, so for now just render an empty // placeholder and pageshow handler. var container_id = menu_container_id(delta); var data_role = null; if (region.attributes && region.attributes['data-role']) { data_role = region.attributes['data-role']; } // If the menu is wrapped, check to see if any wrap_options attributes were // attached. If any were provided use them to build the div container // attributes. We will always overwrite the container id though, since it is // generated by the system (and used to dynamically inject the menu html). var container_attributes = {}; if ( typeof menu.options !== 'undefined' && typeof menu.options.wrap !== 'undefined' && menu.options.wrap && menu.options.wrap_options && menu.options.wrap_options.attributes ) { container_attributes = menu.options.wrap_options.attributes; } container_attributes.id = container_id; return '
    ' + drupalgap_jqm_page_event_script_code({ page_id: drupalgap_get_page_id(), jqm_page_event: 'pageshow', jqm_page_event_callback: 'menu_block_view_pageshow', jqm_page_event_args: JSON.stringify({ menu_name: delta, container_id: container_id, 'data-role': data_role }) }, delta); } catch (error) { console.log('menu_block_view - ' + error); } } /** * The pageshow handler for menu blocks. * @param {Object} options */ function menu_block_view_pageshow(options) { try { var html = ''; // Grab current path so we can watch out for any menu links that match it. var path = drupalgap_path_get(); // Are we about to view a normal menu, or the local task menu? var delta = options.menu_name; if (delta == 'primary_local_tasks') { // LOCAL TASKS MENU LINKS // For the current page's router path, grab any local task menu links add // them into the menu. Note, local tasks are located in a menu link item's // children, or its parent's children (including itself), if there are // any. Local tasks typically have argument wildcards in them, so we'll // replace their wildcards with the current args. var router_path = drupalgap_router_path_get(); if (drupalgap.menu_links[router_path]) { // Determine the parent path, if any. var parent = null; if (drupalgap.menu_links[router_path].parent) { parent = drupalgap.menu_links[router_path].parent; } // Then extract the local tasks paths array. var local_tasks = null; if (drupalgap.menu_links[router_path].children) { local_tasks = drupalgap.menu_links[router_path].children; } else if ( parent && drupalgap.menu_links[parent] && drupalgap.menu_links[parent].children ) { local_tasks = drupalgap.menu_links[parent].children; } var args = arg(); // Define a success callback that will be called later on... var _success = function(result) { try { var menu_items = []; var link_path = ''; if (local_tasks && !empty(local_tasks)) { for (var index in local_tasks) { if (!local_tasks.hasOwnProperty(index)) { continue; } var local_task = local_tasks[index]; if (drupalgap.menu_links[local_task] && ( drupalgap.menu_links[local_task].type == 'MENU_DEFAULT_LOCAL_TASK' || drupalgap.menu_links[local_task].type == 'MENU_LOCAL_TASK' )) { if (drupalgap_menu_access(local_task, null, result)) { menu_items.push(drupalgap.menu_links[local_task]); } } } } // If there was only one local task menu item, and it is the default // local task, don't render the menu, otherwise render the menu as // an item list as long as there are items to render. if (menu_items.length == 1 && menu_items[0].type == 'MENU_DEFAULT_LOCAL_TASK' ) { html = ''; } else { var items = []; for (var index in menu_items) { if (!menu_items.hasOwnProperty(index)) { continue; } var item = menu_items[index]; // Make a deep copy of the menu link so we don't modify it. var link = jQuery.extend(true, {}, item); // If there are no link options, set up defaults. if (!link.options) { link.options = { attributes: { } }; } else if (!link.options.attributes) { link.options.attributes = { }; } // If the link points to the current path, set it as active. // We first need to figure out which path to check, by default // use the link path, but if its a default local task, use its // parent path. var path_to_check = link.path; if (link.type == 'MENU_DEFAULT_LOCAL_TASK' && link.parent) { path_to_check = link.parent; link.path = arg(null, link.parent).join('/'); } if (path_to_check == router_path) { if (!link.options.attributes['class']) { link.options.attributes['class'] = ''; } link.options.attributes['class'] += ' ui-btn ui-btn-active ui-state-persist '; } items.push( l( link.title, drupalgap_place_args_in_path(link.path), link.options ) ); } if (items.length > 0) { html = theme('item_list', {'items': items}); } } // Inject the html. $('#' + options.container_id).html(html).trigger('create'); // If the block's region is a jQM navbar, refresh the navbar. if (options['data-role'] && options['data-role'] == 'navbar') { $('#' + options.container_id).navbar(); } // Optionally remove the placeholder wrapper. var menu = drupalgap.menus[options.menu_name]; if ( typeof menu.options !== 'undefined' && (typeof menu.options.wrap === 'undefined' || !menu.options.wrap) ) { $('#' + options.container_id).children().unwrap(); } } catch (error) { console.log('menu_block_view_pageshow - success - ' + error); } }; // First, determine if any child has an entity arg in the path, and/or // an access_callback handler. var has_entity_arg = false; var has_access_callback = false; if (local_tasks) { for (var index in local_tasks) { if (!local_tasks.hasOwnProperty(index)) { continue; } var local_task = local_tasks[index]; if (drupalgap.menu_links[local_task] && ( drupalgap.menu_links[local_task].type == 'MENU_DEFAULT_LOCAL_TASK' || drupalgap.menu_links[local_task].type == 'MENU_LOCAL_TASK' ) ) { if (drupalgap_path_has_entity_arg(arg(null, local_task))) { has_entity_arg = true; } if ( typeof drupalgap.menu_links[local_task].access_callback !== 'undefined' ) { has_access_callback = true; } } } } // If we have an entity arg, and an access_callback, let's load up the // entity asynchronously. if (has_entity_arg && has_access_callback) { var found_int_arg = false; var int_arg_index = null; for (var i = 0; i < args.length; i++) { if (is_int(parseInt(args[i]))) { // Save the arg index so we can replace it later. int_arg_index = i; found_int_arg = true; break; } } if (!found_int_arg) { _success(null); return; } // Determine the naming convention for the entity load function. var load_function_prefix = args[0]; // default if (args[0] == 'taxonomy') { if (args[1] == 'vocabulary' || args[1] == 'term') { load_function_prefix = args[0] + '_' + args[1]; } } var load_function = load_function_prefix + '_load'; // If the load function exists, load the entity. if (function_exists(load_function)) { var entity_fn = window[load_function]; // Load the entity. MVC items need to pass along the module name and // model type to its load function. All other entity load functions // just need the entity id. var entity_id = parseInt(args[int_arg_index]); if (args[0] == 'item') { entity = entity_fn(args[1], args[2], entity_id); _success(entity); } else { // Force a reset if we are editing the entity. var reset = false; if (arg(2) == 'edit') { reset = true; } // Load the entity asynchronously. entity_fn(entity_id, { reset: reset, success: _success }); } } else { console.log('menu_block_view_pageshow - load function not ' + 'implemented! ' + load_function ); } } else { _success(null); } } } else { // ALL OTHER MENU LINKS // If the block's corresponding menu exists, and it has links, iterate // over each link, add it to an items array, then theme an item list. var menu = false; if (drupalgap.menus[delta] && drupalgap.menus[delta].links) { menu = drupalgap.menus[delta]; var items = []; for (var index in menu.links) { if (!menu.links.hasOwnProperty(index)) { continue; } var menu_link = menu.links[index]; // Make a deep copy of the menu link so we don't modify it. var link = jQuery.extend(true, {}, menu_link); // If there are no link options, set up defaults. if (!link.options) { link.options = {attributes: {}}; } else if (!link.options.attributes) { link.options.attributes = {}; } if (!link.options.attributes['class']) { link.options.attributes['class'] = ''; } // Extract the link's class attribute. var class_names = link.options.attributes['class']; // If the link points to the current path, set it as active. if (link.path == path) { if (class_names.indexOf('ui-btn') == -1) { class_names += ' ui-btn'; } if (class_names.indexOf('ui-btn-active') == -1) { class_names += ' ui-btn-active'; } if (class_names.indexOf('ui-state-persist') == -1) { class_names += ' ui-state-persist'; } } // If there was a data-icon attibute on the link, let's add its // equivalent css class name to the link (if it isn't already // present), otherwise jQM won't render the icon properly. Sounds // like a jQM bug. if ( link.options.attributes['data-icon'] && class_names.indexOf(link.options.attributes['data-icon']) == -1 ) { class_names += ' ui-icon-' + link.options.attributes['data-icon'] + ' '; } // Finally toss the class attribute back on the link and add the // link to the items array. link.options.attributes['class'] = class_names + ' '; items.push(l(t(link.title), link.path, link.options)); } if (items.length > 0) { // Pass along any menu attributes. var attributes = null; if (menu.options && menu.options.attributes) { attributes = drupalgap.menus[delta].options.attributes; } html = theme('item_list', {'items': items, 'attributes': attributes}); } } // Inject the html. $('#' + options.container_id).html(html).trigger('create'); // Remove the placeholder wrapper, unless we were instructed not to. var wrap = false; if ( menu && typeof menu.options !== 'undefined' && typeof menu.options.wrap !== 'undefined' && menu.options.wrap ) { wrap = true; } if (!wrap) { $('#' + options.container_id).children().unwrap(); } } } catch (error) { console.log('menu_block_view_pageshow - ' + error); } } /** * Implements hook_install(). */ function menu_install() { try { // Grab the list of system menus and save each. var system_menus = menu_list_system_menus(); for (var menu_name in system_menus) { if (!system_menus.hasOwnProperty(menu_name)) { continue; } var menu = system_menus[menu_name]; menu_save(menu); } } catch (error) { console.log('menu_install - ' + error); } } /** * Returns a JSON object that can be used as default options for a menu object. * @return {Object} */ function menu_popup_get_default_options() { return { attributes: { 'data-role': 'listview' }, wrap: true, wrap_options: { attributes: { 'data-role': 'popup' } } }; } /** * Given a menu region link, this will return its data JSON object, or null if * no data exists. * @param {Object} region_link * @return {*) */ function menu_region_link_get_data(region_link) { try { // Extract the data associated with this link. If it has a 'region' // property then it is coming from a hook_menu, if it doesn't then it // is coming from settings.js. var data = null; if (typeof region_link.region === 'undefined') { data = region_link; // link defined in settings.js // @TODO - we need to warn people that they can't make a custom menu // with a machine name of 'regions' now that this machine name is a // "system" name for rendering links in regions. } // link defined via hook_menu() else { data = region_link.region; } return data; } catch (error) { console.log('menu_region_link_get_data - ' + error); } } /** * Given a menu region link's class name, this will return what side of the ui * it is on, returns left by default, unless it specifically contains the * ui-btn-right class. * @param {String} class_name * @return {String) */ function menu_region_link_get_side(class_name) { try { var side = 'left'; if (class_name.indexOf('ui-btn-right') != -1) { side = 'right'; } return side; } catch (error) { console.log('menu_region_link_get_side - ' + error); } } /** * Given a menu, this adds it to drupalgap.menus. See menu_list_system_menus * for examples of a menu JSON object. * @param {Object} menu */ function menu_save(menu) { try { drupalgap.menus[menu.menu_name] = menu; } catch (error) { console.log('menu_save - ' + error); } } /** * Given a menu name, this will return it from drupalgap.menus, or return null * if it doesn't exist. * @param {String} name * @return {*} */ function menu_load(name) { try { if (typeof drupalgap.menus[name] !== 'undefined') { return drupalgap.menus[name]; } return null; } catch (error) { console.log('menu_load - ' + error); } } /** * Given a menu name, this will return its container id for that page. You may * optionally pass in a page id as the second argument, otherwise it will use * the current page id. * @param {String} menu_name * @return {String} */ function menu_container_id(menu_name) { try { var page_id = null; if (arguments[1]) { page_id = arguments[1]; } else { page_id = drupalgap_get_page_id(); } return page_id + '_menu_' + menu_name; } catch (error) { console.log('menu_container_id - ' + error); } } /** * The page callback for mvc/collection/list/%/%. * @param {String} module * @param {String} type * @return {Object} */ function collection_list_page(module, type) { try { var content = { 'collection_list': { 'theme': 'jqm_item_list', 'title': t('Collection') } }; var items = []; var collection = collection_load(module, type); if (collection) { for (var id in collection) { if (!collection.hasOwnProperty(id)) { continue; } var item = collection[id]; var path = 'mvc/item/' + module + '/' + type + '/' + id; items.push(l(item.name, path)); } content.collection_list.items = items; } return content; } catch (error) { console.log('collection_list_page - ' + error); } } /** * Given a module name and model type, this will return the collection * JSON object. * @param {String} module * @param {String} type * @return {*} */ function collection_load(module, type) { try { return JSON.parse( window.localStorage.getItem( mvc_get_collection_key('collection', module, type) ) ); } catch (error) { console.log('collection_load - ' + error); } } /** * Given a module name, model type and item collection, this will save the * collection JSON object to local storage. * @param {String} module * @param {String} type * @param {Object} collection */ function collection_save(module, type, collection) { try { window.localStorage.setItem( mvc_get_collection_key('collection', module, type), JSON.stringify(collection) ); } catch (error) { console.log('collection_save - ' + error); } } /** * Given a bucket (e.g. collection, settings), module name and mvc model type, * this will return the local storage key used for the model type's item * collection. * @param {String} bucket * @param {String} module * @param {String} model_type * @return {String} */ function mvc_get_collection_key(bucket, module, model_type) { return 'mvc_' + bucket + '_' + module + '_' + model_type; } /** * Implements hook_install(). */ function mvc_install() { try { // Load models... // For each module that implements hook_mvc_model(), iterate over each model // placing it into drupalgap.mvc.models, each model will be placed into // a namespace according to the module that implements it, to avoid // namespace collisions. var modules = module_implements('mvc_model'); for (var i = 0; i < modules.length; i++) { var module = modules[i]; var models = module_invoke(module, 'mvc_model'); if (models) { // Create namespace for model, keyed by module name. if (!drupalgap.mvc.models[module]) { drupalgap.mvc.models[module] = {}; } // For each model type... for (var model_type in models) { if (!models.hasOwnProperty(model_type)) { continue; } var model = models[model_type]; // Set the primary key 'id', the module name, and model type // on the model fields. These are the mvc_model_system_fields(). model.fields.id = { 'type': 'hidden', 'title': t('ID'), 'required': false }; model.fields.module = { 'type': 'hidden', 'title': t('Module'), 'required': true, 'default_value': module }; model.fields.type = { 'type': 'hidden', 'title': t('Model Type'), 'required': true, 'default_value': model_type }; // Add each model type to its namespace within drupalgap.mvc.models drupalgap.mvc.models[module][model_type] = model; // Save an empty collection to local storage for this model type, if // one doesn't already exist. var collection_key = mvc_get_collection_key( 'collection', module, model_type ); if (!window.localStorage.getItem(collection_key)) { window.localStorage.setItem(collection_key, '[]'); // Save settings for the collection to local storage. The auto // increment value represents an item id, we start counting at // zero since the collection is an array. window.localStorage.setItem( mvc_get_collection_key('settings', module, model_type), '{"auto_increment":0}' ); } } } } //console.log(JSON.stringify(drupalgap.mvc.models)); //drupalgap_alert('drupalgap_mvc_init'); // These may not be needed. Perhaps we should just call assumed hooks that // should be implemented for any custom views and controllers at the time // they are needed, probably no need to bundle them inside drupalgap.mvc. //var views = module_invoke_all('mvc_view'); //var controllers = module_invoke_all('mvc_controller'); } catch (error) { console.log('mvc_install - ' + error); } } /** * Implements hook_menu(). * @return {Object} */ function mvc_menu() { var items = { 'mvc/collection/list/%/%': { 'page_callback': 'collection_list_page', 'page_arguments': [3, 4] }, 'mvc/item/%/%/%': { 'page_callback': 'item_view_page', 'page_arguments': [2, 3, 4] }, 'mvc/item-add/%/%': { title: t('Add'), page_callback: 'drupalgap_get_form', page_arguments: ['item_create_form', 2, 3] } }; return items; } /** * Returns an array of system fields (properties) to be used on item JSON * object. * @return {Array} */ function mvc_model_system_fields() { return ['id', 'module', 'type']; } // We'll need developer friendly front end functions, e.g. // model_load(); // model_save(); // model_delete(); // item_load(); // item_save(); // item_delete(); // etc... /** * Given a module name and a corresponding model name, this will load the model * from drupalgap.mvc.models. * @param {String} module * @param {String} name * @return {Object} */ function model_load(module, name) { try { if ( drupalgap.mvc.models[module] === 'undefined' || drupalgap.mvc.models[module][name] === 'undefined' ) { return false; } return drupalgap.mvc.models[module][name]; } catch (error) { console.log('model_load - ' + error); } } /** * Given a module type, and model type, this generates and returns the form JSON * object to create a model item. * @param {Object} form * @param {Object} form_state * @param {String} module * @param {String} type * @return {Object} */ function item_create_form(form, form_state, module, type) { try { //var form = drupalgap_form_defaults('item_create_form'); var model = model_load(module, type); if (model) { // @todo - this could be dangerous just overriding the elements variable, // we should iterate over the model fields and add them one by one // instead. form.elements = model.fields; form.buttons.cancel = drupalgap_form_cancel_button(); form.elements.submit = { type: 'submit', value: t('Create') }; } return form; } catch (error) { console.log('item_create_form - ' + error); } } /** * Handles the submission of an mvc model item creation form. * @param {Object} form * @param {Object} form_state */ function item_create_form_submit(form, form_state) { try { // Save the item and then view it. if (item_save(form_state.values)) { /*var path = 'item/' + form_state.values.module + '/' + form_state.values.type + '/' + form_state.values.id;*/ var path = 'mvc/collection/list/' + form_state.values.module + '/' + form_state.values.type; // If there is a form action path set, use that instead. if (form.action) { path = form.action; } // Go to our destination path, and force a reload on the page. drupalgap_goto(path, {reloadPage: true}); } else { var msg = 'item_create_form_submit - failed to save item!'; drupalgap_alert(msg); } } catch (error) { console.log('item_create_form_submit - ' + error); } } /** * Given a module name, mvc model type, and item id, this will return the item, * or false if the item fails to load. * @param {String} module * @param {String} type * @param {String} id * @return {Object} */ function item_load(module, type, id) { try { var item = false; var collection = collection_load(module, type); if (collection && typeof collection[id] !== 'undefined') { item = collection[id]; } return item; } catch (error) { console.log('item_load - ' + error); } } /** * Given an mvc item, this saves it to local storage. * @param {Object} item * @return {Boolean} */ function item_save(item) { try { if (typeof item === 'undefined') { return false; } // Grab the settings for this collection. var settings_key = mvc_get_collection_key( 'settings', item.module, item.type ); var settings = JSON.parse(window.localStorage.getItem(settings_key)); // If there is no id, then this is a new item, grab the next id to use. if (!item.id) { item.id = settings.auto_increment; } // Load the collection from local storage. var collection = collection_load(item.module, item.type); // Add the item onto the collection. collection[item.id] = item; // Save the collection to local storage. collection_save(item.module, item.type, collection); // Increment to the next item id and save the settings. settings.auto_increment = item.id + 1; window.localStorage.setItem(settings_key, JSON.stringify(settings)); return true; } catch (error) { console.log('item_save - ' + error); } } /** * The page callback for mvc/item/%/%/%. * @param {String} module * @param {String} type * @param {Object} item * @return {String} */ function item_view_page(module, type, item) { try { var html = ''; var model = model_load(module, type); if (model) { html = theme('item', {'model': model, 'item': item}); } return html; } catch (error) { console.log('item_view_page - ' + error); } } /** * Given a node, this determines if the current user has access to it. Returns * true if so, false otherwise. This function implementation is incomplete, use * with caution. * @param {Object} node * @return {Boolean} */ function node_access(node) { try { if ( ( node.uid == Drupal.user.uid && user_access('edit own ' + node.type + ' content') ) || user_access('edit any ' + node.type + ' content') ) { return true; } else { return false; } } catch (error) { console.log('node_access - ' + error); } } /** * Page call back for node/add. * @return {Object} */ function node_add_page() { try { var content = { header: { markup: '

    ' + t('Create Content') + '

    ' }, node_type_listing: { theme: 'jqm_item_list', title: t('Content Types'), attributes: { id: 'node_type_listing_items' } } }; var items = []; var user_permissions = Drupal.user.content_types_user_permissions; for (var type in user_permissions) { if (!user_permissions.hasOwnProperty(type)) { continue; } var permissions = user_permissions[type]; if (permissions.create) { items.push(l(drupalgap.content_types_list[type].name, 'node/add/' + type)); } } content.node_type_listing.items = items; return content; } catch (error) { console.log('node_add_page - ' + error); } } /** * Page call back function for node/add/[type]. * @param {String} type * @return {Object} */ function node_add_page_by_type(type) { try { return drupalgap_get_form('node_edit', {'type': type}); } catch (error) { console.log('node_add_page_by_type - ' + error); } } /** * Title call back function for node/add/[type]. * @param {Function} callback * @param {String} type * @return {Object} */ function node_add_page_by_type_title(callback, type) { try { var title = t('Create') + ' ' + drupalgap.content_types_list[type].name; return callback.call(null, title); } catch (error) { console.log('node_add_page_by_type_title - ' + error); } } /** * The node edit form. * @param {Object} form * @param {Object} form_state * @param {Object} node * @return {Object} */ function node_edit(form, form_state, node) { try { // Setup form defaults. form.entity_type = 'node'; form.bundle = node.type; // Add the entity's core fields to the form. drupalgap_entity_add_core_fields_to_form('node', node.type, form, node); // Add the fields for this content type to the form. drupalgap_field_info_instances_add_to_form('node', node.type, form, node); // Add submit to form. form.elements.submit = { 'type': 'submit', 'value': t('Save') }; // Add cancel button to form. form.buttons['cancel'] = drupalgap_form_cancel_button(); // Add delete button to form if we're editing a node. if (node && node.nid) { form.buttons['delete'] = drupalgap_entity_edit_form_delete_button('node', node.nid); } return form; } catch (error) { console.log('node_edit - ' + error); } } /** * The node edit form's submit function. * @param {Object} form * @param {Object} form_state */ function node_edit_submit(form, form_state) { try { var node = drupalgap_entity_build_from_form_state(form, form_state); drupalgap_entity_form_submit(form, form_state, node); } catch (error) { console.log('node_edit_submit - ' + error); } } /** * Implements hook_menu(). * @return {Object} */ function node_menu() { var items = { 'node': { 'title': t('Content'), 'page_callback': 'node_page', 'pageshow': 'node_page_pageshow' }, 'node/add': { 'title': t('Add content'), 'page_callback': 'node_add_page' }, 'node/add/%': { title: t('Add content'), title_callback: 'node_add_page_by_type_title', title_arguments: [2], page_callback: 'node_add_page_by_type', page_arguments: [2], options: { reloadPage: true } }, 'node/%': { 'title': t('Node'), 'page_callback': 'node_page_view', 'page_arguments': [1], 'pageshow': 'node_page_view_pageshow', 'title_callback': 'node_page_title', 'title_arguments': [1] }, 'node/%/view': { 'title': t('View'), 'type': 'MENU_DEFAULT_LOCAL_TASK', 'weight': -10 }, 'node/%/edit': { 'title': t('Edit'), 'page_callback': 'entity_page_edit', 'pageshow': 'entity_page_edit_pageshow', 'page_arguments': ['node_edit', 'node', 1], 'weight': 0, 'type': 'MENU_LOCAL_TASK', 'access_callback': 'node_access', 'access_arguments': [1], options: {reloadPage: true} } }; return items; } /** * Page callback for node. * @return {Object} */ function node_page() { var content = { 'create_content': { 'theme': 'button_link', 'path': 'node/add', 'text': t('Create Content') }, 'node_listing': { 'theme': 'jqm_item_list', 'title': t('Content List'), 'items': [], 'attributes': {'id': 'node_listing_items'} } }; return content; } /** * The jQM pageshow callback for the node listing page. */ function node_page_pageshow() { try { // Grab some recent content and display it. views_datasource_get_view_result( 'drupalgap/views_datasource/drupalgap_content', { success: function(content) { // Extract the nodes into items, then drop them in the list. var items = []; for (var index in content.nodes) { if (!content.nodes.hasOwnProperty(index)) { continue; } var object = content.nodes[index]; items.push(l(object.node.title, 'node/' + object.node.nid)); } drupalgap_item_list_populate('#node_listing_items', items); } } ); } catch (error) { console.log('node_page_pageshow - ' + error); } } /** * Page callback for node/%. * @param {Number} nid * @return {Object} */ function node_page_view(nid) { try { if (nid) { var content = { container: _drupalgap_entity_page_container('node', nid, 'view') }; return content; } else { drupalgap_error(t('No node id provided!')); } } catch (error) { console.log('node_page_view - ' + error); } } /** * jQM pageshow handler for node/% pages. * @param {Number} nid */ function node_page_view_pageshow(nid) { try { node_load(nid, { success: function(node) { // By this point the node's content has been assembled into an html // string. This is because when a node is retrieved from the server, // we use a services post processor to render its content. // @see entity_services_request_pre_postprocess_alter() // Does anyone want to take over the rendering of this content type? // Any implementors of hook_node_page_view_alter_TYPE()? // @TODO this should probably be moved up to the entity level. var hook = 'node_page_view_alter_' + node.type; var modules = module_implements(hook); if (modules.length > 0) { if (modules.length > 1) { var msg = 'node_page_view_pageshow - WARNING - there is more ' + 'than one module implementing hook_' + hook + '(), we will ' + 'use the first one: ' + modules[0]; console.log(msg); } var function_name = modules[0] + '_' + hook; var fn = window[function_name]; fn(node, { success: function(content) { _drupalgap_entity_page_container_inject( 'node', node.nid, 'view', content ); } }); return; } // Build a done handler which will inject the given build into the page container. If there was a success // callback attached to the page options call it. var done = function(build) { _drupalgap_entity_page_container_inject( 'node', node.nid, 'view', build ); if (drupalgap.page.options.success) { drupalgap.page.options.success(node); } }; // Figure out the title, and watch for translation. var default_language = language_default(); var node_title = node.title; if (node.title_field && node.title_field[default_language]) { node_title = node.title_field[default_language][0].safe_value; } // Build the node display. Set the node onto the build so it makes it to the theme layer variables. var build = { 'theme': 'node', 'node': node, // @todo - this is a core field and should by fetched from entity.js 'title': { markup: node_title }, 'content': { markup: node.content } }; // If comments are undefined, just inject the page. if (typeof node.comment === 'undefined') { done(build); } // If the comments are closed (1) or open (2), show the comments. else if (node.comment != 0) { if (node.comment == 1 || node.comment == 2) { // Render the comment form, so we can add it to the content later. var comment_form = ''; if (node.comment == 2) { comment_form = drupalgap_get_form( 'comment_edit', { nid: node.nid }, node ); } // If there are any comments, load them. if (node.comment_count != 0) { var query = { parameters: { nid: node.nid } }; comment_index(query, { success: function(results) { try { // Render the comments. var comments = ''; for (var index in results) { if (!results.hasOwnProperty(index)) { continue; } var comment = results[index]; comments += theme('comment', { comment: comment }); } build.content.markup += theme('comments', { node: node, comments: comments }); // If the comments are open, show the comment form. if (node.comment == 2 && user_access('post comments')) { build.content.markup += comment_form; } // Finally, inject the page. done(build); } catch (error) { var msg = 'node_page_view_pageshow - comment_index - ' + error; console.log(msg); } }, error: function(xhr, status, msg) { if (drupalgap.page.options.error) { drupalgap.page.options.error(xhr, status, msg); } } }); } else { // There weren't any comments, append an empty comments wrapper // and show the comment form if comments are open, then inject // the page. if (node.comment == 2) { build.content.markup += theme('comments', { node: node }); if (user_access('post comments')) { build.content.markup += comment_form; } } done(build); } } } else { // Comments are hidden (0), append an empty comments wrapper to the // content and inject the content into the page. build.content.markup += theme('comments', { node: node }); done(build); } }, error: function(xhr, status, msg) { if (drupalgap.page.options.error) { drupalgap.page.options.error(xhr, status, msg); } } }); } catch (error) { console.log('node_page_view_pageshow - ' + error); } } /** * The title call back function for the node view page. * @param {Function} callback * @param {Number} nid */ function node_page_title(callback, nid) { try { // Try to load the node title, then send it back to the given callback. var title = ''; var node = node_load(nid, { success: function(node) { if (node && node.title) { title = node.title; } callback.call(null, title); } }); } catch (error) { console.log('node_page_title - ' + error); } } /** * Implements hook_theme(). * @return {Object} */ function node_theme() { return { node: { template: 'node' } }; } /** * Implements hook_block_info(). * @return {Object} */ function search_block_info() { try { var blocks = {}; blocks['search'] = { delta: 'search', module: 'search' }; return blocks; } catch (error) { console.log('search_block_info - ' + error); } } /** * Implements hook_block_view(). * @param {String} delta * @param {String} region * @return {String} */ function search_block_view(delta, region) { try { var content = ''; if (delta == 'search') { if (user_access('search content')) { content = drupalgap_get_form('search_block_form'); } } return content; } catch (error) { console.log('search_block_view - ' + error); } } /** * Implements hook_menu(). * @return {Object} */ function search_menu() { try { var items = {}; items['search/%/%'] = { title: t('Search'), 'page_callback': 'drupalgap_get_form', 'pageshow': 'search_form_pageshow', 'page_arguments': ['search_form'], 'access_arguments': ['search content'] }; return items; } catch (error) { console.log('search_menu - ' + error); } } /** * The search block form. * @param {Object} form * @param {Object} form_state * @return {Object} */ function search_block_form(form, form_state) { try { form.elements['type'] = { type: 'hidden', default_value: 'node' }; form.elements['keys'] = { type: 'search', title: '', title_placeholder: true, required: true, default_value: '' }; // Since there is no submit button on the form, we'll catch the onsubmit // action and the trigger the form submission. form.options.attributes['onsubmit'] = "_drupalgap_form_submit('" + form.id + "'); return false;"; return form; } catch (error) { console.log('search_block_form - ' + error); } } /** * The search block form submit handler. * @param {Object} form * @param {Object} form_state */ function search_block_form_submit(form, form_state) { try { var type = form_state.values['type']; var keys = form_state.values['keys']; drupalgap_goto('search/' + type + '/' + keys); } catch (error) { console.log('search_block_form_submit - ' + error); } } /** * The search form. * @param {Object} form * @param {Object} form_state * @return {Object} */ function search_form(form, form_state) { try { var type = arg(1); var keys = arg(2); form.elements.type = { type: 'hidden', default_value: type ? type : 'node' }; form.elements.keys = { type: 'textfield', title: t('Enter your keywords'), required: true, default_value: keys ? keys : '' }; form.elements.submit = { type: 'submit', value: t('Go'), options: { attributes: { 'data-icon': 'search' } } }; form.suffix += theme('jqm_item_list', { title: t('Search results'), items: [], options: { attributes: { id: 'search_form_results' } } }); return form; } catch (error) { console.log('search_form - ' + error); } } /** * The search form submit handler. * @param {Object} form * @param {Object} form_state */ function search_form_submit(form, form_state) { try { var type = form_state.values['type']; var keys = form_state.values['keys']; switch (type) { case 'node': search_node(keys, { success: function(results) { var items = []; for (var index in results) { if (!results.hasOwnProperty(index)) { continue; } var result = results[index]; var link = theme('search_result_node', result); items.push(link); } drupalgap_item_list_populate('#search_form_results', items); } }); break; default: console.log('search_form_submit - unsupported type (' + type + ')'); break; } } catch (error) { console.log('search_form_submit - ' + error); } } /** * The pageshow callback for the search form page. * @param {String} form_id */ function search_form_pageshow(form_id) { try { var type = arg(1); var keys = arg(2); switch (type) { case 'node': search_node(keys, { success: function(results) { var items = []; for (var index in results) { if (!results.hasOwnProperty(index)) { continue; } var result = results[index]; var link = theme('search_result_node', result); items.push(link); } drupalgap_item_list_populate('#search_form_results', items); } }); break; default: console.log('search_form_pageshow - unsupported type (' + type + ')'); break; } } catch (error) { console.log('search_form_pageshow - ' + error); } } /** * The search node service. * @param {String} keys The keyword(s) to search for. * @param {Object} options */ function search_node(keys, options) { try { options.method = 'GET'; options.path = 'search_node/retrieve.json&keys=' + encodeURIComponent(keys); options.service = 'search_node'; options.resource = 'retrieve'; Drupal.services.call(options); } catch (error) { console.log('search_node - ' + error); } } /** * Themes a search result node. * @param {Object} variables * @return {String} */ function theme_search_result_node(variables) { try { return l( '

    ' + variables.title + '

    ' + '

    ' + variables.snippet + '

    ', 'node/' + variables.node.nid ); } catch (error) { console.log('theme_search_result_node - ' + error); } } /** * Given an entity type, this will return its corresponding service resource, or * null if the resource doesn't exist. * @param {String} entity_type * @return {?Type|Object|null} */ function drupalgap_services_get_entity_resource(entity_type) { try { console.log( 'WARNING: drupalgap_services_get_entity_resource() is deprecated! ' + 'Use services_get_resource_function_for_entity() instead.' ); // @todo - deprecate this function, it is no longer needed now that entity // c.r.u.d. is built into jDrupal. if (drupalgap.services[entity_type]) { return drupalgap.services[entity_type]; } else { return null; } } catch (error) { console.log('drupalgap_services_get_entity_resource - ' + error); } } /** * Returns the name of the jDrupal function to be used when in need of an entity * C.R.U.D. operation. * @param {String} entity_type * @param {String} crud * @return {String} */ function services_get_resource_function_for_entity(entity_type, crud) { var name = entity_type + '_'; switch (crud) { case 'create': name += 'save'; break; case 'retrieve': name += 'load'; break; case 'update': name += 'save'; break; case 'delete': name += 'delete'; break; default: name += 'load'; break; } return name; } /** * Given a json drupalgap options array from a service resource results call, * this extracts data based on the resource and populates necessary global vars. * @param {Object} options */ function drupalgap_service_resource_extract_results(options) { try { if (options.service == 'system' && options.resource == 'connect') { // The system connect resource's success function places what is in // options.data.user to overwrite Drupal.user, so anything we want in // the Drupal.user object must be added to options.data.user isntead. // Extract and build the user's permissions. options.data.user.permissions = []; var permissions = options.data.user_permissions; for (var permission in permissions) { options.data.user.permissions.push(permissions[permission]); } // Pull out the content types, and set them by their type. var content_types_list = options.data.content_types_list; for (var index in content_types_list) { if (!content_types_list.hasOwnProperty(index)) { continue; } var object = content_types_list[index]; drupalgap.content_types_list[object.type] = object; } // Pull out the content types user permissions. options.data.user.content_types_user_permissions = options.data.content_types_user_permissions; // Pull out the site settings. drupalgap.site_settings = options.data.site_settings; // Pull out the date formats and types. if (typeof options.data.date_formats !== 'undefined') { drupalgap.date_formats = options.data.date_formats; } if (typeof options.data.date_types !== 'undefined') { drupalgap.date_types = options.data.date_types; } } } catch (error) { console.log('drupalgap_service_resource_extract_results - ' + error); } } /** * Given the result of a drupalgap.services.rss.retrieve.call, this will iterate * over the RSS items and assemble them into a nice array of JSON objects and * return them. Returns null if it fails. * @param {Object} data * @return {?Type|Array|null} */ function drupalgap_services_rss_extract_items(data) { try { var items = null; var $xml = $(data); if ($xml) { // Extract the feeds items, then drop them in the list. var items = []; $xml.find('item').each(function() { var $this = $(this), item = { title: $this.find('title').text(), link: $this.find('link').text(), description: $this.find('description').text(), pubDate: $this.find('pubDate').text(), author: $this.find('author').text() }; items.push(item); }); } return items; } catch (error) { console.log('drupalgap_services_rss_extract_items - ' + error); } } var _system_reload_page = null; var _system_reload_messages = null; /** * Implements hook_install(). */ function system_install() { // Remove any old forms from local storage, then purge the form expiration tracker. for (var form_id in Drupal.cache_expiration.forms) { if (!Drupal.cache_expiration.forms.hasOwnProperty(form_id)) { continue; } drupalgap_form_local_storage_delete(form_id); } Drupal.cache_expiration.forms = {}; window.localStorage.setItem('cache_expiration', JSON.stringify(Drupal.cache_expiration)); } /** * Implements hook_block_info(). * @return {Object} */ function system_block_info() { // System blocks. var blocks = { 'main': { 'delta': 'main', 'module': 'system' }, messages: { delta: 'messages', module: 'system' }, 'logo': { 'delta': 'logo', 'module': 'system' }, logout: { delta: 'logout', module: 'system' }, 'title': { 'delta': 'title', 'module': 'system' }, 'powered_by': { 'delta': 'powered_by', 'module': 'system' }, 'help': { 'delta': 'help', 'module': 'system' } }; // Make additional blocks for each system menu. var system_menus = menu_list_system_menus(); for (var menu_name in system_menus) { if (!system_menus.hasOwnProperty(menu_name)) { continue; } var menu = system_menus[menu_name]; var block_delta = menu.menu_name; blocks[block_delta] = { name: block_delta, delta: block_delta, module: 'menu' }; } return blocks; } /** * Implements hook_block_view(). * @param {String} delta * @return {String} */ function system_block_view(delta) { try { switch (delta) { case 'main': // This is the main content block, it is required to be in a theme's // region for the content of a page to show up (nodes, users, taxonomy, // comments, etc). Depending on the menu link router, we need to route // this through the appropriate template files and functions. return drupalgap_render_page(); break; case 'messages': // If there are any messages waiting to be displayed, render them, then // clear out the messages array. var html = ''; if (drupalgap.messages.length == 0) { return html; } for (var index in drupalgap.messages) { if (!drupalgap.messages.hasOwnProperty(index)) { continue; } var msg = drupalgap.messages[index]; html += '
    ' + msg.message + '
    '; } drupalgap.messages = []; return html; break; case 'logo': if (drupalgap.settings.logo) { return ''; } return ''; break; case 'logout': if (Drupal.user.uid) { return theme('logout'); } return ''; break; case 'title': var title_id = system_title_block_id(drupalgap_path_get()); return '

    '; break; case 'powered_by': return '

    ' + t('Powered by') + ': ' + l('DrupalGap', 'http://www.drupalgap.org', {InAppBrowser: true}) + '

    '; break; case 'help': return l('Help', 'http://www.drupalgap.org/support'); break; default: return ''; break; } } catch (error) { console.log('system_block_info - ' + error); } } /** * Implements hook_menu(). * @return {Object} */ function system_menu() { var items = { 'dashboard': { 'title': t('Dashboard'), 'page_callback': 'system_dashboard_page' }, 'error': { 'title': t('Error'), 'page_callback': 'system_error_page' }, 'offline': { 'title': t('Offline'), 'page_callback': 'system_offline_page' }, '401': { title: '401 - ' + t('Not Authorized'), page_callback: 'system_401_page' }, '404': { title: '404 - ' + t('Not Found'), page_callback: 'system_404_page' } }; items['_reload'] = { title: t('Reloading') + '...', page_callback: 'system_reload_page', pageshow: 'system_reload_pageshow' }; return items; } /** * Page callback for the 401 page. * @param {String} path * @return {String} */ function system_401_page(path) { return t('Sorry, you are not authorized to view this page.'); } /** * Page callback for the 404 page. * @param {String} path * @return {String} */ function system_404_page(path) { return t('Sorry, the page you requested was not found.'); } /** * The page callback for the reload page. * @return {String} */ function system_reload_page() { try { // Set aside any messages, then return an empty page. var messages = drupalgap_get_messages(); if (!empty(messages)) { _system_reload_messages = messages.slice(); drupalgap_set_messages([]); } return ''; } catch (error) { console.log('system_reload_page - ' + error); } } /** * The pageshow callback for the reload page. */ function system_reload_pageshow() { try { // Set any messages that were set aside. if (_system_reload_messages && !empty(_system_reload_messages)) { for (var i = 0; i < _system_reload_messages.length; i++) { drupalgap_set_message( _system_reload_messages[i].message, _system_reload_messages[i].type ); } _system_reload_messages = null; } drupalgap_loading_message_show(); } catch (error) { console.log('system_reload_pageshow - ' + error); } } /** * Implements hook_system_drupalgap_goto_post_process(). * @param {String} path */ function system_drupalgap_goto_post_process(path) { try { // To reload the "current" page, grab the path we have been requested to // reload, clear out our global reference to it, then go! // @see https://github.com/signalpoint/DrupalGap/issues/254 if (path == '_reload') { if (!_system_reload_page) { return; } var path = '' + _system_reload_page; _system_reload_page = null; drupalgap_loading_message_show(); drupalgap_goto(path, { reloadPage: true }); } } catch (error) { console.log('system_drupalgap_goto_post_process - ' + error); } } /** * Page callback for the dashboard page. * @return {Object} */ function system_dashboard_page() { try { var content = {}; content.site_info = { markup: '

    ' + Drupal.settings.site_path + '

    ' }; content.welcome = { markup: '

    ' + t('Welcome to DrupalGap') + '

    ' + '

    ' + t('The open source application development kit for Drupal!') + '

    ' }; if (drupalgap.settings.logo) { content.logo = { markup: '
    ' + theme('image', {path: drupalgap.settings.logo}) + '
    ' }; } content.get_started = { theme: 'button_link', text: t('Getting Started Guide'), path: 'http://www.drupalgap.org/get-started', options: {InAppBrowser: true} }; content.support = { theme: 'button_link', text: t('Support'), path: 'http://www.drupalgap.org/support', options: {InAppBrowser: true} }; return content; } catch (error) { console.log('system_dashboard_page - ' + error); } } /** * The page callback for the error page. * @return {Object} */ function system_error_page() { var content = { info: { markup: '

    ' + t('An unexpected error has occurred!') + '

    ' } }; return content; } /** * Call back for the offline page. * @return {Object} */ function system_offline_page() { try { var content = { 'message': { 'markup': '

    ' + t('Failed Connection') + '

    ' + '

    ' + t("Oops! We couldn't connect to") + ':

    ' + '

    ' + Drupal.settings.site_path + '

    ' }, 'try_again': { 'theme': 'button', 'text': t('Try Again'), 'attributes': { 'onclick': 'javascript:offline_try_again();' } }, 'footer': { 'markup': '

    ' + t("Check your device's network settings and try again.") + '

    ' } }; return content; } catch (error) { console.log('system_offline_page - ' + error); } } /** * When the 'try again' button is clicked, check for a connection and if it has * one make a call to system connect then go to the front page, otherwise just * inform user the device is still offline. * @return {*} */ function offline_try_again() { try { var connection = drupalgap_check_connection(); if (drupalgap.online) { system_connect({ success: function() { drupalgap_goto(''); } }); } else { var msg = t('Sorry, no connection found!') + ' (' + connection + ')'; drupalgap_alert(msg, { title: 'Offline' }); return false; } } catch (error) { console.log('offline_try_again - ' + error); } } /** * Returns an array of region names defined by the system that themes must use. * We do this so Core and Contrib Modules can use these regions for UI needs. * @return {Array} */ function system_regions_list() { var regions = ['header', 'content', 'footer']; return regions; } /** * Add default buttons to a form and set its prefix. * @param {Object} form * @param {Object} form_state * @return {Object} */ function system_settings_form(form, form_state) { try { // Add submit button to form if one isn't present. if (!form.elements.submit) { form.elements.submit = { type: 'submit', value: t('Save configuration') }; } // Add cancel button to form if one isn't present. if (!form.buttons.cancel) { form.buttons['cancel'] = drupalgap_form_cancel_button(); } // Attach submit handler. form.submit.push('system_settings_form_submit'); return form; } catch (error) { console.log('system_settings_form - ' + error); } } /** * Execute the system_settings_form. * @param {Object} form * @param {Object} form_state */ function system_settings_form_submit(form, form_state) { try { if (form_state.values) { for (var variable in form_state.values) { if (!form_state.values.hasOwnProperty(variable)) { continue; } var value = form_state.values[variable]; variable_set(variable, value); } } } catch (error) { console.log('system_settings_form_submit - ' + error); } } /** * Returns the block id used on the system's title block. * @param {String} path * @return {String} */ function system_title_block_id(path) { try { var id = 'drupalgap_page_title_' + drupalgap_get_page_id(path); return id; } catch (error) { console.log('system_title_block_id - ' + error); } } /** * The default access callback function for the logout block. Allows the block * to only be shown when a user is viewing their own profile. * @param {Object} options * @return {Boolean} */ function system_logout_block_access_callback(options) { try { var args = arg(null, options.path); if ( args && args.length == 2 && args[0] == 'user' && args[1] == Drupal.user.uid ) { return true; } return false; } catch (error) { console.log('system_logout_block_access_callback - ' + error); } } // Used to hold onto the terms once they've been loaded into a widget, keyed by // the form element's id, this allows (views exposed filters particularly) forms // to easily retrieve the terms after they've been fetch from the server. var _taxonomy_term_reference_terms = {}; /** * Extracts the taxonomy vocabularies JSON objects bundled by the Drupal module * into the system connect's resource results. * @param {Object} taxonomy_vocabularies * @return {Object} */ function drupalgap_taxonomy_vocabularies_extract(taxonomy_vocabularies) { try { var results = false; if (taxonomy_vocabularies && taxonomy_vocabularies.length > 0) { results = {}; for (var index in taxonomy_vocabularies) { if (!taxonomy_vocabularies.hasOwnProperty(index)) { continue; } var vocabulary = taxonomy_vocabularies[index]; results[vocabulary.machine_name] = vocabulary; } } return results; } catch (error) { console.log('drupalgap_taxonomy_vocabularies_extract - ' + error); } } /** * Implements hook_field_formatter_view(). * @param {String} entity_type * @param {Object} entity * @param {Object} field * @param {Object} instance * @param {String} langcode * @param {Object} items * @param {*} display * @return {Object} */ function taxonomy_field_formatter_view(entity_type, entity, field, instance, langcode, items, display) { try { var element = {}; // If items is a string, convert it into a single item JSON object. if (typeof items === 'string') { items = {0: {tid: items}}; } // It's possible the term items are wrapped in a language code, if they are, // pull them out. if (typeof items[language_default()] !== 'undefined') { items = items[language_default()]; } if (!empty(items)) { for (var delta in items) { if (!items.hasOwnProperty(delta)) { continue; } var item = items[delta]; var text = item.tid; if (item.name) { text = item.name; } var content = null; switch (display.type) { case 'taxonomy_term_reference_link': content = { theme: 'button_link', text: text, path: 'taxonomy/term/' + item.tid }; break; case 'taxonomy_term_reference_plain': content = { markup: text }; break; default: content = { markup: text }; break; } element[delta] = content; } } return element; } catch (error) { console.log('taxonomy_field_formatter_view - ' + error); } } /** * Implements hook_field_widget_form(). * @param {Object} form * @param {Object} form_state * @param {Object} field * @param {Object} instance * @param {String} langcode * @param {Object} items * @param {Number} delta * @param {Object} element */ function taxonomy_field_widget_form(form, form_state, field, instance, langcode, items, delta, element) { try { items[delta].type = 'hidden'; // Build the widget and attach it to the item. var list_id = items[delta].id + '-list'; var widget = { theme: 'item_list', items: [], attributes: { 'id': list_id, 'data-role': 'listview', 'data-filter': 'true', 'data-inset': 'true', 'data-filter-placeholder': '...' } }; items[delta].children.push(widget); // Attach JS to handle the widget's data fetching. var machine_name = field.settings.allowed_values[0].vocabulary; var vocabulary = taxonomy_vocabulary_machine_name_load(machine_name); var vid = vocabulary.vid; var js = ''; items[delta].children.push({ markup: js }); } catch (error) { console.log('taxonomy_field_widget_form - ' + error); } } var _taxonomy_field_widget_form_autocomplete_input = null; /** * Handles the remote data fetching for taxonomy term reference autocomplete * tagging widget. * @param {String} id The id of the hidden input that will hold the term id. * @param {Number} vid * @param {Object} list The unordered list that displays the terms. * @param {Object} e * @param {Object} data */ function _taxonomy_field_widget_form_autocomplete(id, vid, list, e, data) { try { // Setup the vars to handle this widget. var $ul = $(list), $input = $(data.input), value = $input.val(), html = ''; // Save a reference to this text input field. Then attach an on change // handler that will set the hidden input's value when the text field // changes. Keep in mind, later on we listen for clicks on autocomplete // results to populate this same hidden input's field. _taxonomy_field_widget_form_autocomplete_input = $input; $(_taxonomy_field_widget_form_autocomplete_input).on('change', function() { $('#' + id).val($(this).val()); }); // Clear the list, then set up its input handlers. $ul.html(''); if (value && value.length > 0) { $ul.html('
  • ' + '' + '
  • '); $ul.listview('refresh'); var query = { fields: ['tid', 'name'], parameters: { vid: vid, name: '%' + value + '%' }, parameters_op: { name: 'like' } }; taxonomy_term_index(query, { success: function(terms) { if (terms.length != 0) { // Extract the terms into items, then drop them in the list. var items = []; for (var index in terms) { if (!terms.hasOwnProperty(index)) { continue; } var term = terms[index]; var attributes = { tid: term.tid, vid: vid, name: term.name, onclick: '_taxonomy_field_widget_form_click(' + "'" + id + "', " + "'" + $ul.attr('id') + "', " + 'this' + ')' }; html += '
  • ' + term.name + '
  • '; } $ul.html(html); $ul.listview('refresh'); $ul.trigger('updatelayout'); } } }); } } catch (error) { console.log('_taxonomy_field_widget_form_autocomplete - ' + error); } } /** * Handles clicks on taxonomy term reference autocomplete widgets. * @param {String} id The id of the hidden input that will hold the term name. * @param {String} list_id The id of the list that holds the terms. * @param {Object} item The list item that was just clicked. */ function _taxonomy_field_widget_form_click(id, list_id, item) { try { var tid = $(item).attr('name'); $('#' + id).val(tid); $(_taxonomy_field_widget_form_autocomplete_input).val($(item).attr('name')); $('#' + list_id).html(''); } catch (error) { console.log('_taxonomy_field_widget_form_click - ' + error); } } /** * Implements hook_assemble_form_state_into_field(). * @param {Object} entity_type * @param {String} bundle * @param {String} form_state_value * @param {Object} field * @param {Object} instance * @param {String} langcode * @param {Number} delta * @param {Object} field_key * @return {Object} */ function taxonomy_assemble_form_state_into_field(entity_type, bundle, form_state_value, field, instance, langcode, delta, field_key) { try { var result = null; switch (instance.widget.type) { case 'taxonomy_autocomplete': field_key.use_wrapper = false; result = form_state_value; break; case 'options_select': result = form_state_value; break; } return result; } catch (error) { console.log('taxonomy_assemble_form_state_into_field - ' + error); } } /** * Implements hook_menu(). * @return {Object} */ function taxonomy_menu() { var items = { 'taxonomy/vocabularies': { 'title': t('Taxonomy'), 'page_callback': 'taxonomy_vocabularies_page', 'pageshow': 'taxonomy_vocabularies_pageshow' }, 'taxonomy/vocabulary/%': { 'title': t('Taxonomy vocabulary'), 'page_callback': 'taxonomy_vocabulary_page', 'page_arguments': [2], 'pageshow': 'taxonomy_vocabulary_pageshow' }, 'taxonomy/vocabulary/%/view': { 'title': t('View'), 'type': 'MENU_DEFAULT_LOCAL_TASK', 'weight': -10 }, 'taxonomy/vocabulary/%/edit': { 'title': t('Edit'), 'page_callback': 'entity_page_edit', 'pageshow': 'entity_page_edit_pageshow', 'page_arguments': [ 'taxonomy_form_vocabulary', 'taxonomy_vocabulary', 2 ], 'weight': 0, 'type': 'MENU_LOCAL_TASK', 'access_arguments': ['administer taxonomy'], options: {reloadPage: true} }, 'taxonomy/term/%': { 'title': t('Taxonomy term'), 'page_callback': 'taxonomy_term_page', 'page_arguments': [2], 'pageshow': 'taxonomy_term_pageshow' }, 'taxonomy/term/%/view': { 'title': t('View'), 'type': 'MENU_DEFAULT_LOCAL_TASK', 'weight': -10 }, 'taxonomy/term/%/edit': { 'title': t('Edit'), 'page_callback': 'entity_page_edit', 'pageshow': 'entity_page_edit_pageshow', 'page_arguments': ['taxonomy_form_term', 'taxonomy_term', 2], 'weight': 0, 'type': 'MENU_LOCAL_TASK', 'access_arguments': ['administer taxonomy'], options: {reloadPage: true} } }; return items; } /** * The taxonomy vocabulary form. * @param {Object} form * @param {Object} form_state * @param {Object} vocabulary * @return {Object} */ function taxonomy_form_vocabulary(form, form_state, vocabulary) { try { // Setup form defaults. form.entity_type = 'taxonomy_vocabulary'; form.bundle = null; form.action = 'taxonomy/vocabularies'; // Add the entity's core fields to the form. drupalgap_entity_add_core_fields_to_form( 'taxonomy_vocabulary', null, form, vocabulary ); // Add submit to form. form.elements.submit = { 'type': 'submit', 'value': t('Save') }; // Add cancel button to form. form.buttons['cancel'] = drupalgap_form_cancel_button(); // If we're editing a vocabulary add a delete button, if the user has // access. if (vocabulary && vocabulary.vid && user_access('administer taxonomy')) { form.buttons['delete'] = drupalgap_entity_edit_form_delete_button( 'taxonomy_vocabulary', vocabulary.vid ); } return form; } catch (error) { console.log('taxonomy_form_vocabulary - ' + error); } } /** * The taxonomy vocabulary form submit handler. * @param {Object} form * @param {Object} form_state */ function taxonomy_form_vocabulary_submit(form, form_state) { try { var vocabulary = drupalgap_entity_build_from_form_state(form, form_state); drupalgap_entity_form_submit(form, form_state, vocabulary); } catch (error) { console.log('taxonomy_form_vocabulary_submit - ' + error); } } /** * The taxonomy term form. * @param {Object} form * @param {Object} form_state * @param {Object} term * @return {Object} */ function taxonomy_form_term(form, form_state, term) { try { // Setup form defaults. form.entity_type = 'taxonomy_term'; form.bundle = null; form.action = 'taxonomy/vocabularies'; // Add the entity's core fields to the form. drupalgap_entity_add_core_fields_to_form('taxonomy_term', null, form, term); // Add submit to form. form.elements.submit = { 'type': 'submit', 'value': t('Save') }; // Add cancel button to form. form.buttons['cancel'] = drupalgap_form_cancel_button(); // If we are editing a term, add a delete button. if (term && term.tid && user_access('administer taxonomy')) { form.buttons['delete'] = drupalgap_entity_edit_form_delete_button( 'taxonomy_term', term.tid ); } return form; } catch (error) { console.log('taxonomy_form_term - ' + error); } } /** * The taxonomy term form submit handler. * @param {Object} form * @param {Object} form_state */ function taxonomy_form_term_submit(form, form_state) { try { var term = drupalgap_entity_build_from_form_state(form, form_state); drupalgap_entity_form_submit(form, form_state, term); } catch (error) { console.log('taxonomy_form_term_submit - ' + error); } } /** * Page callback for taxonomy/term/%. * @param {Number} tid * @return {Object} */ function taxonomy_term_page(tid) { try { if (tid) { var content = { container: _drupalgap_entity_page_container( 'taxonomy_term', tid, 'view' ), taxonomy_term_node_listing: { theme: 'jqm_item_list', items: [], attributes: { id: 'taxonomy_term_node_listing_items_' + tid } } }; return content; } else { console.log('taxonomy_term_pageshow - No term id provided!'); } } catch (error) { console.log('taxonomy_term_pageshow - ' + error); } } /** * jQM pageshow callback for taxonomy/term/% * @param {Number} tid */ function taxonomy_term_pageshow(tid) { try { taxonomy_term_load(tid, { success: function(term) { var description = term.description ? term.description : ''; var content = { 'name': { 'markup': '

    ' + term.name + '

    ' }, 'description': { 'markup': '

    ' + description + '

    ' } }; _drupalgap_entity_page_container_inject( 'taxonomy_term', term.tid, 'view', content ); taxonomy_term_selectNodes(term.tid, { success: function(results) { // Extract the nodes into items, then drop them in the list. var items = []; for (var index in results) { if (!results.hasOwnProperty(index)) { continue; } var node = results[index]; items.push(l(node.title, 'node/' + node.nid)); } drupalgap_item_list_populate( '#taxonomy_term_node_listing_items_' + term.tid, items ); } }); } }); } catch (error) { console.log('taxonomy_term_pageshow - ' + error); } } /** * The selectNodes resource from the Taxonomy Term service. * @param {Number} tid The taxonomy term id. * @param {Object} options */ function taxonomy_term_selectNodes(tid, options) { try { // @TODO - move this function to jDrupal. options.method = 'POST'; options.path = 'taxonomy_term/selectNodes.json'; options.service = 'taxonomy_term'; options.resource = 'selectNodes'; options.data = JSON.stringify({ tid: tid}); Drupal.services.call(options); } catch (error) { console.log('taxonomy_term_selectNodes - ' + error); } } /** * Page call back for taxonomy/vocabularies. * @return {Object} * @return {Object} */ function taxonomy_vocabularies_page() { // Place an empty item list that will hold a list of users. var content = { 'vocabulary_listing': { 'theme': 'jqm_item_list', 'title': t('Vocabularies'), 'items': [], 'attributes': {'id': 'vocabulary_listing_items'} } }; return content; } /** * jQM pageshow call back for taxonomy/vocabularies. */ function taxonomy_vocabularies_pageshow() { try { taxonomy_vocabulary_index(null, { success: function(vocabularies) { // Extract the vocabs into items, then drop them in the list. var items = []; for (var index in vocabularies) { if (!vocabularies.hasOwnProperty(index)) { continue; } var vocabulary = vocabularies[index]; items.push( l(vocabulary.name, 'taxonomy/vocabulary/' + vocabulary.vid) ); } drupalgap_item_list_populate('#vocabulary_listing_items', items); } }); } catch (error) { console.log('taxonomy_vocabularies_pageshow - ' + error); } } /** * Page callback for taxonomy/vocabulary/% * @param {Number} vid * @return {String|Object} */ function taxonomy_vocabulary_page(vid) { try { if (vid) { var content = { container: _drupalgap_entity_page_container( 'taxonomy_vocabulary', vid, 'view' ), taxonomy_term_listing: { theme: 'jqm_item_list', title: t('Terms'), items: [], attributes: { id: 'taxonomy_term_listing_items_' + vid } } }; return content; } else { console.log('taxonomy_vocabulary_page - No vocabulary id provided!'); } } catch (error) { console.log('taxonomy_vocabulary_page - ' + error); } } /** * jQM pageshow callback for taxonomy/vocabulary/%. * @param {Number} vid */ function taxonomy_vocabulary_pageshow(vid) { try { taxonomy_vocabulary_load(vid, { success: function(vocabulary) { var content = { 'name': { 'markup': '

    ' + vocabulary.name + '

    ' }, 'description': { 'markup': '

    ' + vocabulary.description + '

    ' } }; _drupalgap_entity_page_container_inject( 'taxonomy_vocabulary', vocabulary.vid, 'view', content ); var query = { parameters: { vid: vid } }; taxonomy_term_index(query, { success: function(terms) { if (terms.length != 0) { // Extract the terms into items, then drop them in the list. var items = []; for (var index in terms) { if (!terms.hasOwnProperty(index)) { continue; } var term = terms[index]; items.push(l(term.name, 'taxonomy/term/' + term.tid)); } drupalgap_item_list_populate( '#taxonomy_term_listing_items_' + vid, items ); } } }); } }); } catch (error) { console.log('taxonomy_vocabulary_pageshow - ' + error); } } /** * Given a vocabulary machine name this will return the vocabulary id or false. * @param {String} name * @return {Number|Boolean} */ function taxonomy_vocabulary_get_vid_from_name(name) { try { var vocabulary = taxonomy_vocabulary_machine_name_load(name); if (vocabulary) { return vocabulary.vid; } return false; } catch (error) { console.log('taxonomy_vocabulary_get_vid_from_name - ' + error); } } /** * Given a vocabulary machine name this will return the JSON * object for the vocabulary that is attached to the * drupalgap.taxonomy_vocabularies object, or false if it doesn't exist. * @param {String} name * @return {Object|Boolean} */ function taxonomy_vocabulary_machine_name_load(name) { try { if (drupalgap.taxonomy_vocabularies && drupalgap.taxonomy_vocabularies[name]) { return drupalgap.taxonomy_vocabularies[name]; } return false; } catch (error) { console.log('taxonomy_vocabulary_machine_name_load - ' + error); } } /** * Theme's the html for a taxonomy term reference field. * @param {Object} variables * @return {String} */ function theme_taxonomy_term_reference(variables) { try { var html = ''; // Make this a hidden field since the widget will just populate a value. If // somone wants to skip the input element html generation (e.g. taxonomy // term reference views exposed filter), let them do it, but by default // we'll render the input for them. var render_input_element = true; if (typeof variables.render_input_element !== 'undefined') { render_input_element = variables.render_input_element; } if (render_input_element) { variables.attributes.type = 'hidden'; html += ''; } // Is anyone flagging this as required? i.e. is it a views exposed filter? // @TODO - the field system that assembles the elements onto a form, when it // is a taxonomy term reference field, we need to pass along the required // option so when we load the items into the select list, we know whether or // not to include the "empty string" option. This is probably in field.js. var required = false; if (typeof variables.required !== 'undefined') { required = variables.required; } // Is this widget exposed (aka views exposed filter)? var exposed = false; if (typeof variables.exposed !== 'undefined') { exposed = variables.exposed; } // What vocabulary are we using? var machine_name = variables.field_info_field.settings.allowed_values[0].vocabulary; var taxonomy_vocabulary = taxonomy_vocabulary_machine_name_load( machine_name ); // Prepare the variables for the widget and render it based on its type. var widget_type = variables.field_info_instance.widget.type; if (widget_type == 'options_select') { widget_type = 'select'; } var widget_function = 'theme_' + widget_type; var widget_id = variables.attributes.id + '-' + widget_type; if (function_exists(widget_function)) { // Grab the function in charge of themeing this widget. var fn = window[widget_function]; // Build the variables for the widget. var widget_variables = { attributes: { id: widget_id, onchange: "_theme_taxonomy_term_reference_onchange(this, '" + variables.attributes.id + "');" } }; // If the options were previously set aside for this widget, use them. var options_available = false; if (_taxonomy_term_reference_terms[variables.attributes.id]) { options_available = true; widget_variables.options = _taxonomy_term_reference_terms[variables.attributes.id]; } // Was their a value present to include as the default value for the // widget, if so include it. If not, and this filter is not required, set // the default value to an empty string so the widget renders the default // option correctly. A views exposed filter uses 'All' instead of an // empty string. if (typeof variables.value !== 'undefined') { widget_variables.value = variables.value; } else if (!required) { if (exposed) { widget_variables.value = 'All'; } else { widget_variables.value = ''; } } // Render the widget. html += fn.call(null, widget_variables); // If the options weren't available, attach a pageshow handler to the // current page that will load the terms into the widget. Keep in mind, // this inline pageshow handler only gets called once when the first view // is loaded. That's why we later set aside the options so they can be // used again without having to fire the pageshow event. if (!options_available) { var options = { page_id: drupalgap_get_page_id(drupalgap_path_get()), jqm_page_event: 'pageshow', jqm_page_event_callback: '_theme_taxonomy_term_reference_load_items', jqm_page_event_args: JSON.stringify({ taxonomy_vocabulary: taxonomy_vocabulary, element_id: variables.attributes.id, widget_id: widget_id, required: required, exposed: exposed }) }; html += drupalgap_jqm_page_event_script_code(options); } } else { console.log( 'WARNING: theme_taxonomy_term_reference() - ' + 'unsupported widget type! (' + widget_type + ')' ); } return html; } catch (error) { console.log('theme_taxonomy_term_reference - ' + error); } } /** * An internal function used to load the taxonomy terms from a term reference * field and populate them into select list identified by options.widget_id. * @param {Object} options */ function _theme_taxonomy_term_reference_load_items(options) { try { // Build the index query, then make the call to the server. var query = { parameters: { vid: options.taxonomy_vocabulary.vid }, options: { orderby: { weight: 'asc', name: 'asc' } } }; taxonomy_term_index(query, { success: function(terms) { if (terms.length == 0) { return; } // As we iterate over the terms, we'll set them aside in a JSON // object so they can be used later. _taxonomy_term_reference_terms[options.element_id] = { }; // Grab the widget. var widget = $('#' + options.widget_id); // If it's not required, place an empty option on the widget and set // it aside. if (!options.required) { var option = null; if (options.exposed) { option = ''; _taxonomy_term_reference_terms[options.element_id]['All'] = '- Any -'; } else { option = ''; _taxonomy_term_reference_terms[options.element_id][''] = '- None -'; } $(widget).append(option); } // Place each term in the widget as an option, and set the option // aside. for (var index in terms) { if (!terms.hasOwnProperty(index)) { continue; } var term = terms[index]; var option = ''; $(widget).append(option); _taxonomy_term_reference_terms[options.element_id][term.tid] = term.name; } // Refresh the select list. $(widget).selectmenu('refresh', true); } }); } catch (error) { console.log('_theme_taxonomy_term_reference_load_items - ' + error); } } /** * An internal function used by a taxonomy term reference field widget to * detect changes on it and populate the hidden field that holds the tid in the * form. * @param {Object} input * @param {String} id */ function _theme_taxonomy_term_reference_onchange(input, id) { try { $('#' + id).val($(input).val()); } catch (error) { console.log('_theme_taxonomy_term_reference_onchange - ' + error); } } /** * Implements hook_views_exposed_filter(). * @param {Object} form * @param {Object} form_state * @param {Object} element * @param {Object} filter * @param {Object} field */ function taxonomy_views_exposed_filter( form, form_state, element, filter, field) { try { //dpm('taxonomy_views_exposed_filter'); //console.log(element); //console.log(filter); //console.log(field); // @TODO this filter loses its value after one submission, aka the next // submission will submit it as 'All' even though we have a term selected in // the widget from the previous submission. // Autocomplete. if (filter.options.type == 'textfield') { element.type = 'autocomplete'; element.remote = true; element.custom = true; element.handler = 'index'; element.entity_type = 'taxonomy_term'; if (typeof filter.options.vocabulary !== 'undefined') { element.vid = taxonomy_vocabulary_get_vid_from_name(filter.options.vocabulary); } element.value = 'name'; element.label = 'name'; element.filter = 'name'; } // Dropdown. else { // Change the input to hidden, then iterate over each vocabulary and inject // them into the widget. We'll just use a taxonomy term reference field and // fake its instance. element.type = 'hidden'; for (var index in field.settings.allowed_values) { if (!field.settings.allowed_values.hasOwnProperty(index)) { continue; } var object = field.settings.allowed_values[index]; // Build the variables for the widget. var variables = { required: element.required, render_input_element: false, attributes: { id: element.options.attributes.id }, field_info_field: field, field_info_instance: { widget: { type: 'options_select' } }, exposed: true }; // If we have a default value, send it along. // @TODO add support for multiple values. if (!empty(filter.value)) { variables.value = parseInt(filter.value[0]); variables.attributes.value = variables.value; } // Add the widget as a child to the form element, including a label if // necessary (since the original label is lost because it was turned // into a hidden element). var child = ''; if (!empty(element.title)) { child += theme('form_element_label', { element: element }); } child += theme('taxonomy_term_reference', variables); element.children.push({ markup: child }); } } } catch (error) { console.log('taxonomy_views_exposed_filter - ' + error); } } /** * The user login form. * @param {Object} form * @param {Object} form_state * @return {Object} */ function user_login_form(form, form_state) { try { form.entity_type = 'user'; form.bundle = null; form.elements.name = { type: 'textfield', title: t('Username'), title_placeholder: true, required: true, attributes: { autocapitalize: 'none' } }; form.elements.pass = { type: 'password', title: t('Password'), title_placeholder: true, required: true, attributes: { onkeypress: "drupalgap_form_onkeypress('" + form.id + "', event)" } }; form.elements.submit = { type: 'submit', value: t('Login') }; if (user_register_access()) { form.buttons['create_new_account'] = { title: t('Create new account'), attributes: { onclick: "drupalgap_goto('user/register')" } }; } form.buttons['forgot_password'] = { title: t('Request new password'), attributes: { onclick: "drupalgap_goto('user/password')" } }; return form; } catch (error) { console.log('user_login_form - ' + error); } } /** * The user login form submit handler. * @param {Object} form * @param {Object} form_state */ function user_login_form_submit(form, form_state) { try { user_login(form_state.values.name, form_state.values.pass, { success: function(result) { drupalgap_goto( typeof form.action !== 'undefined' ? form.action : drupalgap.settings.front, { reloadPage:true } ); } }); } catch (error) { console.log('user_login_form_submit - ' + error); } } /** * The user registration form. * @param {Object} form * @param {Object} form_state * @return {Object} */ function user_register_form(form, form_state) { try { form.entity_type = 'user'; form.bundle = null; var description = t('Spaces are allowed; punctuation is not allowed except for periods, hyphens, apostrophes, and underscores.'); form.elements.name = { type: 'textfield', title: t('Username'), title_placeholder: true, required: true, description: description }; form.elements.mail = { type: 'email', title: t('E-mail address'), title_placeholder: true, required: true }; // If e-mail verification is not required, provide password fields and // the confirm e-mail address field. if (!drupalgap.site_settings.user_email_verification) { form.elements.conf_mail = { type: 'email', title: t('Confirm e-mail address'), title_placeholder: true, required: true }; form.elements.pass = { type: 'password', title: t('Password'), title_placeholder: true, required: true }; form.elements.pass2 = { type: 'password', title: t('Confirm password'), title_placeholder: true, required: true }; } // @TODO - instead of a null bundle, it appears drupal uses the bundle 'user' instead. drupalgap_field_info_instances_add_to_form('user', null, form, null); // Add registration messages to form. form.user_register = { 'user_mail_register_no_approval_required_body': t('Registration complete!'), 'user_mail_register_pending_approval_required_body': t('Registration complete, waiting for administrator approval.'), 'user_mail_register_email_verification_body': t('Registration complete, check your e-mail inbox to verify the ' + 'account.') }; // Set the auto login boolean. This only happens when the site's account // settings require no e-mail verification. Others can stop this from // happening via hook_form_alter(). form.auto_user_login = true; // Add submit button. form.elements.submit = { 'type': 'submit', 'value': t('Create new account') }; return form; } catch (error) { console.log('user_register_form - ' + error); } } /** * Define the form's validation function (optional). * @param {Object} form * @param {Object} form_state */ function user_register_form_validate(form, form_state) { try { // If e-mail verification is not required, make sure the passwords match. if (!drupalgap.site_settings.user_email_verification && form_state.values.pass != form_state.values.pass2) { drupalgap_form_set_error('pass', t('Passwords do not match!')); } // If there are two e-mail address fields on the form, make sure they match. if (!empty(form_state.values.mail) && !empty(form_state.values.conf_mail) && form_state.values.mail != form_state.values.conf_mail ) { drupalgap_form_set_error('mail', t('E-mail addresses do not match!')); } } catch (error) { console.log('user_register_form_validate - ' + error); } } /** * The user registration form submit handler. * @param {Object} form * @param {Object} form_state */ function user_register_form_submit(form, form_state) { try { var account = drupalgap_entity_build_from_form_state(form, form_state); user_register(account, { success: function(data) { var config = form.user_register; var options = { title: t('Registered') }; var destination = typeof form.action !== 'undefined' ? form.action : drupalgap.settings.front; // Check if e-mail verification is required or not.. if (!drupalgap.site_settings.user_email_verification) { // E-mail verification not needed, if administrator approval is // needed, notify the user, otherwise log them in. if (drupalgap.site_settings.user_register == '2') { drupalgap_alert( config.user_mail_register_pending_approval_required_body, options ); drupalgap_goto(destination); } else { drupalgap_alert( config.user_mail_register_no_approval_required_body, options ); // If we're automatically logging in do it, otherwise just go to // the front page. if (form.auto_user_login) { user_login(account.name, account.pass, { success: function(result) { drupalgap_goto(''); } }); } else { drupalgap_goto(destination); } } } else { // E-mail verification needed... notify the user. drupalgap_alert( config.user_mail_register_email_verification_body, options ); drupalgap_goto(destination); } }, error: function(xhr, status, message) { // If there were any form errors, display them. var msg = _drupalgap_form_submit_response_errors(form, form_state, xhr, status, message); if (msg) { drupalgap_alert(msg); } } }); } catch (error) { console.log('user_register_form_submit - ' + error); } } /** * The user profile form. * @param {Object} form * @param {Object} form_state * @param {Object} account * @return {Object} */ function user_profile_form(form, form_state, account) { try { // Setup form defaults. form.entity_type = 'user'; form.bundle = null; // Add the entity's core fields to the form. drupalgap_entity_add_core_fields_to_form('user', null, form, account); // Add the fields for accounts to the form. drupalgap_field_info_instances_add_to_form('user', null, form, account); // If the user can't change their user name, remove access to it. if (!user_access('change own username')) { form.elements['name'].access = false; form.elements['name'].required = false; } // If profile pictures are disabled, remove the core field from the form. if (drupalgap.site_settings.user_pictures == 0) { delete form.elements.picture; } // Add password fields to the form. We show the current password field only // if the user is editing their account. We show the password and confirm // password field no matter what. if (Drupal.user.uid == account.uid) { form.elements.current_pass = { 'title': t('Current password'), 'type': 'password', 'description': t('Enter your current password to change the E-mail ' + 'address or Password.') }; } form.elements.pass_pass1 = { 'title': t('Password'), 'type': 'password' }; form.elements.pass_pass2 = { 'title': t('Confirm password'), 'type': 'password', 'description': t('To change the current user password, enter the new ' + 'password in both fields.') }; // Add submit to form. form.elements.submit = { 'type': 'submit', 'value': t('Save') }; // Add cancel button to form. form.buttons['cancel'] = { 'title': t('Cancel'), attributes: { onclick: 'javascript:drupalgap_back();' } }; return form; } catch (error) { console.log('user_profile_form - ' + error); } } /** * The user profile form validate handler. * @param {Object} form * @param {Object} form_state */ function user_profile_form_validate(form, form_state) { try { // If they entered their current password, and entered new passwords, make // sure the new passwords match. if (!empty(form_state.values['current_pass'])) { if ( !empty(form_state.values['pass_pass1']) && !empty(form_state.values['pass_pass2']) && form_state.values['pass_pass1'] != form_state.values['pass_pass2'] ) { drupalgap_form_set_error('pass_pass1', t('Passwords do not match.')); } } // If they didn't enter their current password and entered new passwords, // tell them they need to enter their current password. else if ( empty(form_state.values['current_pass']) && !empty(form_state.values['pass_pass1']) && !empty(form_state.values['pass_pass2']) ) { drupalgap_form_set_error( 'current_pass', t('You must enter your current password to change your password.') ); } } catch (error) { console.log('user_profile_form_validate - ' + error); } } /** * The user profile form submit handler. * @param {Object} form * @param {Object} form_state */ function user_profile_form_submit(form, form_state) { try { var account = drupalgap_entity_build_from_form_state(form, form_state); // If they provided their current password, and their new password, prepare // the account submission values. if ( account.current_pass && !empty(account.pass_pass1) && !empty(account.pass_pass2) ) { account.pass = account.pass_pass1; delete account.pass_pass1; delete account.pass_pass2; } drupalgap_entity_form_submit(form, form_state, account); } catch (error) { console.log('user_profile_form_submit - ' + error); } } /** * The request new password form. * @param {Object} form * @param {Object} form_state * @return {Object} */ function user_pass_form(form, form_state) { form.elements['name'] = { type: 'textfield', title: t('Username or e-mail address'), required: true, attributes: { onkeypress: "drupalgap_form_onkeypress('" + form.id + "', event)" } }; form.elements['submit'] = { type: 'submit', value: t('E-mail new password') }; return form; } /** * The request new password form submission handler. * @param {Object} form * @param {Object} form_state */ function user_pass_form_submit(form, form_state) { user_request_new_password(form_state.values['name'], { success: function(result) { if (result[0]) { var msg = t('Further instructions have been sent to your e-mail address.'); drupalgap_set_message(msg); } else { var msg = t('There was a problem sending an e-mail to your address.'); drupalgap_set_message(msg, 'warning'); } var path = 'user/login'; if (form.action) { path = form.action; } if (_GET('destination')) { path = _GET('destination'); } drupalgap_goto(path, { reloadPage: true }); } }); } /** * Determine whether the user has a given privilege. Optionally pass in a user * account JSON object for the second paramater to check that particular * account. * @param {String} string The permission, such as "administer nodes", being * checked for. * @return {Boolean} */ function user_access(string) { try { var account; if (arguments[1]) { account = arguments[1]; } else { account = Drupal.user; } if (account.uid == 1) { return true; } var access = false; for (var index in account.permissions) { if (!account.permissions.hasOwnProperty(index)) { continue; } var object = account.permissions[index]; if (object.permission == string) { access = true; break; } } return access; } catch (error) { console.log('user_access - ' + error); } } /** * The access callback for the user/%/edit page. * @param {Object} account * @return {Boolean} */ function user_edit_access(account) { try { // If the current user is looking at their own account, or if they have the // 'administer users' permission, then they are allowed to edit the account. if (Drupal.user.uid == account.uid || user_access('administer users')) { return true; } return false; } catch (error) { console.log('user_edit_access - ' + error); } } /** * A page call back function to display a simple list of drupal users. * @return {Object} */ function user_listing() { var content = {}; content['user_list'] = { theme: 'view', format: 'ul', path: 'drupalgap/views_datasource/drupalgap_users', row_callback: 'user_listing_row', empty_callback: 'user_listing_empty', attributes: { id: 'user_listing_' + user_password() /* make a random id */ } }; return content; } /** * The row callback for the simple user listing page. * @param view * @param row * @returns {String} */ function user_listing_row(view, row) { try { return l(t(row.name), 'user/' + row.uid); } catch (error) { console.log('user_listing_row - ' + error); } } /** * The empty callback for the simple user listing page. * @param view * @returns {String} */ function user_listing_empty(view) { return t('Sorry, no users were found.'); } /** * The user logout page callback. * @return {String} */ function user_logout_callback() { return '

    ' + t('Logging out') + '...

    '; } /** * The user logout pageshow callback. This actually handles the call to the * user logout service resource. */ function user_logout_pageshow() { try { user_logout({ success: function(data) { drupalgap_remove_pages_from_dom(); drupalgap_goto(drupalgap.settings.front, { reloadPage:true }); } }); } catch (error) { console.log('user_logout_pagechange - ' + error); } } /** * Implements hook_menu(). * @return {Object} */ function user_menu() { var items = { 'user': { 'page_callback': 'user_page' }, 'user/login': { 'title': t('Login'), 'page_callback': 'drupalgap_get_form', 'page_arguments': ['user_login_form'], options: {reloadPage: true} }, 'user/logout': { 'title': t('Logout'), 'page_callback': 'user_logout_callback', 'pageshow': 'user_logout_pageshow', options: {reloadPage: true} }, 'user/register': { 'title': t('Register'), 'page_callback': 'drupalgap_get_form', 'page_arguments': ['user_register_form'], 'access_callback': 'user_register_access', options: {reloadPage: true} }, 'user/%': { title: t('My account'), title_callback: 'user_view_title', title_arguments: [1], page_callback: 'user_view', pageshow: 'user_view_pageshow', page_arguments: [1] }, 'user/%/view': { 'title': t('View'), 'type': 'MENU_DEFAULT_LOCAL_TASK', 'weight': -10 }, 'user/%/edit': { 'title': t('Edit'), 'page_callback': 'entity_page_edit', 'pageshow': 'entity_page_edit_pageshow', 'page_arguments': ['user_profile_form', 'user', 1], 'access_callback': 'user_edit_access', 'access_arguments': [1], 'weight': 0, 'type': 'MENU_LOCAL_TASK', options: {reloadPage: true} } }; items['user-listing'] = { title: t('Users'), page_callback: 'user_listing', access_arguments: ['access user profiles'] }; items['user/password'] = { title: t('Request new password'), page_callback: 'drupalgap_get_form', page_arguments: ['user_pass_form'] }; return items; } /** * Page callback for the user page. * @return {String} */ function user_page() { // NOTE, this page call back isn't actually used, because the 'user' path // in DrupalGap is redirected to either 'user/login' or e.g. // 'user/123'. return 'user_page()'; } /** * Access callback for the user registration page. * @return {Boolean} */ function user_register_access() { try { switch (drupalgap.site_settings.user_register.toString()) { case '0': // admins only can register return false; break; case '1': // visitors can register case '2': // visitors can register, but admin approval is needed return true; break; } } catch (error) { console.log('user_register_access - ' + error); } } /** * Implements hook_services_postprocess(). * @param {Object} options * @param {Object} result */ function user_services_postprocess(options, result) { try { // Don't process any other services. if (options.service != 'user') { return; } var resources = ['login', 'logout', 'register']; // Only process login, logout and registration. if (!in_array(options.resource, resources)) { return; } // If there were any form errors, alert them to the user. if (!result.responseText) { return; } var response = JSON.parse(result.responseText); if ($.isArray(response)) { var msg = ''; for (var index in response) { if (!response.hasOwnProperty(index)) { continue; } var message = response[index]; msg += t(message) + '\n'; } if (msg != '') { drupalgap_alert(msg); } } } catch (error) { console.log('user_services_postprocess - ' + error); } } /** * Implements hook_theme(). * @return {Object} */ function user_theme() { return { user_picture: { template: 'user-picture' }, user_profile: { template: 'user-profile' } }; } /** * Page callback for user/%. * @param {Number} uid * @return {Object} */ function user_view(uid) { try { if (uid) { var content = { container: _drupalgap_entity_page_container('user', uid, 'view') }; return content; } else { console.log('user_view - No user id provided!'); } } catch (error) { console.log('user_view - ' + error); } } /** * jQM pageshow handler for node/% pages. * @param {Number} uid */ function user_view_pageshow(uid) { try { user_load(uid, { success: function(account) { // Determine the incoming arguments, and set defaults if necessary. var view_mode = 'full'; var langcode = null; if (arguments[1]) { view_mode = arguments[1]; } if (arguments[2]) { langcode = arguments[2]; } if (!langcode) { langcode = language_default(); } if (account) { var build = { 'theme': 'user_profile', 'account': account, 'view_mode': view_mode, 'language': langcode, 'name': {'markup': account.name}, 'created': { markup: '

    ' + t('History') + '

    ' + '
    ' + t('Member since') + '
    ' + (new Date(parseInt(account.created) * 1000)).toDateString() + '
    ' } }; // Any content? if (typeof account.content !== 'undefined') { build.content = { markup: account.content }; } // Any picture? if (account.picture && account.picture.fid) { build.picture = { 'theme': 'image', 'path': image_style_url( drupalgap.site_settings.user_picture_style, account.picture.uri ) }; } _drupalgap_entity_page_container_inject( 'user', account.uid, 'view', build ); } } }); } catch (error) { console.log('user_view_pageshow - ' + error); } } /** * Title callback for the user profile view page. * @param {Function} callback * @param {Number} uid */ function user_view_title(callback, uid) { try { user_load(uid, { success: function(account) { callback.call(null, account.name); } }); } catch (error) { console.log('user_view_title - ' + error); } } /** * Given a user role (string), this determines if the current user has the role. * Returns true if the user has the role, false otherwise. You may pass in a * user account object to check against a certain account, instead of the * current user. * @param {String} role * @return {Boolean} */ function drupalgap_user_has_role(role) { try { var has_role = false; var account = null; if (arguments[1]) { account = arguments[1]; } else { account = Drupal.user; } for (var rid in account.roles) { if (!account.roles.hasOwnProperty(rid)) { continue; } var value = account.roles[rid]; if (role == value) { has_role = true; break; } } return has_role; } catch (error) { console.log('drupalgap_user_has_role - ' + error); } } // Holds onto views contexts on a per page basis. var _views_embedded_views = {}; /** * Gets an embedded view for the given page id. * @param {String} page_id * @returns {Object} */ function views_embedded_view_get(page_id) { try { if (!_views_embedded_views[page_id]) { return null; } var property = arguments[1]; if (!property) { return _views_embedded_views[page_id]; } return _views_embedded_views[page_id][property]; } catch (error) { console.log('views_embedded_view_get - ' + error); } } /** * Given a page id, property name and value, this will set the value for the * embedded view. * @param {String} page_id * @param {String} property * @param {*} value */ function views_embedded_view_set(page_id, property, value) { try { if (!_views_embedded_views[page_id]) { _views_embedded_views[page_id] = {}; } _views_embedded_views[page_id][property] = value; } catch (error) { console.log('views_embedded_view_set - ' + error); } } /** * Given a page id, this will delete the embedded view for the page from * memory. Returns true if successful, false otherwise. * @param page_id * @returns {boolean} */ function views_embedded_view_delete(page_id) { try { if (!_views_embedded_views[page_id]) { return false; } var property = arguments[1]; if (!property) { delete _views_embedded_views[page_id]; } else { delete _views_embedded_views[page_id][property]; } return true; } catch (error) { console.log('views_embedded_view_delete - ' + error); } } /** * Given a path to a Views Datasource (Views JSON) view, this will get the * results and pass them along to the provided success callback. * @param {String} path * @param {Object} options */ function views_datasource_get_view_result(path, options) { try { // Since we do not use clean URLs, replace any potential question marks from // the path with an ampersand so the path will not be invalid. if (path.indexOf('?') != -1) { var replacement = path.replace('?', '&'); path = replacement; } // If local storage caching is enabled, let's see if we can load the results // from there. If we successfully loaded the result, make sure it didn't // expire. If it did expire, remove it from local storage. If we don't have // it in local storage, or local storage caching is disabled, call Drupal to // get the results. After the results are fetched, save them to local // storage with an expiration time, if necessary. We use the path of the // view as the local storage key. // If we are resetting, remove the item from localStorage. if (options.reset) { window.localStorage.removeItem(path); } else if (Drupal.settings.cache.views.enabled) { var result = window.localStorage.getItem(path); if (result) { // Loaded from local storage, did it expire? result = JSON.parse(result); if (typeof result.expiration !== 'undefined' && result.expiration != 0 && time() > result.expiration ) { // Expired, remove from local storage. window.localStorage.removeItem(path); } else if (options.success) { // Did not expire yet, use it. options.success(result); return; } } } Drupal.services.call({ endpoint: '', service: 'views_datasource', resource: '', method: 'GET', path: path, success: function(result) { try { if (options.success) { // Add the path to the result. result.path = path; // If any views caching is enabled, cache the results in local // storage. if (Drupal.settings.cache.views.enabled) { var expiration = time() + Drupal.settings.cache.views.expiration; if (Drupal.settings.cache.views.expiration == 0) { expiration = 0; } // Saving to local storage. result.expiration = expiration; window.localStorage.setItem(path, JSON.stringify(result)); } options.success(result); } } catch (error) { console.log( 'views_datasource_get_view_result - success - ' + error ); } }, error: function(xhr, status, message) { try { if (options.error) { options.error(xhr, status, message); } } catch (error) { console.log('views_datasource_get_view_result - error - ' + error); } } }); } catch (error) { console.log('views_datasource_get_view_result - ' + error); } } /** * The exposed filter form builder. * @param {Object} form * @param {Object} form_state * @param {Object} options * @return {Object} */ function views_exposed_form(form, form_state, options) { try { //console.log(form); //console.log(form_state); //console.log(options); // @TODO we tried to make the filters collapsible, but jQM doesn't seem to // like collapsibles with form inputs in them... weird. //var title = form.title ? form.title : 'Filter'; //form.prefix += '

    ' + title + '

    '; // Attach the variables to the form so it can be used later. form.variables = options.variables; for (var views_field in options.filter) { if (!options.filter.hasOwnProperty(views_field)) { continue; } var filter = options.filter[views_field]; // Prep the element basics. var element_id = null; var element = null; // @TODO - This ID is NOT unique enough, it will cause DOM collisions if // the same exposed filter gets used twice... // @TODO - we should be attaching the value right here, so filter // implementors don't need to extract it on their own. Once this is // implemented. Then update *_views_exposed_filter() to use this value // instead. element_id = filter.options.expose.identifier; element = { id: element_id, type: filter.options.group_info.widget, title: filter.options.expose.label, required: filter.options.expose.required, options: { attributes: { id: drupalgap_form_get_element_id(element_id, form.id) } }, views_field: views_field, filter: filter, children: [] }; // Attach the value to the element, if there is one. // @TODO Add multi value support. //if (!empty(filter.value)) { element.value = filter.value[0]; } // Grab the field name and figure out which module is in charge of it. var field_name = filter.definition.field_name; if (field_name) { // This is an entity field... // Grab the field info, and determine the module that will handle it. // Then see if hook_views_exposed_filter() has been implemented by // that module. That module will be used to assemble the element. If // we don't have a handler then just skip the filter. var field = drupalgap_field_info_field(field_name); var module = field.module; var handler = module + '_views_exposed_filter'; if (!function_exists(handler)) { dpm( 'WARNING: views_exposed_form() - ' + handler + '() must be ' + 'created to assemble the ' + field.type + ' filter used by ' + field_name ); continue; } // We have a handler, let's call it so the element can be assembled. window[handler](form, form_state, element, filter, field); } else { // This is NOT an entity field, so it is probably a core field (e.g. // nid, status, etc). Let's assemble the element. In some cases we may // just be able to forward it to a pre-existing handler. // "Has taxonomy term" exposed filter. if (filter.definition.handler == 'views_handler_filter_term_node_tid') { element.type = 'autocomplete'; var autocomplete = { remote: true, custom: true, handler: 'index', entity_type: 'taxonomy_term', value: 'name', label: 'name', filter: 'name' }; if (filter.options.vocabulary != '') { autocomplete.vid = taxonomy_vocabulary_get_vid_from_name(filter.options.vocabulary); } $.extend(element, autocomplete, true); } // User ID exposed filter. else if (filter.definition.handler == 'views_handler_filter_user_name') { element.type = 'autocomplete'; var autocomplete = { remote: true, custom: true, handler: 'index', entity_type: 'user', value: 'name', label: 'name', filter: 'name' }; $.extend(element, autocomplete, true); } else if (element.type == 'select') { list_views_exposed_filter(form, form_state, element, filter, null); } else { dpm( 'WARNING: views_exposed_form() - I do not know how to handle ' + 'the exposed filter for the "' + views_field + '" field' ); console.log(filter); continue; } } // Finally attach the assembled element to the form. if (element_id) { form.elements[element_id] = element; } } // Add the submit button. form.elements['submit'] = { type: 'submit', value: options.exposed_data.submit }; // Add the reset button, if necessary. if ( options.exposed_data.reset && views_embedded_view_get(form.variables.page_id, 'exposed_filter_reset') ) { form.buttons['reset'] = { title: options.exposed_data.reset, attributes: { id: form.id + '-reset', onclick: 'views_exposed_form_reset()' } }; } //form.suffix += '
    '; return form; } catch (error) { console.log('views_exposed_form - ' + error); } } /** * The exposed filter submission handler. * @param {Object} form * @param {Object} form_state */ function views_exposed_form_submit(form, form_state) { try { var page_id = form.variables.page_id; // Assemble the query string from the form state values. var query = ''; for (var key in form_state.values) { if (!form_state.values.hasOwnProperty(key)) { continue; } var value = form_state.values[key]; if (empty(value)) { continue; } query += key + '=' + encodeURIComponent(value) + '&'; } if (!empty(query)) { query = query.substr(0, query.length - 1); } // If there is a query set aside from previous requests, and it is equal to // the submitted query, then stop the submission. Otherwise remove it from // the path. var _query = views_embedded_view_get(page_id, 'exposed_filter_query'); if (_query) { if (_query == query) { return; } if (form.variables.path.indexOf('&' + _query) != -1) { form.variables.path = form.variables.path.replace('&' + _query, ''); } } // Set aside a copy of the query string, so it can be removed from the path // upon subsequent submissions of the form. views_embedded_view_set(page_id, 'exposed_filter_query', query); // Indicate that we have an exposed filter, so the reset button can easily // be shown/hidden. views_embedded_view_set(page_id, 'exposed_filter_reset', true); // Update the path for the view, reset the pager, hold onto the variables, // then theme the view. form.variables.path += '&' + query; form.variables.page = 0; views_embedded_view_set( page_id, 'exposed_filter_submit_variables', form.variables ); _theme_view(form.variables); } catch (error) { console.log('views_exposed_form_submit - ' + error); } } /** * Handles clicks on the views exposed filter form's reset button. */ function views_exposed_form_reset() { try { var page_id = drupalgap_get_page_id(); // Reset the path to the view, the page, and the global vars, then re-theme // the view. var exposed_filter_submit_variables = views_embedded_view_get(page_id, 'exposed_filter_submit_variables'); exposed_filter_submit_variables.path = exposed_filter_submit_variables.path.replace( '&' + views_embedded_view_get(page_id, 'exposed_filter_query'), '' ); exposed_filter_submit_variables.page = 0; views_embedded_view_set( page_id, 'exposed_filter_submit_variables', exposed_filter_submit_variables ); views_embedded_view_set(page_id, 'exposed_filter_reset', false); views_embedded_view_set(page_id, 'exposed_filter_query', null); _theme_view(exposed_filter_submit_variables); } catch (error) { console.log('views_exposed_form_reset - ' + error); } } /** * Themes a view. * @param {Object} variables * @return {String} */ function theme_view(variables) { try { // Create a random id attribute if one wasn't provided. if (!variables.attributes.id) { variables.attributes.id = 'views-view--' + user_password(); } // Since multiple pages can stack up in the DOM, warn the developer if they // re-use a Views ID that is already in the DOM. If the ID hasn't been used // yet, add it to the drupalgap.views.ids array. if (in_array(variables.attributes.id, drupalgap.views.ids)) { // Double check to make sure it is actually in the DOM. If it wasn't // really in the DOM, remove the id from drupalgap.views.ids and continue // onward. if (!$('#' + variables.attributes.id)) { // @see http://stackoverflow.com/a/3596141/763010 drupalgap.views.ids.splice( $.inArray(variables.attributes.id, drupalgap.views.ids), 1 ); } else { dpm( 'WARNING: theme_view() - this id already exists in the DOM: #' + variables.attributes.id + ' - the view will be rendered into the first container that is ' + 'located in the DOM - if you are re-using this same view, it is ' + 'recommended to append a unique identifier (e.g. an entity id) to ' + 'your views id, that way you can re-use the same view across ' + 'multiple pages.' ); } } else { drupalgap.views.ids.push(variables.attributes.id); } // Keep track of the page id for context. var page_id = drupalgap_get_page_id(); variables.page_id = page_id; // Since we'll by making an asynchronous call to load the view, we'll just // return an empty div container, with a script snippet to load the view. variables.attributes['class'] += 'view '; var html = '
    '; var options = { page_id: page_id, jqm_page_event: 'pageshow', jqm_page_event_callback: '_theme_view', jqm_page_event_args: JSON.stringify(variables) }; return html += drupalgap_jqm_page_event_script_code(options, variables.attributes.id); } catch (error) { console.log('theme_view - ' + error); } } /** * An internal function used to theme a view. * @param {Object} variables */ function _theme_view(variables) { try { // Determine what page of the view we're on. var page = 0; if (variables.page) { page = variables.page; } // Make a copy of variables and prepare the success callback for embedding // the view. var variables_copy = $.extend( {}, { success: function(html) { try { $('#' + variables.attributes.id).html(html); } catch (error) { console.log('_theme_view - success - ' + error); } } }, variables ); // Finally, embed the view. views_embed_view(variables.path + '&page=' + page, variables_copy); } catch (error) { console.log('_theme_view - ' + error); } } /** * Returns the html string to options.success, used to embed a view. * @param {String} path * @param {Object} options */ function views_embed_view(path, options) { try { views_datasource_get_view_result(path, { success: function(results) { try { views_embedded_view_set(options.page_id, 'results', results); views_embedded_view_set(options.page_id, 'options', options); if (!options.success) { return; } options.results = results; // Render the view if there are some results, or if there are no results and an // empty_callback has been specified. Otherwise remove the empty div container for // the view from the DOM. if (results.view.count != 0 || results.view.count == 0 && options.empty_callback) { options.success(theme('views_view', options)); } else { var elem = document.getElementById(options.attributes.id); elem.parentElement.removeChild(elem); } } catch (error) { console.log('views_embed_view - success - ' + error); } }, error: function(xhr, status, message) { try { views_embedded_view_set(options.page_id, 'results', null); if (options.error) { options.error(xhr, status, message); } } catch (error) { console.log('views_embed_view - error - ' + error); } } }); } catch (error) { console.log('views_embed_view - ' + error); } } /** * Theme's a view. * @param {Object} variables * @return {String} */ function theme_views_view(variables) { try { var html = ''; // Extract the results. var results = views_embedded_view_get(variables.page_id, 'results'); if (!results) { return html; } // Figure out the format. if (!variables.format) { variables.format = 'unformatted_list'; } // Extract the root and child object name. var root = results.view.root; var child = results.view.child; // Is there a title to display? if (variables.title) { var title_attributes = variables.title_attributes ? variables.title_attributes : null; html += theme( 'header', { text: variables.title, attributes: title_attributes } ); // Place spacers after the header for each format, except unformatted. if (variables.format != 'unformatted') { html += theme('views_spacer', null); } } // Render the exposed filters if there are any, and the developer didn't explicitly exclude // them via the Views Render Array. var views_exposed_form_html = ''; if (typeof results.view.exposed_data !== 'undefined' && (typeof variables.exposed_filters === 'undefined' || variables.exposed_filters) ) { views_exposed_form_html = drupalgap_get_form( 'views_exposed_form', { exposed_data: results.view.exposed_data, exposed_raw_input: results.view.exposed_raw_input, filter: results.view.filter, variables: variables } ); } // Determine views container selector and place it in the global context. var selector = '#' + variables.page_id + ' #' + variables.attributes.id; views_embedded_view_set(variables.page_id, 'selector', selector); // Are the results empty? If so, return the empty callback's html, if it // exists. Often times, the empty callback will want to place html that // needs to be enhanced by jQM, therefore we'll set a timeout to trigger // the creation of the content area. // @TODO putting views_litepager support here is a hack, we should be // implementing views_litepager_views_view for theme_views_view() instead. var views_litepager_present = module_exists('views_litepager'); if ( (results.view.count == 0 && !views_litepager_present) || (views_litepager_present && results.view.pages == null && results.view.count == 0) ) { $(selector).hide(); setTimeout(function() { $(selector).trigger('create').show('fast'); }, 100); if (variables.empty_callback && function_exists(variables.empty_callback)) { var empty_callback = window[variables.empty_callback]; return views_exposed_form_html + drupalgap_render(empty_callback(results.view, variables)); } return html + views_exposed_form_html; } // The results are not empty... // Append the exposed filter html. html += views_exposed_form_html; // Render all the rows. var result_formats = drupalgap_views_get_result_formats(variables); var rows = '' + result_formats.open + drupalgap_views_render_rows( variables, results, root, child, result_formats.open_row, result_formats.close_row ) + result_formats.close; // If we have any pages, render the pager above or below the results // according to the pager_pos setting. var pager = ''; if (results.view.pages) { pager = theme('pager', variables); } var pager_pos = 'top'; if (typeof variables.pager_pos !== 'undefined') { pager_pos = variables.pager_pos; } // Append the rendered rows and the pager to the html string according to // the pager position, unless the views infinite scroll module is enabled, // then no pager at all. // @TODO having this special case for views_infinite_scroll is a hack, we // obviously need a hook or something around here... if ( module_exists('views_infinite_scroll') && views_infinite_scroll_ok() ) { html += rows; } else if (pager_pos == 'top') { html += pager; if (!empty(pager)) { html += theme('views_spacer', null); } html += rows; } else if (pager_pos == 'bottom') { html += rows; if (!empty(pager)) { html += theme('views_spacer', null); } html += pager; } else { console.log('WARNING: theme_views_view - unsupported pager_pos (' + pager_pos + ')'); } // Since the views content is injected dynamically after the page is loaded, // we need to have jQM refresh the page to add its styling. $(selector).hide(); setTimeout(function() { $(selector).trigger('create').show('fast'); }, 100); return html; } catch (error) { console.log('theme_views_view - ' + error); } } /** * Themes a spacer that can be placed between displayed components of the view. * @param {Object} variables * @return {String} */ function theme_views_spacer(variables) { return '

     

    '; } /** * Themes a pager. * @param {Object} variables * @return {String} */ function theme_pager(variables) { try { var html = ''; // Extract the view and pager data. var view = variables.results.view; var page = view.page; var pages = view.pages; var count = view.count; var limit = view.limit; var page = view.page; // If we don't have any results, return. // @TODO putting views_litepager support here is a hack, we should be // implementing views_litepager_pager() for theme_pager() instead. var views_litepager_present = module_exists('views_litepager'); if ( (count == 0 && !views_litepager_present) || (views_litepager_present && variables.results.view.pages == null) ) { return html; } // Add the pager items to the list. var items = []; if (page != 0) { items.push(theme('pager_previous', variables)); } if ( (page != pages - 1 && !views_litepager_present) || views_litepager_present ) { items.push(theme('pager_next', variables)); } if (items.length > 0) { // Make sure we have an id to use since we need to dynamically build the // navbar container for the pager. If we don't have one, generate a random // one. var id = 'theme_pager_' + user_password(); var attrs = { id: id, 'class': 'pager', 'data-role': 'navbar' }; html += '
    ' + theme('item_list', { items: items }) + '
    ' + ''; } return html; } catch (error) { console.log('theme_pager - ' + error); } } /** * Themes a pager link. * @param {Object} variables * @param {Object} link_vars * @return {String} */ function theme_pager_link(variables, link_vars) { try { if (!link_vars.attributes) { link_vars.attributes = {}; } link_vars.attributes.href = '#'; var attributes = drupalgap_attributes(link_vars.attributes); return "' + link_vars.text + ''; } catch (error) { console.log('theme_pager_link - ' + error); } } /** * An internal function used to generate the onclick handler JS for a pager * link. * @param {Object} variables * @return {String} */ function _theme_pager_link_onclick(variables) { try { // Make a copy of variables. While doing so remove any results from it // because we don't want them in the onclick handler's html. The results // will be available in the _views_embed_view_results variable if they are // needed var copy = $.extend({ }, { }, variables); if (copy.results) { delete copy.results; } var onclick = '_theme_pager_link_click(' + JSON.stringify(copy) + ')'; return onclick; } catch (error) { console.log('_theme_pager_link_onclick - ' + error); } } /** * An internal function used to handle clicks on pager links. * @param {Object} variables */ function _theme_pager_link_click(variables) { try { _theme_view(variables); } catch (error) { console.log('_theme_pager_link_click - ' + error); } } /** * Themes a pager next link. * @param {Object} variables * @return {String} */ function theme_pager_next(variables) { try { var html; variables.page = parseInt(variables.results.view.page) + 1; var link_vars = { text: '»', attributes: { 'class': 'pager_next' } }; html = theme_pager_link(variables, link_vars); return html; } catch (error) { console.log('theme_pager_next - ' + error); } } /** * Themes a pager previous link. * @param {Object} variables * @return {String} */ function theme_pager_previous(variables) { try { var html; variables.page = parseInt(variables.results.view.page) - 1; var link_vars = { text: '«', attributes: { 'class': 'pager_previous' } }; html = theme_pager_link(variables, link_vars); return html; } catch (error) { console.log('theme_pager_previous - ' + error); } } /** * A helper function used to retrieve the various open and closing tags for * views results, depending on their format. * @param {Object} variables * @return {Object} */ function drupalgap_views_get_result_formats(variables) { try { var result_formats = {}; // Depending on the format, let's render the container opening and closing, // and then render the rows. if (!variables.format) { variables.format = 'unformatted_list'; } var open = ''; var close = ''; var open_row = ''; var close_row = ''; // Prepare the format's container attributes. var format_attributes = {}; if (typeof variables.format_attributes !== 'undefined') { format_attributes = $.extend( true, format_attributes, variables.format_attributes ); } // Add a views-results class if (typeof format_attributes['class'] === 'undefined') { format_attributes['class'] = ''; } format_attributes['class'] += ' views-results '; switch (variables.format) { case 'grid': // Determine columns and jqm grid type. var columns = jqm_grid_verify_columns(variables); var grid = jqm_grid_get_type(columns); // Prep the container class name. if (!format_attributes.class) { format_attributes.class = ''; } format_attributes.class += ' ui-grid-' + grid + ' '; // Build the strings. open = '
    '; close = '
    '; open_row = '
    '; // This class will be replaced dynamically. close_row = '
    '; break; case 'ul': if (typeof format_attributes['data-role'] === 'undefined') { format_attributes['data-role'] = 'listview'; } open = ''; open_row = '
  • '; close_row = '
  • '; break; case 'ol': if (typeof format_attributes['data-role'] === 'undefined') { format_attributes['data-role'] = 'listview'; } open = '
      '; close = '
    '; open_row = '
  • '; close_row = '
  • '; break; case 'table': case 'jqm_table': if (variables.format == 'jqm_table') { if (typeof format_attributes['data-role'] === 'undefined') { format_attributes['data-role'] = 'table'; } if (typeof format_attributes['data-mode'] === 'undefined') { format_attributes['data-mode'] = 'reflow'; } console.log( 'WARNING: theme_views_view() - jqm_table not supported, yet' ); } open = ''; close = '
    '; open_row = ''; close_row = ''; break; case 'unformatted_list': default: if (typeof format_attributes['class'] === 'undefined') { format_attributes['class'] = ''; } format_attributes['class'] += ' views-rows'; open = '
    '; close = '
    '; open_row = ''; close_row = ''; break; } result_formats.open = open; result_formats.close = close; result_formats.open_row = open_row; result_formats.close_row = close_row; return result_formats; } catch (error) { console.log('drupalgap_views_get_result_formats - ' + error); } } /** * A helper function used to render a views result's rows. * @param {Object} * @param {Object} * @param {String} * @param {String} * @param {String} * @param {String} * @return {String} */ function drupalgap_views_render_rows(variables, results, root, child, open_row, close_row) { try { var html = ''; var totalRows = results[root].length; for (var count in results[root]) { if (!results[root].hasOwnProperty(count)) { continue; } // Extract the row and mark its position. var object = results[root][count]; var row = object[child]; row._position = parseInt(count); // If a row_callback function exists, call it to render the row, // otherwise use the default row render mechanism. var row_content = ''; if (variables.row_callback && function_exists(variables.row_callback)) { row_callback = window[variables.row_callback]; row_content = row_callback(results.view, row, variables); } else { row_content = JSON.stringify(row); } // If we're rendering a grid, replace the class name for the current column. Otherwise // just render the row as usual. if (variables.format == 'grid') { var className = jqm_grid_get_item_class(row._position, variables.columns); var openRow = jqm_grid_set_item_row_class(open_row, className); html += openRow + row_content + close_row; } else { html += open_row + row_content + close_row; } } return html; } catch (error) { console.log('drupalgap_views_render_rows - ' + error); } } /** * @deprecated - Use views_datasource_get_view_result() instead. */ drupalgap.views_datasource = { 'options': { }, 'call': function(options) { try { var msg = 'WARNING: drupalgap.views_datasource has been deprecated! ' + 'Use views_datasource_get_view_result() instead.'; console.log(msg); views_datasource_get_view_result(options.path, options); } catch (error) { console.log('drupalgap.views_datasource - ' + error); } } };