зеркало из https://github.com/mozilla/pjs.git
Bug 468418 - Expose level for nested lists in HTML, r=marcoz, aaronlev
This commit is contained in:
Родитель
9714de8cfa
Коммит
5e4bb7fd66
|
@ -2015,95 +2015,12 @@ nsAccessible::GetAttributes(nsIPersistentProperties **aAttributes)
|
|||
}
|
||||
}
|
||||
|
||||
// Level/setsize/posinset
|
||||
// Group attributes (level/setsize/posinset)
|
||||
if (!nsAccUtils::HasAccGroupAttrs(attributes)) {
|
||||
// The role of an accessible can be pointed by ARIA attribute but ARIA
|
||||
// posinset, level, setsize may be skipped. Therefore we calculate here
|
||||
// these properties to map them into description.
|
||||
|
||||
// If accessible is invisible we don't want to calculate group ARIA
|
||||
// attributes for it.
|
||||
if ((role == nsIAccessibleRole::ROLE_LISTITEM ||
|
||||
role == nsIAccessibleRole::ROLE_MENUITEM ||
|
||||
role == nsIAccessibleRole::ROLE_CHECK_MENU_ITEM ||
|
||||
role == nsIAccessibleRole::ROLE_RADIO_MENU_ITEM ||
|
||||
role == nsIAccessibleRole::ROLE_RADIOBUTTON ||
|
||||
role == nsIAccessibleRole::ROLE_PAGETAB ||
|
||||
role == nsIAccessibleRole::ROLE_OPTION ||
|
||||
role == nsIAccessibleRole::ROLE_RADIOBUTTON ||
|
||||
role == nsIAccessibleRole::ROLE_OUTLINEITEM) &&
|
||||
0 == (nsAccUtils::State(this) & nsIAccessibleStates::STATE_INVISIBLE)) {
|
||||
|
||||
PRUint32 baseRole = role;
|
||||
if (role == nsIAccessibleRole::ROLE_CHECK_MENU_ITEM ||
|
||||
role == nsIAccessibleRole::ROLE_RADIO_MENU_ITEM)
|
||||
baseRole = nsIAccessibleRole::ROLE_MENUITEM;
|
||||
|
||||
nsCOMPtr<nsIAccessible> parent = GetParent();
|
||||
NS_ENSURE_TRUE(parent, NS_ERROR_FAILURE);
|
||||
|
||||
PRInt32 positionInGroup = 0;
|
||||
PRInt32 setSize = 0;
|
||||
|
||||
nsCOMPtr<nsIAccessible> sibling, nextSibling;
|
||||
parent->GetFirstChild(getter_AddRefs(sibling));
|
||||
NS_ENSURE_TRUE(sibling, NS_ERROR_FAILURE);
|
||||
|
||||
PRBool foundCurrent = PR_FALSE;
|
||||
PRUint32 siblingRole, siblingBaseRole;
|
||||
while (sibling) {
|
||||
sibling->GetFinalRole(&siblingRole);
|
||||
|
||||
siblingBaseRole = siblingRole;
|
||||
if (siblingRole == nsIAccessibleRole::ROLE_CHECK_MENU_ITEM ||
|
||||
siblingRole == nsIAccessibleRole::ROLE_RADIO_MENU_ITEM)
|
||||
siblingBaseRole = nsIAccessibleRole::ROLE_MENUITEM;
|
||||
|
||||
// If sibling is visible and has the same base role.
|
||||
if (siblingBaseRole == baseRole &&
|
||||
!(nsAccUtils::State(sibling) & nsIAccessibleStates::STATE_INVISIBLE)) {
|
||||
++ setSize;
|
||||
if (!foundCurrent) {
|
||||
++ positionInGroup;
|
||||
if (sibling == this)
|
||||
foundCurrent = PR_TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
// If the sibling is separator
|
||||
if (siblingRole == nsIAccessibleRole::ROLE_SEPARATOR) {
|
||||
if (foundCurrent) // the our group is ended
|
||||
break;
|
||||
|
||||
// not our group, continue the searching
|
||||
positionInGroup = 0;
|
||||
setSize = 0;
|
||||
}
|
||||
|
||||
sibling->GetNextSibling(getter_AddRefs(nextSibling));
|
||||
sibling = nextSibling;
|
||||
}
|
||||
|
||||
PRInt32 groupLevel = 0;
|
||||
if (role == nsIAccessibleRole::ROLE_OUTLINEITEM) {
|
||||
groupLevel = 1;
|
||||
nsCOMPtr<nsIAccessible> nextParent;
|
||||
while (parent) {
|
||||
parent->GetFinalRole(&role);
|
||||
|
||||
if (role == nsIAccessibleRole::ROLE_OUTLINE)
|
||||
break;
|
||||
if (role == nsIAccessibleRole::ROLE_GROUPING)
|
||||
++ groupLevel;
|
||||
|
||||
parent->GetParent(getter_AddRefs(nextParent));
|
||||
parent.swap(nextParent);
|
||||
}
|
||||
}
|
||||
|
||||
nsAccUtils::SetAccGroupAttrs(attributes, groupLevel, positionInGroup,
|
||||
setSize);
|
||||
}
|
||||
// Calculate group attributes based on accessible hierarhy if they weren't
|
||||
// provided by ARIA or by accessible class implementation.
|
||||
rv = ComputeGroupAttributes(role, attributes);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
}
|
||||
|
||||
// Expose all ARIA attributes
|
||||
|
@ -3567,3 +3484,145 @@ nsAccessible::GetActionRule(PRUint32 aStates)
|
|||
|
||||
return eNoAction;
|
||||
}
|
||||
|
||||
nsresult
|
||||
nsAccessible::ComputeGroupAttributes(PRUint32 aRole,
|
||||
nsIPersistentProperties *aAttributes)
|
||||
{
|
||||
// The role of an accessible can be specified by ARIA attribute but ARIA
|
||||
// posinset, level, setsize may be skipped. As well this method is used
|
||||
// for non ARIA accessibles to avoid GetAccessibleInternal() method
|
||||
// implementation in subclasses. For example, it's being used to calculate
|
||||
// group attributes for HTML li elements.
|
||||
|
||||
// If accessible is invisible we don't want to calculate group attributes for
|
||||
// it.
|
||||
if (nsAccUtils::State(this) & nsIAccessibleStates::STATE_INVISIBLE)
|
||||
return NS_OK;
|
||||
|
||||
if (aRole != nsIAccessibleRole::ROLE_LISTITEM &&
|
||||
aRole != nsIAccessibleRole::ROLE_MENUITEM &&
|
||||
aRole != nsIAccessibleRole::ROLE_CHECK_MENU_ITEM &&
|
||||
aRole != nsIAccessibleRole::ROLE_RADIO_MENU_ITEM &&
|
||||
aRole != nsIAccessibleRole::ROLE_RADIOBUTTON &&
|
||||
aRole != nsIAccessibleRole::ROLE_PAGETAB &&
|
||||
aRole != nsIAccessibleRole::ROLE_OPTION &&
|
||||
aRole != nsIAccessibleRole::ROLE_OUTLINEITEM)
|
||||
return NS_OK;
|
||||
|
||||
PRUint32 baseRole = aRole;
|
||||
if (aRole == nsIAccessibleRole::ROLE_CHECK_MENU_ITEM ||
|
||||
aRole == nsIAccessibleRole::ROLE_RADIO_MENU_ITEM)
|
||||
baseRole = nsIAccessibleRole::ROLE_MENUITEM;
|
||||
|
||||
nsCOMPtr<nsIAccessible> parent = GetParent();
|
||||
NS_ENSURE_TRUE(parent, NS_ERROR_FAILURE);
|
||||
|
||||
// Compute 'posinset' and 'setsize' attributes.
|
||||
PRInt32 positionInGroup = 0;
|
||||
PRInt32 setSize = 0;
|
||||
|
||||
nsCOMPtr<nsIAccessible> sibling, nextSibling;
|
||||
parent->GetFirstChild(getter_AddRefs(sibling));
|
||||
NS_ENSURE_STATE(sibling);
|
||||
|
||||
PRBool foundCurrent = PR_FALSE;
|
||||
PRUint32 siblingRole, siblingBaseRole;
|
||||
while (sibling) {
|
||||
siblingRole = nsAccUtils::Role(sibling);
|
||||
|
||||
siblingBaseRole = siblingRole;
|
||||
if (siblingRole == nsIAccessibleRole::ROLE_CHECK_MENU_ITEM ||
|
||||
siblingRole == nsIAccessibleRole::ROLE_RADIO_MENU_ITEM)
|
||||
siblingBaseRole = nsIAccessibleRole::ROLE_MENUITEM;
|
||||
|
||||
// If sibling is visible and has the same base role.
|
||||
if (siblingBaseRole == baseRole &&
|
||||
!(nsAccUtils::State(sibling) & nsIAccessibleStates::STATE_INVISIBLE)) {
|
||||
++ setSize;
|
||||
if (!foundCurrent) {
|
||||
++ positionInGroup;
|
||||
if (sibling == this)
|
||||
foundCurrent = PR_TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
// If the sibling is separator
|
||||
if (siblingRole == nsIAccessibleRole::ROLE_SEPARATOR) {
|
||||
if (foundCurrent) // the our group is ended
|
||||
break;
|
||||
|
||||
// not our group, continue the searching
|
||||
positionInGroup = 0;
|
||||
setSize = 0;
|
||||
}
|
||||
|
||||
sibling->GetNextSibling(getter_AddRefs(nextSibling));
|
||||
sibling = nextSibling;
|
||||
}
|
||||
|
||||
// Compute 'level' attribute.
|
||||
PRInt32 groupLevel = 0;
|
||||
if (aRole == nsIAccessibleRole::ROLE_OUTLINEITEM) {
|
||||
// Always expose 'level' attribute for 'outlineitem' accessible. The number
|
||||
// of nested 'grouping' accessibles containing 'outlineitem' accessible is
|
||||
// its level.
|
||||
groupLevel = 1;
|
||||
nsCOMPtr<nsIAccessible> nextParent;
|
||||
while (parent) {
|
||||
PRUint32 parentRole = nsAccUtils::Role(parent);
|
||||
|
||||
if (parentRole == nsIAccessibleRole::ROLE_OUTLINE)
|
||||
break;
|
||||
if (parentRole == nsIAccessibleRole::ROLE_GROUPING)
|
||||
++ groupLevel;
|
||||
|
||||
parent->GetParent(getter_AddRefs(nextParent));
|
||||
parent.swap(nextParent);
|
||||
}
|
||||
} else if (aRole == nsIAccessibleRole::ROLE_LISTITEM) {
|
||||
// Expose 'level' attribute on nested lists. We assume nested list is a last
|
||||
// child of listitem of parent list. We don't handle the case when nested
|
||||
// lists have more complex structure, for example when there are accessibles
|
||||
// between parent listitem and nested list.
|
||||
|
||||
// Calculate 'level' attribute based on number of parent listitems.
|
||||
nsCOMPtr<nsIAccessible> nextParent;
|
||||
while (parent) {
|
||||
PRUint32 parentRole = nsAccUtils::Role(parent);
|
||||
|
||||
if (parentRole == nsIAccessibleRole::ROLE_LISTITEM)
|
||||
++ groupLevel;
|
||||
else if (parentRole != nsIAccessibleRole::ROLE_LIST)
|
||||
break;
|
||||
|
||||
parent->GetParent(getter_AddRefs(nextParent));
|
||||
parent.swap(nextParent);
|
||||
}
|
||||
|
||||
if (groupLevel == 0) {
|
||||
// If this listitem is on top of nested lists then expose 'level'
|
||||
// attribute.
|
||||
nsCOMPtr<nsIAccessible> parent = GetParent();
|
||||
parent->GetFirstChild(getter_AddRefs(sibling));
|
||||
|
||||
while (sibling) {
|
||||
nsCOMPtr<nsIAccessible> siblingChild;
|
||||
sibling->GetLastChild(getter_AddRefs(siblingChild));
|
||||
if (nsAccUtils::Role(siblingChild) == nsIAccessibleRole::ROLE_LIST) {
|
||||
groupLevel = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
sibling->GetNextSibling(getter_AddRefs(nextSibling));
|
||||
sibling.swap(nextSibling);
|
||||
}
|
||||
} else
|
||||
groupLevel++; // level is 1-index based
|
||||
}
|
||||
|
||||
nsAccUtils::SetAccGroupAttrs(aAttributes, groupLevel, positionInGroup,
|
||||
setSize);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
|
|
@ -284,6 +284,17 @@ protected:
|
|||
*/
|
||||
PRUint32 GetActionRule(PRUint32 aStates);
|
||||
|
||||
/**
|
||||
* Compute group attributes ('posinset', 'setsize' and 'level') based
|
||||
* on accessible hierarchy. Used by GetAttributes() method if group attributes
|
||||
* weren't provided by ARIA or by internal accessible implementation.
|
||||
*
|
||||
* @param aRole [in] role of this accessible
|
||||
* @param aAttributes [in, out] object attributes
|
||||
*/
|
||||
nsresult ComputeGroupAttributes(PRUint32 aRole,
|
||||
nsIPersistentProperties *aAttributes);
|
||||
|
||||
/**
|
||||
* Fires platform accessible event. It's notification method only. It does
|
||||
* change nothing on Gecko side. Mostly you should use
|
||||
|
|
|
@ -64,7 +64,8 @@ _TEST_FILES =\
|
|||
test_bug420863.html \
|
||||
test_cssattrs.html \
|
||||
test_events_caretmove.html \
|
||||
$(warning test_groupattrs.xul temporarily disabled) \
|
||||
test_groupattrs.xul \
|
||||
test_groupattrs.html \
|
||||
$(warning test_table_indexes.html temporarily disabled) \
|
||||
test_nsIAccessible_actions.html \
|
||||
$(warning test_nsIAccessible_actions.xul temporarily disabled) \
|
||||
|
|
|
@ -4,15 +4,15 @@
|
|||
/**
|
||||
* Test object attributes.
|
||||
*
|
||||
* @param aID [in] the ID of DOM element having accessible
|
||||
* @param aAccOrElmOrID [in] the ID, DOM node or accessible
|
||||
* @param aAttrs [in] the map of expected object attributes
|
||||
* (name/value pairs)
|
||||
* @param aSkipUnexpectedAttrs [in] points this function doesn't fail if
|
||||
* unexpected attribute is encountered
|
||||
*/
|
||||
function testAttrs(aID, aAttrs, aSkipUnexpectedAttrs)
|
||||
function testAttrs(aAccOrElmOrID, aAttrs, aSkipUnexpectedAttrs)
|
||||
{
|
||||
var accessible = getAccessible(aID);
|
||||
var accessible = getAccessible(aAccOrElmOrID);
|
||||
if (!accessible)
|
||||
return;
|
||||
|
||||
|
@ -22,14 +22,35 @@ function testAttrs(aID, aAttrs, aSkipUnexpectedAttrs)
|
|||
} catch (e) { }
|
||||
|
||||
if (!attrs) {
|
||||
ok(false, "Can't get object attributes for " + aID);
|
||||
ok(false, "Can't get object attributes for " + aAccOrElmOrID);
|
||||
return;
|
||||
}
|
||||
|
||||
var errorMsg = " for " + aID;
|
||||
var errorMsg = " for " + aAccOrElmOrID;
|
||||
compareAttrs(errorMsg, attrs, aAttrs, aSkipUnexpectedAttrs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test group object attributes (posinset, setsize and level)
|
||||
*
|
||||
* @param aAccOrElmOrID [in] the ID, DOM node or accessible
|
||||
* @param aPosInSet [in] the value of 'posinset' attribute
|
||||
* @param aSetSize [in] the value of 'setsize' attribute
|
||||
* @param aLevel [in, optional] the value of 'level' attribute
|
||||
*/
|
||||
function testGroupAttrs(aAccOrElmOrID, aPosInSet, aSetSize, aLevel)
|
||||
{
|
||||
var attrs = {
|
||||
"posinset": String(aPosInSet),
|
||||
"setsize": String(aSetSize)
|
||||
};
|
||||
|
||||
if (aLevel)
|
||||
attrs["level"] = String(aLevel);
|
||||
|
||||
testAttrs(aAccOrElmOrID, attrs, true);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Text attributes.
|
||||
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<title>Group attributes tests</title>
|
||||
<link rel="stylesheet" type="text/css"
|
||||
href="chrome://mochikit/content/tests/SimpleTest/test.css" />
|
||||
|
||||
<script type="application/javascript"
|
||||
src="chrome://mochikit/content/MochiKit/packed.js"></script>
|
||||
<script type="application/javascript"
|
||||
src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
|
||||
<script type="application/javascript"
|
||||
src="chrome://mochikit/content/a11y/accessible/common.js"></script>
|
||||
<script type="application/javascript"
|
||||
src="chrome://mochikit/content/a11y/accessible/attributes.js"></script>
|
||||
|
||||
<script type="application/javascript">
|
||||
function doTest()
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// HTML select
|
||||
testGroupAttrs("opt1", 1, 2);
|
||||
testGroupAttrs("opt2", 2, 2);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// HTML ul/ol
|
||||
testGroupAttrs("li1", 1, 3);
|
||||
testGroupAttrs("li2", 2, 3);
|
||||
testGroupAttrs("li3", 3, 3);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// HTML ul/ol (nested lists)
|
||||
|
||||
testGroupAttrs("li4", 1, 3, 1);
|
||||
testGroupAttrs("li5", 2, 3, 1);
|
||||
testGroupAttrs("li6", 3, 3, 1);
|
||||
|
||||
testGroupAttrs("n_li4", 1, 3, 2);
|
||||
testGroupAttrs("n_li5", 2, 3, 2);
|
||||
testGroupAttrs("n_li6", 3, 3, 2);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// ARIA list
|
||||
testGroupAttrs("li7", 1, 3);
|
||||
testGroupAttrs("li8", 2, 3);
|
||||
testGroupAttrs("li9", 3, 3);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// ARIA list (nested lists)
|
||||
testGroupAttrs("li10", 1, 3, 1);
|
||||
testGroupAttrs("li11", 2, 3, 1);
|
||||
testGroupAttrs("li12", 3, 3, 1);
|
||||
|
||||
testGroupAttrs("n_li10", 1, 3, 2);
|
||||
testGroupAttrs("n_li11", 2, 3, 2);
|
||||
testGroupAttrs("n_li12", 3, 3, 2);
|
||||
|
||||
SimpleTest.finish();
|
||||
}
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
addLoadEvent(doTest);
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a target="_blank"
|
||||
href="https://bugzilla.mozilla.org/show_bug.cgi?id=468418"
|
||||
title="Expose level for nested lists in HTML">
|
||||
Mozilla Bug 468418
|
||||
</a>
|
||||
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none"></div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
|
||||
<select size="4">
|
||||
<option id="opt1">option1</option>
|
||||
<option id="opt2">option2</option>
|
||||
</select>
|
||||
|
||||
<ul>
|
||||
<li id="li1">Oranges</li>
|
||||
<li id="li2">Apples</li>
|
||||
<li id="li3">Bananas</li>
|
||||
</ul>
|
||||
|
||||
<ol>
|
||||
<li id="li4">Oranges</li>
|
||||
<li id="li5">Apples</li>
|
||||
<li id="li6">Bananas
|
||||
<ul>
|
||||
<li id="n_li4">Oranges</li>
|
||||
<li id="n_li5">Apples</li>
|
||||
<li id="n_li6">Bananas</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<span role="list">
|
||||
<span role="listitem" id="li7">Oranges</span>
|
||||
<span role="listitem" id="li8">Apples</span>
|
||||
<span role="listitem" id="li9">Bananas</span>
|
||||
</span>
|
||||
|
||||
<span role="list">
|
||||
<span role="listitem" id="li10">Oranges</span>
|
||||
<span role="listitem" id="li11">Apples</span>
|
||||
<span role="listitem" id="li12">Bananas
|
||||
<span role="list">
|
||||
<span role="listitem" id="n_li10">Oranges</span>
|
||||
<span role="listitem" id="n_li11">Apples</span>
|
||||
<span role="listitem" id="n_li12">Bananas</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</body>
|
||||
</html>
|
|
@ -18,19 +18,6 @@
|
|||
|
||||
<script type="application/javascript">
|
||||
<![CDATA[
|
||||
function testGroupAttrs(aID, aPosInSet, aSetSize, aLevel)
|
||||
{
|
||||
var attrs = {
|
||||
"posinset": aPosInSet,
|
||||
"setsize": aSetSize
|
||||
};
|
||||
|
||||
if (aLevel)
|
||||
attrs["level"] = aLevel;
|
||||
|
||||
testAttrs(aID, attrs, true);
|
||||
}
|
||||
|
||||
function doTest()
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
@ -40,26 +27,24 @@
|
|||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// xul:menu (bug 443881)
|
||||
if (navigator.platform == "Win32") {
|
||||
var menu1 = document.getElementById("menu_item1");
|
||||
menu1.open = true;
|
||||
|
||||
window.setTimeout(function() {
|
||||
var menu2 = document.getElementById("menu_item2");
|
||||
menu2.open = true;
|
||||
|
||||
window.setTimeout(function() {
|
||||
testGroupAttrs("menu_item1.1", "1", "1");
|
||||
testGroupAttrs("menu_item1.2", "1", "3");
|
||||
testGroupAttrs("menu_item1.4", "2", "3");
|
||||
testGroupAttrs("menu_item2", "3", "3");
|
||||
testGroupAttrs("menu_item2.1", "1", "2", "1");
|
||||
testGroupAttrs("menu_item2.2", "2", "2", "1");
|
||||
|
||||
SimpleTest.finish();
|
||||
}, 0);
|
||||
}, 0);
|
||||
}
|
||||
var menu1 = document.getElementById("menu_item1");
|
||||
menu1.open = true;
|
||||
|
||||
window.setTimeout(function() {
|
||||
var menu2 = document.getElementById("menu_item2");
|
||||
menu2.open = true;
|
||||
|
||||
window.setTimeout(function() {
|
||||
testGroupAttrs("menu_item1.1", "1", "1");
|
||||
testGroupAttrs("menu_item1.2", "1", "3");
|
||||
testGroupAttrs("menu_item1.4", "2", "3");
|
||||
testGroupAttrs("menu_item2", "3", "3");
|
||||
testGroupAttrs("menu_item2.1", "1", "2", "1");
|
||||
testGroupAttrs("menu_item2.2", "2", "2", "1");
|
||||
|
||||
SimpleTest.finish();
|
||||
}, 200);
|
||||
}, 200);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// ARIA menu (bug 441888)
|
||||
|
@ -67,9 +52,6 @@
|
|||
testGroupAttrs("aria-menuitemcheckbox", "2", "3");
|
||||
testGroupAttrs("aria-menuitemradio", "3", "3");
|
||||
testGroupAttrs("aria-menuitem2", "1", "1");
|
||||
if (navigator.platform != "Win32")
|
||||
SimpleTest.finish();
|
||||
|
||||
}
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
|
Загрузка…
Ссылка в новой задаче