Статьи

Javascript Tutorial — Radial Menus Part 1

[img_assist | nid = 4093 | title = | desc = | link = none | align = left | width = 50 | height = 50] Иногда пользовательские интерфейсы могут быть действительно ограниченными. Многое нужно сделать, и не хватает пикселей, чтобы сделать это. В обычных настольных приложениях очень часто помещают полезные и повторяющиеся элементы в контекстное меню, чтобы они не загромождали экран. Тем не менее, обычно не рекомендуется размещать контекстные меню на веб-странице — потому что тогда они будут мешать нормальному контекстному меню браузера. Но, как всегда, есть особые случаи — например, у Google Maps есть свое собственное контекстное меню, и оно, кажется, работает довольно хорошо.

Итак, сегодня мы поговорим о контекстных меню на веб-страницах. Но не любые старые контекстные меню — мы поговорим о радиальных меню, или, более конкретно, о том, как создать свои собственные радиальные меню. Вы спрашиваете, что такое радиальные меню ? Также известные как круговые меню, они представляют собой круговое всплывающее / контекстное меню. Они не особенно распространены в стандартных приложениях Windows (потому что их нет ни в одной из стандартных библиотек виджетов), но они часто появляются в видеоиграх.

Если вы используете Firefox и хотите поэкспериментировать с использованием кругового / кругового меню, вы можете загрузить расширение easyGestures, которое предоставляет вам радиальное меню в качестве замены стандартного контекстного меню. Но хватит о настольных приложениях — мы заботимся о веб-страницах! Сайт, который изначально дал мне идею издеваться над этим, — Songza . Они используют радиальные меню, чтобы представить все параметры для конкретной песни, как вы можете видеть на скриншоте ниже:

[Img_assist | NID = 4094 | название = | убывание = | ссылка = нет | Align = нет | ширина = 297 | Высота = 168]

Пример, который мы сделаем сегодня, далеко не так хорош, но я надеюсь, что когда-нибудь в недалеком будущем у нас будет улучшенная (и легко расширяемая) версия для части 2. Итак, продолжайте, щелкните левой кнопкой мыши в поле ниже, чтобы вызвать меню (вы можете даже взять середину меню, чтобы перетащить его): см. Рабочий пример здесь. Время погрузиться в код. Сначала мы рассмотрим HTML, который на самом деле очень стандартный:

<div id="radialContainer" style="position:absolute;
display:none;">
<div id="radialTop" class="radialLevel1" style="top:-90px;
left:-30px;border-bottom-width:0px;cursor:pointer;"
onclick="closeMenu();alert('Delete!');">
<!-- Content For Top Menu -->
</div>
<div id="radialLeft" class="radialLevel1"
style="top:-30px;left:-90px;border-right-width:0px;">
<!-- Content For Left Menu -->
<div id="radialLeftSubContent" class="radialLevel2"
style="left:-100px;top:-50px;display:none;
width:100px;height:160px;">
<!-- Content For Left SubMenu -->
</div>
</div>
<div id="radialBottom" class="radialLevel1"
style="top:30px;left:-30px;border-top-width:0px;">
<!-- Content For Bottom Menu -->
<div id="radialBottomSubContent" class="radialLevel2"
style="left:-50px;top:60px;display:none;
width:160px;height:100px">
<!-- Content For Bottom SubMenu -->
</div>
</div>
<div id="radialRight" class="radialLevel1" style="top:-30px;
left:30px;border-left-width:0px;cursor:pointer;"
onclick="closeMenu();alert('Print!');">
<!-- Content For Right Menu -->
</div>

<div id="radialCenter" class="radialSquare" style="top:-30px;
left:-30px;cursor:move;background-color:White;">
</div>
</div>

Итак, сначала у нас есть контейнер div, называемый здесь radialContainer. Очевидно, что большую часть времени этот div будет скрыт (потому что меню не будет отображаться), поэтому стиль отображения: ни один не установлен. И это должно быть абсолютно позиционировано, потому что мы будем открывать его в соответствии с абсолютным положением мыши.

Внутри этого контейнера есть еще 5 элементов. Один для каждого из четырех листов (radialTop, radialLeft, radialBottom, radialRight) и центральный квадрат (radialSquare). Все расположено так, что центр меню находится в положении (0,0) относительно контейнера. Таким образом, мы можем просто установить верхнюю и левую позиции контейнера на текущую позицию мыши, и меню будет располагаться соответствующим образом.

Чтобы точно понять, что делают стили, вероятно, полезно увидеть классы 

.radialSquare, .radialLevel1
{
position:absolute;
height:60px;
width:60px;
border:solid 1px #B4B4B4;
}

.radialLevel1
{
background-color:#E2E2E2;
border:solid 1px #B4B4B4;
}

.radialLevel2
{
position:absolute;
background-color:#E2E2E2;
border:solid 1px #B4B4B4;
}

Вы могли заметить некоторую забавность с границей — как я очищаю внутреннюю границу каждого из квадратов уровня 1. Это из-за разницы между тем, как Firefox и IE отображают границы — Firefox помещает их внутри элемента, а IE — снаружи. Очистив внутреннюю границу каждого из квадратов уровня 1 и присвоив внутреннему квадрату границу, мы можем сделать так, чтобы меню выглядело одинаково во всех браузерах (если не идентично пикселю). Разве не все любят стандарты?

Итак, вернемся к этому HTML. Внутри каждого из листовых элементов есть комментарий типа <! — Content For Top Menu ->. Это потому, что хорошо, вот куда идет контент. Вы можете поместить туда практически все, что захотите, и в случае с примером меню здесь я добавил изображение и текст. Есть один особый случай с вещами внутри этого div — и вы можете увидеть это, если посмотрите на левый или нижний div.

Если вы хотите, чтобы квадрат имел подменю, вы добавляете div с идентификатором квадрата плюс строку «SubMenu». Например, квадрат radialLeft имеет подменю с идентификатором radialLeftSubMenu. Вскоре мы увидим, что код javascript на самом деле ищет элементы div с идентификаторами в этом формате, чтобы определить, есть ли в подменю меню. И так же, как и в меню уровня 1, вы можете поместить в меню уровня 2 все, что захотите.

Ok, on to the javascript code. First, let’s take a look at how we trigger the menu to come up:

var g_RadialDragObj = null;
var g_RMenu = null;
var g_LeafArray = null;
var g_LeafSubArray = null;


function triggerMenu(e)
{
if(g_RMenu == null)
{
g_RMenu = document.getElementById('radialContainer');

g_LeafArray = new Array();
g_LeafArray.push(
document.getElementById('radialTop'));
g_LeafArray.push(
document.getElementById('radialLeft'));
g_LeafArray.push(
document.getElementById('radialBottom'));
g_LeafArray.push(
document.getElementById('radialRight'));

g_LeafSubArray = new Array();
g_LeafSubArray.push(
document.getElementById('radialTopSubContent'));
g_LeafSubArray.push(
document.getElementById('radialLeftSubContent'));
g_LeafSubArray.push(
document.getElementById('radialBottomSubContent'));
g_LeafSubArray.push(
document.getElementById('radialRightSubContent'));

g_RadialDragObj =
new dragObject('radialContainer', 'radialCenter');
}

e = e ? e : window.event;
if(g_RMenu.style.display == '' || e.button != 0)
return;

var pos = absoluteCursorPostion(e);

g_RMenu.style.left = pos.X + 'px';
g_RMenu.style.top = pos.Y + 'px';
g_RMenu.style.display = '';

hookEvent(document, "mousedown", testCloseMenu);
hookEvent(document, "mousemove", subMenuActivate);
}

At the top there, we have a couple global variables. They essentially are going to be holding references to the various html elements that make up the radial menu, so we don’t have to continuously look them up. Hopefully in version two of this this, I will encapsulate everything so we don’t need any global variables.

The triggerMenu function should be attached to the onclick event of the element that you want to raise the menu. So in the case of the example on this page, the code looks something like this:

<div style="border:solid 1px black;width:500px;height:200px;" onclick="triggerMenu(event);">
Click In Me!!
</div>

So we enter the triggerMenu function. First, we want to set those global variables if they are not already set. So we do a bunch of document.getElementById calls. As you can tell, the g_LeafSubArray will have null entries for leaves that have no submenu — and this works out well for us, as we will see in a bit. But what’s this at the end of the block of code for setting the global variables? What does new dragObject mean?

Well, we are using code here from the javascript tutorial on Draggable Elements (man, we always seem to be using that code). What that line does is enable the user to grab the center of the menu (radialCenter) and drag the whole thing around (radialContainer). For more info on how that works, you should check out that tutorial

Next, we pull out the event object for the click, and do a simple check — if the menu is already visible, or if the click was not with the left button, we bail out of the function (when button is equal to 0, it means the left mouse button was clicked).

Next, we grab out the absolute position of the cursor. This function is also from the Draggable Elements tutorial, but it is simple enough to reproduce here:

function absoluteCursorPostion(eventObj)
{
eventObj = eventObj ? eventObj : window.event;

if(isNaN(window.scrollX))
return new Position(eventObj.clientX
+ document.documentElement.scrollLeft
+ document.body.scrollLeft,
eventObj.clientY
+ document.documentElement.scrollTop
+ document.body.scrollTop);
else
return new Position(
eventObj.clientX + window.scrollX,
eventObj.clientY + window.scrollY);
}

For the explanation of why we have to do so much work just to get the absolute mouse position, you should go check out that tutorial. The object that this function returns is a Position object (also defined in the Draggable Elements code), but it does exactly what you might expect — hold an X and Y position.

So now that we have the absolute mouse position, we set the top and left of the radial menu container to this position, and make the menu visible. Finally, we hook into the mousedown and mousemove events on the document, using the hookEvent function. The hookevent function is a nice wrapper around the different browser code for hooking an event — you can learn about it in the Working With Events tutorial.

Why do we hook into the mousedown and mousemove events, and why do we have to hook them at the document level? That’s a good question, so let’s first take a look at the mousedown case.

function testCloseMenu(e)
{
var tar = getEventTarget(e);

do
{
if(tar == g_RMenu)
return;
tar = tar.parentNode;
}while(tar != null);

closeMenu();
}

function getEventTarget(e)
{
e = e ? e : window.event;
return e.target ? e.target : e.srcElement;
}

The function testCloseMenu gets called on every mouse down event on the document once the radial menu is up. This is because we want to close the menu as soon as the user clicks something that is not actually inside of the menu. So on every mouse down, we get the event target, and we walk up the tree of elements from parent to parent, checking to see if the element that was clicked is a child of the radial menu. If it is, we leave the menu open, but if it isn’t, we call the function closeMenu.

function closeMenu()
{
unhookEvent(document, "mousedown", testCloseMenu);
unhookEvent(document, "mousemove", subMenuActivate);

g_RMenu.style.display = 'none';

for(var i=0; i <g_LeafArray.length; i++)
{
g_LeafArray[i].style.backgroundColor = '#E2E2E2';
if(g_LeafSubArray[i] != null)
g_LeafSubArray[i].style.display = 'none';
}
}

So the first thing we do when we close the radial menu is unhook those two events. This unhookEvent function is from the same place as the hookEvent function — the tutorial «Working With Events». We then hide the menu by setting the display to none on the menu container. And then we walk through all the elements of the menu, setting everything back to their initial state (in case the menu was closed while an element was still highlighted, or a sub menu was visible).

Now, lets take a look at why we hooked that mousemove event:

function subMenuActivate(e)
{
var tar = getEventTarget(e);

do
{
var found = false;
for(var i=0; i <g_LeafArray.length; i++)
if(tar == g_LeafArray[i])
{
found = true;
break;
}
if(found)
break;
tar = tar.parentNode;
}while(tar != null);

for(var i=0; i <g_LeafArray.length; i++)
{
if(g_LeafArray[i] == tar)
{
g_LeafArray[i].style.backgroundColor = '#BEBEBE';
if(g_LeafSubArray[i] != null)
g_LeafSubArray[i].style.display = '';
}
else
{
g_LeafArray[i].style.backgroundColor = '#E2E2E2';
if(g_LeafSubArray[i] != null)
g_LeafSubArray[i].style.display = 'none';
}
}
}

When the user’s mouse is over a menu item, we want it to highlight and the submenu (if there is any) to appear — and when the mouse leaves for everything to go back to normal. And since mouseout is a very finicky event (it won’t fire if you move your mouse too fast) it is a whole lot easier to track the mouse across the entire document. We do the same type of parent tree walking here as we did in the testCloseMenu function, except in this case we are looking to match any of the four leaves. If we match one, that leaf should highlight and the submenu appear.

So as soon as we find a match, we break out of the loop, and proceed to loop though the leaves. If the leaf matched, we change the background color to the highlight color, and if there is a submenu we make it visible. If the leaf did not match, we make sure that the background color is the standard color and that the submenu is hidden.

And there we go! Granted, the code is not as clean as I would like, but this was my first stab at making a radial menu for a web page. Hopfully, I’ll get around to refactoring this and making it a lot more extensible soon, so look forward to a part 2. You can grab all the code for this example here. As always, if you have any questions or comments, feel free to leave them below.