mirror of
https://github.com/moodle/moodle.git
synced 2025-08-05 08:56:36 +02:00
MDL-36296 block_navigation: consistency fixes
This change fixes a couple of consistency issues in the navigation block. At the same time it fixes it the CSS for the navigation block as it had really degraded over the last years. Notable points about this change: * Fixed icon display by wrapping content in a span allowing columns to be styled. * Re-wrote the CSS entirely as it could be done much better. * Fixed inconsistencies between the php and JS for rendering nodes.
This commit is contained in:
parent
4c27f52d91
commit
892e21aadd
6 changed files with 167 additions and 87 deletions
|
@ -51,7 +51,7 @@ class block_navigation_renderer extends plugin_renderer_base {
|
|||
/**
|
||||
* Produces a navigation node for the navigation tree
|
||||
*
|
||||
* @param array $items
|
||||
* @param navigation_node[] $items
|
||||
* @param array $attrs
|
||||
* @param int $expansionlimit
|
||||
* @param array $options
|
||||
|
@ -59,13 +59,12 @@ class block_navigation_renderer extends plugin_renderer_base {
|
|||
* @return string
|
||||
*/
|
||||
protected function navigation_node($items, $attrs=array(), $expansionlimit=null, array $options = array(), $depth=1) {
|
||||
|
||||
// exit if empty, we don't want an empty ul element
|
||||
if (count($items)==0) {
|
||||
// Exit if empty, we don't want an empty ul element.
|
||||
if (count($items) === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// array of nested li elements
|
||||
// Turn our navigation items into list items.
|
||||
$lis = array();
|
||||
foreach ($items as $item) {
|
||||
if (!$item->display && !$item->contains_active_node()) {
|
||||
|
@ -86,10 +85,13 @@ class block_navigation_renderer extends plugin_renderer_base {
|
|||
|
||||
if ($hasicon) {
|
||||
$icon = $this->output->render($item->icon);
|
||||
// Because an icon is being used we're going to wrap the actual content in a span.
|
||||
// This will allow designers to create columns for the content, as we've done in styles.css.
|
||||
$content = $icon . html_writer::span($content, 'item-content-wrap');
|
||||
} else {
|
||||
$icon = '';
|
||||
}
|
||||
$content = $icon.$content; // use CSS for spacing of icons
|
||||
|
||||
if ($item->helpbutton !== null) {
|
||||
$content = trim($item->helpbutton).html_writer::tag('span', $content, array('class'=>'clearhelpbutton'));
|
||||
}
|
||||
|
@ -116,13 +118,16 @@ class block_navigation_renderer extends plugin_renderer_base {
|
|||
$link->text = $icon.$link->text;
|
||||
$link->attributes = array_merge($link->attributes, $attributes);
|
||||
$content = $this->output->render($link);
|
||||
$linkrendered = true;
|
||||
} else if ($item->action instanceof moodle_url) {
|
||||
$content = html_writer::link($item->action, $content, $attributes);
|
||||
}
|
||||
|
||||
// this applies to the li item which contains all child lists too
|
||||
// This applies to the li item which contains all child lists too.
|
||||
$liclasses = array($item->get_css_type(), 'depth_'.$depth);
|
||||
|
||||
// Class attribute on the div item which only contains the item content.
|
||||
$divclasses = array('tree_item');
|
||||
|
||||
$liexpandable = array();
|
||||
if ($item->has_children() && (!$item->forceopen || $item->collapse)) {
|
||||
$liclasses[] = 'collapsed';
|
||||
|
@ -130,30 +135,30 @@ class block_navigation_renderer extends plugin_renderer_base {
|
|||
if ($isbranch) {
|
||||
$liclasses[] = 'contains_branch';
|
||||
$liexpandable = array('aria-expanded' => in_array('collapsed', $liclasses) ? "false" : "true");
|
||||
} else if ($hasicon) {
|
||||
$liclasses[] = 'item_with_icon';
|
||||
}
|
||||
if ($item->isactive === true) {
|
||||
$liclasses[] = 'current_branch';
|
||||
}
|
||||
$liattr = array('class' => join(' ',$liclasses)) + $liexpandable;
|
||||
// class attribute on the div item which only contains the item content
|
||||
$divclasses = array('tree_item');
|
||||
if ($isbranch) {
|
||||
$divclasses[] = 'branch';
|
||||
} else {
|
||||
$divclasses[] = 'leaf';
|
||||
}
|
||||
if ($hasicon) {
|
||||
// Add this class if the item has an icon, whether it is a branch or not.
|
||||
$liclasses[] = 'item_with_icon';
|
||||
$divclasses[] = 'hasicon';
|
||||
}
|
||||
if ($item->isactive === true) {
|
||||
$liclasses[] = 'current_branch';
|
||||
}
|
||||
if (!empty($item->classes) && count($item->classes)>0) {
|
||||
$divclasses[] = join(' ', $item->classes);
|
||||
}
|
||||
|
||||
// Now build attribute arrays.
|
||||
$liattr = array('class' => join(' ', $liclasses)) + $liexpandable;
|
||||
$divattr = array('class'=>join(' ', $divclasses));
|
||||
if (!empty($item->id)) {
|
||||
$divattr['id'] = $item->id;
|
||||
}
|
||||
|
||||
// Create the structure.
|
||||
$content = html_writer::tag('p', $content, $divattr);
|
||||
if ($isexpandable) {
|
||||
$content .= $this->navigation_node($item->children, array(), $expansionlimit, $options, $depth+1);
|
||||
|
@ -165,11 +170,14 @@ class block_navigation_renderer extends plugin_renderer_base {
|
|||
$lis[] = $content;
|
||||
}
|
||||
|
||||
if (count($lis)) {
|
||||
return html_writer::tag('ul', implode("\n", $lis), $attrs);
|
||||
} else {
|
||||
if (count($lis) === 0) {
|
||||
// There is still a chance, despite having items, that nothing had content and no list items were created.
|
||||
return '';
|
||||
}
|
||||
|
||||
// We used to separate using new lines, however we don't do that now, instead we'll save a few chars.
|
||||
// The source is complex already anyway.
|
||||
return html_writer::tag('ul', implode('', $lis), $attrs);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,66 +1,105 @@
|
|||
/** General display rules **/
|
||||
.block_navigation .block_tree {margin:5px;padding-left:0px;overflow:visible;}
|
||||
.block_navigation .block_tree li {margin:3px;list-style: none;padding:0;}
|
||||
.block_navigation .block_tree li.item_with_icon > p {position:relative; padding-left: 21px;}
|
||||
.block_navigation .block_tree li.item_with_icon > p img,
|
||||
.block_navigation .block_tree .type_activity > p.tree_item.active_tree_node img,
|
||||
.block_navigation .block_tree li > p.hasicon img {vertical-align:middle;position:absolute;left:0;top:-1px;width:16px;height:16px;}
|
||||
.block_navigation .block_tree li.item_with_icon.contains_branch > p img {left:16px;}
|
||||
.block_navigation .block_tree .type_activity > p.branch.hasicon,
|
||||
.block_navigation .block_tree .type_activity > p.emptybranch.hasicon,
|
||||
.block_navigation .block_tree li.item_with_icon.contains_branch > .tree_item {padding-left:37px;}
|
||||
.block_navigation .block_tree {
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.block_navigation .block_tree li ul {padding-left:0;margin:0;}
|
||||
.block_navigation .block_tree li.depth_2 ul {padding-left:16px;margin:0;}
|
||||
.block_navigation .block_tree .type_activity > p.tree_item.branch.hasicon.active_tree_node,
|
||||
.block_navigation .block_tree .tree_item {padding-left: 21px;margin:3px 0px;text-align:left;}
|
||||
.block_navigation .block_tree .depth_1 > .tree_item.branch,
|
||||
.block_navigation .block_tree .depth_1 > .tree_item.emptybranch {
|
||||
padding-left: 0;
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
.block_navigation .block_tree .tree_item.branch {background-image: url([[pix:t/expanded]]);background-position: 0 0;background-repeat: no-repeat;}
|
||||
.block_navigation .block_tree .tree_item.branch.navigation_node {background-image:none;padding-left:0;}
|
||||
.block_navigation .block_tree .type_activity > .tree_item.emptybranch,
|
||||
.block_navigation .block_tree .type_activity > .tree_item.branch {background-image:none;position:relative;}
|
||||
.block_navigation .block_tree .type_activity > .tree_item.hasicon.emptybranch img,
|
||||
.block_navigation .block_tree .type_activity > .tree_item.branch img {left: 16px;}
|
||||
.block_navigation .block_tree .root_node.leaf {padding-left:0px;}
|
||||
.block_navigation .block_tree .active_tree_node {font-weight:bold;}
|
||||
.block_navigation .block_tree .depth_1.current_branch ul {font-weight:normal;}
|
||||
.block_navigation .block_tree .tree_item {
|
||||
margin: 3px 0;
|
||||
background-position: 0 50%;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.dock .block_navigation .tree_item {white-space: nowrap;}
|
||||
.block_navigation .block_tree .tree_item.branch {
|
||||
padding-left: 21px;
|
||||
cursor: pointer;
|
||||
background-image: url('[[pix:t/expanded]]');
|
||||
}
|
||||
|
||||
.jsenabled .block_navigation .block_tree .tree_item.branch {cursor:pointer;}
|
||||
.jsenabled .block_navigation .block_tree .tree_item.emptybranch {background-image: url([[pix:t/collapsed_empty]]);background-position: 0 0;background-repeat: no-repeat;}
|
||||
.jsenabled .block_navigation .block_tree .collapsed ul {display: none;}
|
||||
.jsenabled .block_navigation .block_tree .type_activity > .tree_item.branch {background-image: url([[pix:t/expanded]]);}
|
||||
.jsenabled .block_navigation .block_tree .collapsed .tree_item.branch {background-image: url([[pix:t/collapsed]]);}
|
||||
.jsenabled .block_navigation .block_tree .tree_item.branch.loadingbranch {background-image:url([[pix:i/loading_small]]);}
|
||||
.block_navigation .block_tree .tree_item.emptybranch {
|
||||
padding-left: 21px;
|
||||
background-image: url('[[pix:t/collapsed_empty]]');
|
||||
}
|
||||
|
||||
/** JavaScript state rules **/
|
||||
.jsenabled .block_navigation.dock_on_load,
|
||||
.block_navigation .block_tree_box .requiresjs {display:none;}
|
||||
.jsenabled .block_navigation .block_tree_box .requiresjs {display:inline;}
|
||||
.block_navigation .block_tree .tree_item.loadingbranch {
|
||||
background-image: url('[[pix:i/loading_small]]');
|
||||
}
|
||||
|
||||
/** Internet explorer specific rules **/
|
||||
.ie6 .block_navigation .block_tree .tree_item {width:100%;}
|
||||
.block_navigation .block_tree .tree_item img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: 3px;
|
||||
margin-right: 5px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/** Overide for RTL layout **/
|
||||
.dir-rtl .block_navigation .block_tree li.depth_2 ul {padding-left: 0; padding-right: 16px;}
|
||||
.dir-rtl .block_navigation .block_tree .type_activity > p.tree_item.branch.hasicon.active_tree_node,
|
||||
.dir-rtl .block_navigation .block_tree .tree_item {padding-right: 21px;text-align:right;}
|
||||
.block_navigation .block_tree .tree_item.active_tree_node {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dir-rtl .block_navigation .block_tree .tree_item.branch {background-position: center right;}
|
||||
.block_navigation .block_tree .tree_item.hasicon {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dir-rtl .block_navigation .block_tree,
|
||||
.dir-rtl .block_navigation .block_tree li ul,
|
||||
.dir-rtl .block_navigation .block_tree .navigation_node.tree_item.branch,
|
||||
.dir-rtl .block_navigation .block_tree .root_node.leaf {padding-right:0;}
|
||||
.block_navigation .block_tree .tree_item.hasicon .item-content-wrap {
|
||||
display: inline-block;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.dir-rtl .block_navigation .block_tree li.item_with_icon > p img,
|
||||
.dir-rtl .block_navigation .block_tree .type_activity > p.tree_item.active_tree_node img,
|
||||
.dir-rtl .block_navigation .block_tree li > p.hasicon img {left:auto; right:0;}
|
||||
.dir-rtl .block_navigation .block_tree li.item_with_icon.contains_branch > p img {left: auto; right:16px;}
|
||||
.dir-rtl .block_navigation .block_tree .type_activity > p.branch.hasicon,
|
||||
.dir-rtl .block_navigation .block_tree li.item_with_icon.contains_branch > .tree_item {padding-right:37px; padding-left: 0;}
|
||||
.dir-rtl .block_navigation .block_tree .type_activity > .tree_item.branch img {right: 16px; left: auto;}
|
||||
.block_navigation .block_tree ul {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.jsenabled.dir-rtl .block_navigation .block_tree .tree_item.emptybranch {background-image: url([[pix:t/collapsed_empty_rtl]]);background-position: center right;}
|
||||
.jsenabled.dir-rtl .block_navigation .block_tree .collapsed .tree_item.branch {background-image: url([[pix:t/collapsed_rtl]]);}
|
||||
.block_navigation .block_tree ul ul {
|
||||
margin: 0 0 0 16px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.jsenabled .block_navigation .block_tree li.collapsed ul {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.jsenabled .block_navigation .block_tree li.collapsed .tree_item.branch {
|
||||
background-image: url('[[pix:t/collapsed]]');
|
||||
}
|
||||
|
||||
.jsenabled .block_navigation.dock_on_load {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dir-rtl .block_navigation .block_tree .depth_1 .tree_item {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.dir-rtl .block_navigation .block_tree .tree_item {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
|
||||
.dir-rtl .block_navigation .block_tree .tree_item.branch {
|
||||
padding-right: 21px;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.dir-rtl .block_navigation .block_tree .tree_item.emptybranch {
|
||||
padding-right: 21px;
|
||||
padding-left: 0;
|
||||
background-image: url('[[pix:t/collapsed_empty_rtl]]');
|
||||
}
|
||||
|
||||
.dir-rtl .block_navigation .block_tree .tree_item img {
|
||||
margin-right: 0;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.dir-rtl .block_navigation .block_tree ul {
|
||||
margin: 0 16px 0 0;
|
||||
}
|
||||
|
||||
.dir-rtl.jsenabled .block_navigation .block_tree .collapsed .tree_item.branch {
|
||||
background-image: url('[[pix:t/collapsed_rtl]]');
|
||||
}
|
||||
|
|
|
@ -483,6 +483,9 @@ BRANCH.prototype = {
|
|||
* This function creates a DOM structure for the branch and then injects
|
||||
* it into the navigation tree at the correct point.
|
||||
*
|
||||
* It is important that this is kept in check with block_navigation_renderer::navigation_node as that produces
|
||||
* the same thing as this but on the php side.
|
||||
*
|
||||
* @method draw
|
||||
* @chainable
|
||||
* @param {Node} element
|
||||
|
@ -494,6 +497,7 @@ BRANCH.prototype = {
|
|||
var branchli = Y.Node.create('<li></li>');
|
||||
var link = this.get('link');
|
||||
var branchp = Y.Node.create('<p class="tree_item"></p>').setAttribute('id', this.get('id'));
|
||||
var name;
|
||||
if (!link) {
|
||||
//add tab focus if not link (so still one focus per menu node).
|
||||
// it was suggested to have 2 foci. one for the node and one for the link in MDL-27428.
|
||||
|
@ -508,10 +512,11 @@ BRANCH.prototype = {
|
|||
// Prepare the icon, should be an object representing a pix_icon
|
||||
var branchicon = false;
|
||||
var icon = this.get('icon');
|
||||
if (icon && (!isbranch || this.get('type') === NODETYPE.ACTIVITY)) {
|
||||
if (icon && (!isbranch || this.get('type') === NODETYPE.ACTIVITY || this.get('type') === NODETYPE.RESOURCE)) {
|
||||
branchicon = Y.Node.create('<img alt="" />');
|
||||
branchicon.setAttribute('src', M.util.image_url(icon.pix, icon.component));
|
||||
branchli.addClass('item_with_icon');
|
||||
branchp.addClass('hasicon');
|
||||
if (icon.alt) {
|
||||
branchicon.setAttribute('alt', icon.alt);
|
||||
}
|
||||
|
@ -529,8 +534,11 @@ BRANCH.prototype = {
|
|||
var branchspan = Y.Node.create('<span></span>');
|
||||
if (branchicon) {
|
||||
branchspan.appendChild(branchicon);
|
||||
name = '<span class="item-content-wrap">' + this.get('name') + '</span>';
|
||||
} else {
|
||||
name = this.get('name');
|
||||
}
|
||||
branchspan.append(this.get('name'));
|
||||
branchspan.append(name);
|
||||
if (this.get('hidden')) {
|
||||
branchspan.addClass('dimmed_text');
|
||||
}
|
||||
|
@ -539,8 +547,11 @@ BRANCH.prototype = {
|
|||
var branchlink = Y.Node.create('<a title="'+this.get('title')+'" href="'+link+'"></a>');
|
||||
if (branchicon) {
|
||||
branchlink.appendChild(branchicon);
|
||||
name = '<span class="item-content-wrap">' + this.get('name') + '</span>';
|
||||
} else {
|
||||
name = this.get('name');
|
||||
}
|
||||
branchlink.append(this.get('name'));
|
||||
branchlink.append(name);
|
||||
if (this.get('hidden')) {
|
||||
branchlink.addClass('dimmed');
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -482,6 +482,9 @@ BRANCH.prototype = {
|
|||
* This function creates a DOM structure for the branch and then injects
|
||||
* it into the navigation tree at the correct point.
|
||||
*
|
||||
* It is important that this is kept in check with block_navigation_renderer::navigation_node as that produces
|
||||
* the same thing as this but on the php side.
|
||||
*
|
||||
* @method draw
|
||||
* @chainable
|
||||
* @param {Node} element
|
||||
|
@ -493,6 +496,7 @@ BRANCH.prototype = {
|
|||
var branchli = Y.Node.create('<li></li>');
|
||||
var link = this.get('link');
|
||||
var branchp = Y.Node.create('<p class="tree_item"></p>').setAttribute('id', this.get('id'));
|
||||
var name;
|
||||
if (!link) {
|
||||
//add tab focus if not link (so still one focus per menu node).
|
||||
// it was suggested to have 2 foci. one for the node and one for the link in MDL-27428.
|
||||
|
@ -507,10 +511,11 @@ BRANCH.prototype = {
|
|||
// Prepare the icon, should be an object representing a pix_icon
|
||||
var branchicon = false;
|
||||
var icon = this.get('icon');
|
||||
if (icon && (!isbranch || this.get('type') === NODETYPE.ACTIVITY)) {
|
||||
if (icon && (!isbranch || this.get('type') === NODETYPE.ACTIVITY || this.get('type') === NODETYPE.RESOURCE)) {
|
||||
branchicon = Y.Node.create('<img alt="" />');
|
||||
branchicon.setAttribute('src', M.util.image_url(icon.pix, icon.component));
|
||||
branchli.addClass('item_with_icon');
|
||||
branchp.addClass('hasicon');
|
||||
if (icon.alt) {
|
||||
branchicon.setAttribute('alt', icon.alt);
|
||||
}
|
||||
|
@ -528,8 +533,11 @@ BRANCH.prototype = {
|
|||
var branchspan = Y.Node.create('<span></span>');
|
||||
if (branchicon) {
|
||||
branchspan.appendChild(branchicon);
|
||||
name = '<span class="item-content-wrap">' + this.get('name') + '</span>';
|
||||
} else {
|
||||
name = this.get('name');
|
||||
}
|
||||
branchspan.append(this.get('name'));
|
||||
branchspan.append(name);
|
||||
if (this.get('hidden')) {
|
||||
branchspan.addClass('dimmed_text');
|
||||
}
|
||||
|
@ -538,8 +546,11 @@ BRANCH.prototype = {
|
|||
var branchlink = Y.Node.create('<a title="'+this.get('title')+'" href="'+link+'"></a>');
|
||||
if (branchicon) {
|
||||
branchlink.appendChild(branchicon);
|
||||
name = '<span class="item-content-wrap">' + this.get('name') + '</span>';
|
||||
} else {
|
||||
name = this.get('name');
|
||||
}
|
||||
branchlink.append(this.get('name'));
|
||||
branchlink.append(name);
|
||||
if (this.get('hidden')) {
|
||||
branchlink.addClass('dimmed');
|
||||
}
|
||||
|
|
|
@ -481,6 +481,9 @@ BRANCH.prototype = {
|
|||
* This function creates a DOM structure for the branch and then injects
|
||||
* it into the navigation tree at the correct point.
|
||||
*
|
||||
* It is important that this is kept in check with block_navigation_renderer::navigation_node as that produces
|
||||
* the same thing as this but on the php side.
|
||||
*
|
||||
* @method draw
|
||||
* @chainable
|
||||
* @param {Node} element
|
||||
|
@ -492,6 +495,7 @@ BRANCH.prototype = {
|
|||
var branchli = Y.Node.create('<li></li>');
|
||||
var link = this.get('link');
|
||||
var branchp = Y.Node.create('<p class="tree_item"></p>').setAttribute('id', this.get('id'));
|
||||
var name;
|
||||
if (!link) {
|
||||
//add tab focus if not link (so still one focus per menu node).
|
||||
// it was suggested to have 2 foci. one for the node and one for the link in MDL-27428.
|
||||
|
@ -506,10 +510,11 @@ BRANCH.prototype = {
|
|||
// Prepare the icon, should be an object representing a pix_icon
|
||||
var branchicon = false;
|
||||
var icon = this.get('icon');
|
||||
if (icon && (!isbranch || this.get('type') === NODETYPE.ACTIVITY)) {
|
||||
if (icon && (!isbranch || this.get('type') === NODETYPE.ACTIVITY || this.get('type') === NODETYPE.RESOURCE)) {
|
||||
branchicon = Y.Node.create('<img alt="" />');
|
||||
branchicon.setAttribute('src', M.util.image_url(icon.pix, icon.component));
|
||||
branchli.addClass('item_with_icon');
|
||||
branchp.addClass('hasicon');
|
||||
if (icon.alt) {
|
||||
branchicon.setAttribute('alt', icon.alt);
|
||||
}
|
||||
|
@ -527,8 +532,11 @@ BRANCH.prototype = {
|
|||
var branchspan = Y.Node.create('<span></span>');
|
||||
if (branchicon) {
|
||||
branchspan.appendChild(branchicon);
|
||||
name = '<span class="item-content-wrap">' + this.get('name') + '</span>';
|
||||
} else {
|
||||
name = this.get('name');
|
||||
}
|
||||
branchspan.append(this.get('name'));
|
||||
branchspan.append(name);
|
||||
if (this.get('hidden')) {
|
||||
branchspan.addClass('dimmed_text');
|
||||
}
|
||||
|
@ -537,8 +545,11 @@ BRANCH.prototype = {
|
|||
var branchlink = Y.Node.create('<a title="'+this.get('title')+'" href="'+link+'"></a>');
|
||||
if (branchicon) {
|
||||
branchlink.appendChild(branchicon);
|
||||
name = '<span class="item-content-wrap">' + this.get('name') + '</span>';
|
||||
} else {
|
||||
name = this.get('name');
|
||||
}
|
||||
branchlink.append(this.get('name'));
|
||||
branchlink.append(name);
|
||||
if (this.get('hidden')) {
|
||||
branchlink.addClass('dimmed');
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue