diff --git a/core/COPYRIGHT.txt b/core/COPYRIGHT.txt index 1c4ad0e..c1671c2 100644 --- a/core/COPYRIGHT.txt +++ b/core/COPYRIGHT.txt @@ -47,6 +47,8 @@ Javascript jQuery Once - Copyright (c) 2009 Konstantin Käfer + jQuery tree - (c) 2009 Filament Group + jQuery UI - Copyright (c) 2012 by the original authors (http://jqueryui.com/about) diff --git a/core/misc/jquery.tree.js b/core/misc/jquery.tree.js new file mode 100644 index 0000000..0195a2b --- /dev/null +++ b/core/misc/jquery.tree.js @@ -0,0 +1,180 @@ +/** + * -------------------------------------------------------------------- + * jQuery tree plugin + * Author: Scott Jehl, scott@filamentgroup.com + * Copyright (c) 2009 Filament Group + * licensed under MIT (filamentgroup.com/examples/mit-license.txt) + * -------------------------------------------------------------------- + */ +;(function($) { + +$.fn.tree = function(settings){ + var o = $.extend({ + expanded: '' + },settings); + + return $(this).each(function(){ + if( !$(this).parents('.tree').length ){ + //save reference to tree UL + var tree = $(this); + + //add the role and default state attributes + if( !$('body').is('[role]') ){ $('body').attr('role','application'); } + //add role and class of tree + tree.attr({'role': 'tree'}).addClass('tree'); + //set first node's tabindex to 0 + tree.find('a:eq(0)').attr('tabindex','0'); + //set all others to -1 + tree.find('a:gt(0)').attr('tabindex','-1'); + //add group role and tree-group-collapsed class to all ul children + tree.find('ul').attr('role','group').addClass('tree-group-collapsed'); + //add treeitem role to all li children + tree.find('li').attr('role','treeitem'); + //find tree group parents + tree.find('li:has(ul)') + .attr('aria-expanded', 'false') + .find('>a') + .addClass('tree-parent tree-parent-collapsed'); + + //expanded at load + tree + .find(o.expanded) + .attr('aria-expanded', 'true') + .find('>a') + .removeClass('tree-parent-collapsed') + .next() + .removeClass('tree-group-collapsed'); + + + //bind the custom events + tree + //expand a tree node + .bind('expand',function(event){ + var target = $(event.target) || tree.find('a[tabindex=0]'); + target.removeClass('tree-parent-collapsed'); + target.next().hide().removeClass('tree-group-collapsed').slideDown(150, function(){ + $(this).removeAttr('style'); + target.parent().attr('aria-expanded', 'true'); + }); + }) + //collapse a tree node + .bind('collapse',function(event){ + var target = $(event.target) || tree.find('a[tabindex=0]'); + target.addClass('tree-parent-collapsed'); + target.next().slideUp(150, function(){ + target.parent().attr('aria-expanded', 'false'); + $(this).addClass('tree-group-collapsed').removeAttr('style'); + }); + }) + .bind('toggle',function(event){ + var target = $(event.target) || tree.find('a[tabindex=0]'); + //check if target parent LI is collapsed + if( target.parent().is('[aria-expanded=false]') ){ + //call expand function on the target + target.trigger('expand'); + } + //otherwise, parent must be expanded + else{ + //collapse the target + target.trigger('collapse'); + } + }) + //shift focus down one item + .bind('traverseDown',function(event){ + var target = $(event.target) || tree.find('a[tabindex=0]'); + var targetLi = target.parent(); + if(targetLi.is('[aria-expanded=true]')){ + target.next().find('a').eq(0).focus(); + } + else if(targetLi.next().length) { + targetLi.next().find('a').eq(0).focus(); + } + else { + targetLi.parents('li').next().find('a').eq(0).focus(); + } + }) + //shift focus up one item + .bind('traverseUp',function(event){ + var target = $(event.target) || tree.find('a[tabindex=0]'); + var targetLi = target.parent(); + if(targetLi.prev().length){ + if( targetLi.prev().is('[aria-expanded=true]') ){ + targetLi.prev().find('li:visible:last a').eq(0).focus(); + } + else{ + targetLi.prev().find('a').eq(0).focus(); + } + } + else { + targetLi.parents('li:eq(0)').find('a').eq(0).focus(); + } + }); + + + //and now for the native events + tree + .focus(function(event){ + //deactivate previously active tree node, if one exists + tree.find('[tabindex=0]').attr('tabindex','-1').removeClass('tree-item-active'); + //assign 0 tabindex to focused item + $(event.target).attr('tabindex','0').addClass('tree-item-active'); + }) + .click(function(event){ + //save reference to event target + var target = $(event.target); + //check if target is a tree node + if( target.is('a.tree-parent') ){ + target.trigger('toggle'); + target.eq(0).focus(); + //return click event false because it's a tree node (folder) + return false; + } + }) + .keydown(function(event){ + var target = tree.find('a[tabindex=0]'); + //check for arrow keys + if(event.keyCode == 37 || event.keyCode == 38 || event.keyCode == 39 || event.keyCode == 40){ + //if key is left arrow + if(event.keyCode == 37){ + //if list is expanded + if(target.parent().is('[aria-expanded=true]')){ + target.trigger('collapse'); + } + //try traversing to parent + else { + target.parents('li:eq(1)').find('a').eq(0).focus(); + } + } + //if key is right arrow + if(event.keyCode == 39){ + //if list is collapsed + if(target.parent().is('[aria-expanded=false]')){ + target.trigger('expand'); + } + //try traversing to child + else { + target.parents('li:eq(0)').find('li a').eq(0).focus(); + } + } + //if key is up arrow + if(event.keyCode == 38){ + target.trigger('traverseUp'); + } + //if key is down arrow + if(event.keyCode == 40){ + target.trigger('traverseDown'); + } + //return any of these keycodes false + return false; + } + //check if enter or space was pressed on a tree node + else if((event.keyCode == 13 || event.keyCode == 32) && target.is('a.tree-parent')){ + target.trigger('toggle'); + //return click event false because it's a tree node (folder) + return false; + } + }); + } + }); +}; +})(jQuery); diff --git a/core/modules/system/system.module b/core/modules/system/system.module index ed4f0c2..456512e 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -1999,6 +1999,16 @@ function system_library_info() { ), ); + // jQuery tree plugin. + $libraries['jquery.tree'] = array( + 'title' => 'jQuery tree', + 'website' => 'https://github.com/filamentgroup/jQuery-Tree-Control', + 'version' => '1.0', + 'js' => array( + 'core/misc/jquery.tree.js' => array('group' => JS_LIBRARY), + ), + ); + return $libraries; } diff --git a/core/modules/token/arrow-down.png b/core/modules/token/arrow-down.png new file mode 100644 index 0000000..2edbb17 --- /dev/null +++ b/core/modules/token/arrow-down.png @@ -0,0 +1,4 @@ +‰PNG + + IHDR H%v?PLTEÿÿÿ···kR%»tRNS@æØfIDATxœc`@õì0È`ào``g``‚ +KzHƒ“ÏIEND®B`‚ \ No newline at end of file diff --git a/core/modules/token/arrow-right.png b/core/modules/token/arrow-right.png new file mode 100644 index 0000000..6f2f482 --- /dev/null +++ b/core/modules/token/arrow-right.png @@ -0,0 +1,3 @@ +‰PNG + + IHDR rëä|sBIT|dˆ6IDAT(‘cøÿÿ?©«àöíÛÿ“¥ ŸF¼špi$¨ ›F¢4¡k$Z²FÚÙDûУ}Š „Óþ‘¿Ö÷éIEND®B`‚ \ No newline at end of file diff --git a/core/modules/token/token.css b/core/modules/token/token.css new file mode 100644 index 0000000..f0bd301 --- /dev/null +++ b/core/modules/token/token.css @@ -0,0 +1,62 @@ +/** + * @file Minimal CSS for token browsing. + */ + +/** + * Basic CSS for without the JS overrides. + */ + +body.token-tree { + margin-left: 10px; + margin-right: 10px; +} + +.item-list ul.token-tree li { + padding-left: 10px; + padding-right: 10px; + margin-left: 10px; + margin-right: 10px; + list-style: disc; +} + +.item-list ul.token-tree li li, +.item-list ul.token-tree li li li li, +.item-list ul.token-tree li li li li li li, +.item-list ul.token-tree li li li li li li li li { + list-style: square; +} + +.item-list ul.token-tree li li li, +.item-list ul.token-tree li li li li li, +.item-list ul.token-tree li li li li li li li, +.item-list ul.token-tree li li li li li li li li li { + list-style: disc; +} + +/** + * CSS to handle the expand/collapse JavaScript + */ + +.item-list ul.token-tree-processed li, +.item-list ul.token-tree-processed li li, +.item-list ul.token-tree-processed li li li, +.item-list ul.token-tree-processed li li li li, +.item-list ul.token-tree-processed li li li li li, +.item-list ul.token-tree-processed li li li li li li, +.item-list ul.token-tree-processed li li li li li li li, +.item-list ul.token-tree-processed li li li li li li li li { + list-style: none; +} + +.item-list ul.token-tree-processed li a.tree-parent { + background: url('arrow-down.png') no-repeat; + padding-left: 15px; +} + +.item-list ul.token-tree-processed li a.tree-parent-collapsed { + background: url('arrow-right.png') no-repeat; +} + +.item-list ul.tree-group-collapsed { + display: none; +} diff --git a/core/modules/token/token.info b/core/modules/token/token.info new file mode 100644 index 0000000..de910b6 --- /dev/null +++ b/core/modules/token/token.info @@ -0,0 +1,5 @@ +name = Token +description = Provides a user interface for tokens. +package = Core +version = VERSION +core = 8.x diff --git a/core/modules/token/token.js b/core/modules/token/token.js new file mode 100644 index 0000000..7f224b1 --- /dev/null +++ b/core/modules/token/token.js @@ -0,0 +1,83 @@ +/** + * @file + * JavaScript behaviors for the token selector tree. + */ + +(function ($) { + +Drupal.behaviors.tokenTree = { + attach: function (context, settings) { + $(context).find('ul.token-tree').addClass('token-tree-processed').tree(); + } +}; + +Drupal.behaviors.tokenDialog = { + attach: function (context, settings) { + $('a.token-dialog', context).once('token-dialog').click(function() { + var url = $(this).attr('href'); + var dialog = $('').appendTo('body'); + dialog.dialog({ + title: $(this).attr('title') || Drupal.t('Available tokens'), + width: 700, + close: function(event, ui) { + dialog.remove(); + } + }); + // Load the token tree using AJAX. + dialog.load( + url, + {}, + function (responseText, textStatus, XMLHttpRequest) { + dialog.removeClass('loading'); + } + ); + // Prevent browser from following the link. + return false; + }); + } +} + +Drupal.behaviors.tokenInsert = { + attach: function (context, settings) { + // Keep track of which textfield was last selected/focused. + $('textarea, input[type="text"]', context).focus(function() { + Drupal.settings.tokenFocusedField = this; + }); + + $('.token-click-insert .token-key', context).once('token-click-insert', function() { + var newThis = $('' + $(this).html() + '').click(function(){ + if (typeof Drupal.settings.tokenFocusedField == 'undefined') { + alert(Drupal.t('First click a text field to insert your tokens into.')); + } + else { + var myField = Drupal.settings.tokenFocusedField; + var myValue = $(this).text(); + + //IE support + if (document.selection) { + myField.focus(); + sel = document.selection.createRange(); + sel.text = myValue; + } + + //MOZILLA/NETSCAPE support + else if (myField.selectionStart || myField.selectionStart == '0') { + var startPos = myField.selectionStart; + var endPos = myField.selectionEnd; + myField.value = myField.value.substring(0, startPos) + + myValue + + myField.value.substring(endPos, myField.value.length); + } else { + myField.value += myValue; + } + + $('html,body').animate({scrollTop: $(myField).offset().top}, 500); + } + return false; + }); + $(this).html(newThis); + }); + } +}; + +})(jQuery); diff --git a/core/modules/token/token.module b/core/modules/token/token.module new file mode 100644 index 0000000..4667342 --- /dev/null +++ b/core/modules/token/token.module @@ -0,0 +1,427 @@ +' . t('About') . ''; + $output .= '

' . t('The Token module provides a token browser, which allows you to select tokens to insert into fields that accept tokens.') . '

'; + return $output; + } +} + +/** + * Implements hook_menu(). + */ +function token_menu() { + $items['token/autocomplete/%token_type'] = array( + 'page callback' => 'token_autocomplete_token', + 'page arguments' => array(2), + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + 'file' => 'token.pages.inc', + ); + + $items['token/tree'] = array( + 'page callback' => 'token_page_output_tree', + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + 'file' => 'token.pages.inc', + ); + + return $items; +} + +/** + * Loads a token type for menu router items. + * + * @param string $token_type + * String representing the type of token to load (generally the module or + * entity name). + * + * @return array + * Array containing the token information for this token type. + */ +function token_type_load($token_type) { + $info = token_get_info(); + return isset($info['types'][$token_type]) ? $info['types'][$token_type] : FALSE; +} + +/** + * Implements hook_theme(). + */ +function token_theme() { + // Displays a tree of available tokens. + $info['token_tree'] = array( + 'variables' => array( + // Array of token types to list. + 'token_types' => array(), + // TRUE to also include global token types. + 'global_types' => TRUE, + // TRUE to have "click to insert" functionality. + 'click_insert' => TRUE, + // TRUE to show restricted tokens. + 'show_restricted' => FALSE, + // Depth limit for tree. + 'recursion_limit' => 3, + // TRUE to display as a flat list; FALSE as nested list. + 'flat' => FALSE, + ), + 'file' => 'token.pages.inc', + ); + + // Link to pop up a token tree in a dialog or new window. + $info['token_tree_link'] = array( + 'variables' => array( + // Link text: defaults to t('Browse available tokens'). + 'text' => NULL, + // Options for the URL. + 'options' => array(), + // TRUE to display using a JavaScript dialog; FALSE as new window. + 'dialog' => TRUE, + ), + 'file' => 'token.pages.inc', + ); + + // This is just like theme('item_list') but without the extra divs, + // since they don't work with the tree control. + $info['tree_item_list'] = array( + 'variables' => array( + 'title' => '', + 'items' => array(), + 'type' => '', + 'attributes' => array(), + ), + 'file' => 'token.pages.inc', + ); + + return $info; +} + +/** + * Implements hook_library_info(). + */ +function token_library_info() { + $libraries['token_dialog'] = array( + 'title' => 'Token dialog', + 'version' => '1.0', + 'js' => array( + drupal_get_path('module', 'token') . '/token.js' => array(), + ), + 'css' => array( + drupal_get_path('module', 'token') . '/token.css' => array(), + ), + 'dependencies' => array( + array('system', 'jquery.ui.dialog'), + array('system', 'jquery.tree'), + array('system', 'drupal'), + ), + ); + + return $libraries; +} + +/** + * Retrieves and sorts token information. + * + * @param string $token_type + * (optional) A particular token type to return information for. + * @param string $token + * (optional) A particular token name within $token_type to return + * information for. + * + * @return array + * An array of all token information from hook_token_info(), or the array + * of a token type or specific token. + * + * @see hook_token_info() + * @see hook_token_info_alter() + */ +function token_get_info($token_type = NULL, $token = NULL) { + global $language; + + // @todo This function needs to use database caching. + + // Use the advanced drupal_static() pattern, since this is called very often. + static $drupal_static_fast; + if (!isset($drupal_static_fast)) { + $drupal_static_fast['token_info'] = &drupal_static(__FUNCTION__); + } + $token_info = &$drupal_static_fast['token_info']; + + if (empty($token_info)) { + $token_info = token_info(); + + foreach (array_keys($token_info['types']) as $type_key) { + if (isset($token_info['types'][$type_key]['type'])) { + $base_type = $token_info['types'][$type_key]['type']; + // If this token type extends another token type, then merge in + // the base token type's tokens. + if (isset($token_info['tokens'][$base_type])) { + $token_info['tokens'] += array($type_key => array()); + $token_info['tokens'][$type_key] += $token_info['tokens'][$base_type]; + } + } + else { + // Add a 'type' value to each token type so we can properly use + // token_type_load(). + $token_info['types'][$type_key]['type'] = $type_key; + } + } + + // Pre-sort tokens. + uasort($token_info['types'], 'token_asort_tokens'); + foreach (array_keys($token_info['tokens']) as $type) { + uasort($token_info['tokens'][$type], 'token_asort_tokens'); + } + } + + if (isset($token_type) && isset($token)) { + return isset($token_info['tokens'][$token_type][$token]) ? $token_info['tokens'][$token_type][$token] : NULL; + } + elseif (isset($token_type)) { + return isset($token_info['types'][$token_type]) ? $token_info['types'][$token_type] : NULL; + } + else { + return $token_info; + } +} + +/** + * Sorts tokens by the 'name' property. + * + * Callback for uasort() within token_get_info(). + */ +function token_asort_tokens($token_a, $token_b) { + return strnatcmp($token_a['name'], $token_b['name']); +} + +/** + * Gets a list of token types that can be used without any context (global). + * + * @return + * An array of global token types. + */ +function token_get_global_token_types() { + $global_types = &drupal_static(__FUNCTION__, array()); + + // @todo This function needs to use database caching. + + if (empty($global_types)) { + $token_info = token_get_info(); + foreach ($token_info['types'] as $type => $type_info) { + // If the token types has not specified that 'needs-data' => TRUE, then + // it is a global token type that will always be replaced in any context. + if (empty($type_info['needs-data'])) { + $global_types[] = $type; + } + } + } + + return $global_types; +} + +/** + * Builds a tree array of tokens used for theming or information. + * + * @param string $token_type + * The token type. + * @param array $options + * Array containing options governing how the tree is built: + * - restricted: TRUE to show restricted tokens; FALSE (default) to hide them. + * - depth: Maximum depth to traverse in the token tree. + * - flat: TRUE to return a flat array; FALSE (default) to return a tree. + * + * @return array + * Array containing the tree of tokens. + */ +function token_build_tree($token_type, array $options = array()) { + global $language; + + // Static cache of already built token trees. + $trees = &drupal_static(__FUNCTION__, array()); + + // @todo This function needs to use database caching. + + $options += array( + 'restricted' => FALSE, + 'depth' => 4, + 'data' => array(), + 'values' => FALSE, + 'flat' => FALSE, + ); + + // Do not allow past the maximum token information depth. + $options['depth'] = min($options['depth'], TOKEN_MAX_DEPTH); + + // If $token_type is an entity, make sure we are using the actual token type. + if ($entity_token_type = token_get_entity_mapping('entity', $token_type)) { + $token_type = $entity_token_type; + } + + $tree_cid = "tree:{$token_type}:{$language->language}:{$options['depth']}"; + + // If we do not have this base tree in the static cache, check {cache_token} + // otherwise generate and store it in the cache. + if (!isset($trees[$tree_cid])) { + $options['parents'] = array(); + $trees[$tree_cid] = _token_build_tree($token_type, $options); + } + + $tree = $trees[$tree_cid]; + + // If the user has requested a flat tree, convert it. + if (!empty($options['flat'])) { + $tree = token_flatten_tree($tree); + } + + // Fill in token values. + if (!empty($options['values'])) { + $token_values = array(); + foreach ($tree as $token => $token_info) { + if (!empty($token_info['dynamic']) || !empty($token_info['restricted'])) { + continue; + } + elseif (!isset($token_info['value'])) { + $token_values[$token_info['token']] = $token; + } + } + if (!empty($token_values)) { + $token_values = token_generate($token_type, $token_values, $options['data']); + foreach ($token_values as $token => $replacement) { + $tree[$token]['value'] = $replacement; + } + } + } + + return $tree; +} + +/** + * Flattens a token tree. + * + * @param array $tree + * The tree to flatten. + * + * @return array + * The flattened tree. + */ +function token_flatten_tree($tree) { + $result = array(); + foreach ($tree as $token => $token_info) { + $result[$token] = $token_info; + if (isset($token_info['children']) && is_array($token_info['children'])) { + $result += token_flatten_tree($token_info['children']); + } + } + return $result; +} + +/** + * Generates a token tree. + */ +function _token_build_tree($token_type, array $options) { + $options += array( + 'parents' => array(), + ); + + $info = token_get_info(); + if ($options['depth'] <= 0 || !isset($info['types'][$token_type]) || !isset($info['tokens'][$token_type])) { + return array(); + } + + $tree = array(); + foreach ($info['tokens'][$token_type] as $token => $token_info) { + // Build the raw token string. + $token_parents = $options['parents']; + if (empty($token_parents)) { + // If the parents array is currently empty, assume the token type is its + // parent. + $token_parents[] = $token_type; + } + elseif (in_array($token, array_slice($token_parents, 1))) { + // Prevent duplicate recursive tokens. For example, this will prevent + // the tree from generating the following tokens or deeper: + // [comment:parent:parent] + // [comment:parent:root:parent] + continue; + } + + $token_parents[] = $token; + if (!empty($token_info['dynamic'])) { + $token_parents[] = '?'; + } + $raw_token = '[' . implode(':', $token_parents) . ']'; + $tree[$raw_token] = $token_info; + $tree[$raw_token]['raw token'] = $raw_token; + + // Add the token's real name (leave out the base token type). + $tree[$raw_token]['token'] = implode(':', array_slice($token_parents, 1)); + + // Add the token's parent as its raw token value. + if (!empty($options['parents'])) { + $tree[$raw_token]['parent'] = '[' . implode(':', $options['parents']) . ']'; + } + + // Fetch the child tokens. + if (!empty($token_info['type'])) { + $child_options = $options; + $child_options['depth']--; + $child_options['parents'] = $token_parents; + $tree[$raw_token]['children'] = _token_build_tree($token_info['type'], $child_options); + } + } + + return $tree; +} + +/** + * Returns all entity type to token type mappings, or looks up one mapping. + * + * This is needed especially for taxonomy-related tokens, which do not + * use the entity name as the token type name. + * + * @param string $lookup_type + * (optional) Type of value to search for, either 'token' or 'entity'. + * @param string $lookup_value + * (optional) Value to search for (a token type or entity string). For + * instance, if $lookup_type is 'token', $lookup_value gives a token type + * that you want to find the entity type for. + * + * @return array|string|null + * Array of all mappings of entity type to the corresponding token type, or + * if both $lookup_type and $lookup_value are provided, the corresponding + * mapping (or NULL if not found). + */ +function token_get_entity_mapping($lookup_type = 'token', $lookup_value = NULL) { + $mapping = &drupal_static(__FUNCTION__, array()); + + if (empty($mapping)) { + foreach (entity_get_info() as $entity_type => $info) { + $mapping[$entity_type] = !empty($info['token type']) ? $info['token type'] : $entity_type; + } + } + + if (!isset($lookup_value)) { + return $mapping; + } + elseif ($lookup_type == 'token') { + return array_search($lookup_value, $mapping); + } + elseif ($lookup_type == 'entity') { + return isset($mapping[$value]) ? $mapping[$value] : FALSE; + } +} diff --git a/core/modules/token/token.pages.inc b/core/modules/token/token.pages.inc new file mode 100644 index 0000000..132c5f5 --- /dev/null +++ b/core/modules/token/token.pages.inc @@ -0,0 +1,316 @@ + '_blank'); + + return l($variables['text'], 'token/tree', $variables['options']); +} + +/** + * Page callback: Outputs a token tree on its own page. + * + * @see token_menu() + */ +function token_page_output_tree() { + $options = isset($_GET['options']) ? drupal_json_decode($_GET['options']) : array(); + + drupal_add_library('token', 'token_dialog'); + + // Check the token against the serialized options to prevent random access to + // the token browser page. + if (!isset($_GET['token']) || !drupal_valid_token($_GET['token'], 'token-tree:' . serialize($options))) { + return MENU_ACCESS_DENIED; + } + + $output = theme('token_tree', $options); + print '' . drupal_get_css() . drupal_get_js() . ''; + print '' . $output . ''; + drupal_exit(); +} + +/** + * Returns HTML for a tree display of nested tokens. + * + * @ingroup themeable + */ +function theme_token_tree($variables) { + $token_types = $variables['token_types']; + + if ($token_types == 'all') { + $token_types = array_keys($info['types']); + } + elseif ($variables['global_types']) { + $token_types = array_merge($token_types, token_get_global_token_types()); + } + + $element = array( + '#cache' => array( + 'cid' => 'tree-rendered:' . hash('sha256', serialize(array('token_types' => $token_types, 'global_types' => NULL) + $variables)), + 'bin' => 'cache_token', + ), + ); + + $options = array( + 'flat' => $variables['flat'], + 'restricted' => $variables['show_restricted'], + 'depth' => $variables['recursion_limit'], + ); + + // Make an array suitable for theme_item_list(), containing the nested + // list of tokens. + + $info = token_get_info(); + $items = array(); + + foreach ($info['types'] as $type => $type_info) { + if (!in_array($type, $token_types)) { + continue; + } + + $item = array( + // @todo This should look like a table if possible? + '#markup' => '' . $type_info['name'] . ' - ' . $type_info['description'] . '', + ); + + $tree = token_build_tree($type, $options); + $item['children'] = _token_make_list($tree, $variables['show_restricted']); + $items[] = $item; + } + + return theme('tree_item_list', array( + 'title' => t('List of available tokens'), + 'items' => $items, + 'type' => 'ul', + 'attributes' => array('class' => array('token-tree')), + )); +} + +/** + * Recursively builds an item list of tokens. + */ +function _token_make_list($tree, $show_restricted) { + + $list = array(); + + foreach ($tree as $token => $token_info) { + // Skip restricted tokens. + if (!empty($token_info['restricted']) && !$show_restricted) { + continue; + } + + // Display other tokens. + $item = array( + // @todo: This should be formatted better eventually to look like a table. + // @todo: Make these into links to insert the token. + '#markup' => $token . ' - ' . $token_info['name'] . ': ' . $token_info['description'], + ); + + // Recurse into child array if present. + if (isset($token_info['children'])) { + $item['children'] = _token_make_list($token_info['children'], $variables); + $item['#markup'] = '' . $item['#markup'] . ''; + } + + $list[] = $item; + } + + return $list; +} + +/** + * Page callback: Returns auto-complete options for tokens. + */ +function token_autocomplete() { + $args = func_get_args(); + $string = implode('/', $args); + + $token_info = token_info(); + + preg_match_all('/\[([^\s\]:]*):?([^\s\]]*)?\]?/', $string, $matches); + $types = $matches[1]; + $tokens = $matches[2]; + + foreach ($types as $index => $type) { + if (!empty($tokens[$index]) || isset($token_info['types'][$type])) { + token_autocomplete_token($type, $tokens[$index]); + } + else { + token_autocomplete_type($type); + } + } + +} + +/** + * Outputs the best token type matches for the given string in JSON format. + * + * There is no return value (output is sent to the browser). The output is + * an array of the closest matches between the input string and existing + * token types, in JSON format. + * + * @param string $string + * String to match. + */ +function token_autocomplete_type($string = '') { + $token_info = token_info(); + $types = $token_info['types']; + $matches = array(); + + foreach ($types as $type => $info) { + if (!$string || strpos($type, $string) === 0) { + $type_key = "[{$type}:"; + $matches[$type_key] = levenshtein($type, $string); + } + } + + if ($string) { + asort($matches); + } + else { + ksort($matches); + } + + $matches = drupal_map_assoc(array_keys($matches)); + drupal_json_output($matches); +} + +/** + * Outputs the best token type matches for the given string in JSON format. + * + * There is no return value (output is sent to the browser). The output is + * an array of the closest matches between the input string and existing + * tokens, in JSON format. + * + * @param array $token_type + * The type of token (array of token information) to look for matches on. + * @param string ... + * The rest of the arguments are used for matching. + */ +function token_autocomplete_token($token_type) { + $args = func_get_args(); + array_shift($args); + $string = trim(implode('/', $args)); + $string = substr($string, strrpos($string, '[')); + + $token_type = $token_type['type']; + $matches = array(); + + if (!drupal_strlen($string)) { + $matches["[{$token_type}:"] = 0; + } + else { + $depth = max(1, substr_count($string, ':')); + $tree = token_build_tree($token_type, array('flat' => TRUE, 'depth' => $depth)); + foreach (array_keys($tree) as $token) { + if (strpos($token, $string) === 0) { + $matches[$token] = levenshtein($token, $string); + if (isset($tree[$token]['children'])) { + $token = rtrim($token, ':]') . ':'; + $matches[$token] = levenshtein($token, $string); + } + } + } + } + + asort($matches); + $matches = drupal_map_assoc(array_keys($matches)); + drupal_json_output($matches); +} + +/** + * Returns HTML for an item list that will work with a tree control. + * + * This is just like theme_item_list() but without the divs, which do not work + * with trees. + */ +function theme_tree_item_list($variables) { + $items = $variables['items']; + $title = (string) $variables['title']; + // @todo 'type' clashes with '#type'. Rename to 'tag'. + $type = $variables['type']; + $list_attributes = $variables['attributes']; + + $output = ''; + if ($items) { + $output .= '<' . $type . new Attribute($list_attributes) . '>'; + + $num_items = count($items); + $i = 0; + foreach ($items as &$item) { + $i++; + $attributes = array(); + if (is_array($item)) { + if (isset($item['#wrapper_attributes'])) { + $attributes = $item['#wrapper_attributes']; + } + $item = drupal_render($item); + } + $attributes['class'][] = ($i % 2 ? 'odd' : 'even'); + if ($i == 1) { + $attributes['class'][] = 'first'; + } + if ($i == $num_items) { + $attributes['class'][] = 'last'; + } + $output .= '' . $item . ''; + } + $output .= ""; + } + + // Only output the list container and title, if there are any list items. + // Check to see whether the block title exists before adding a header. + // Empty headers are not semantic and present accessibility challenges. + if ($output !== '') { + if ($title !== '') { + $title = '

' . $title . '

'; + $output = '
' . $title . $output . '
'; + } + } + + return $output; +} + +function template_preprocess_tree_item_list(&$variables) { + template_preprocess_item_list($variables); +} diff --git a/core/modules/user/user.admin.inc b/core/modules/user/user.admin.inc index 263add5..e3803f0 100644 --- a/core/modules/user/user.admin.inc +++ b/core/modules/user/user.admin.inc @@ -385,8 +385,19 @@ function user_admin_settings($form, &$form_state) { '#type' => 'vertical_tabs', ); // These email tokens are shared for all settings, so just define - // the list once to help ensure they stay in sync. - $email_token_help = t('Available variables are: [site:name], [site:url], [user:name], [user:mail], [site:login-url], [site:url-brief], [user:edit-url], [user:one-time-login-url], [user:cancel-url].'); + // the list or link once. + if (module_exists('token')) { + $email_token_help = theme('token_tree_link', array( + 'text' => t('Browse tokens that can be used in e-mail messages'), + 'token_types' => array('user'), + 'show_restricted' => TRUE, + // Uncomment this line to open the browser in a real window. + // 'dialog' => FALSE, + )); + } + else { + $email_token_help = t('Available variables are: [site:name], [site:url], [user:name], [user:mail], [site:login-url], [site:url-brief], [user:edit-url], [user:one-time-login-url], [user:cancel-url].'); + } $form['email_admin_created'] = array( '#type' => 'details',