зеркало из https://github.com/mozilla/pjs.git
bug 179656 - allow drag and drop reordering of tabs, patch originally based on miniT by dorando, r=vlad, a=shaver
This commit is contained in:
Родитель
9d38a066c5
Коммит
98970d4d4b
|
@ -22,6 +22,7 @@
|
|||
-
|
||||
- Contributor(s):
|
||||
- David Hyatt <hyatt@netscape.com> (Original Author of <tabbrowser>)
|
||||
- Mike Connor <mconnor@steelgryphon.com>
|
||||
-
|
||||
- Alternatively, the contents of this file may be used under the terms of
|
||||
- either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||
|
@ -56,7 +57,14 @@
|
|||
<xul:stringbundle src="chrome://global/locale/tabbrowser.properties"/>
|
||||
<xul:tabbox flex="1" eventnode="document" xbl:inherits="handleCtrlPageUpDown"
|
||||
onselect="if (!('updateCurrentBrowser' in this.parentNode) || event.target.localName != 'tabpanels') return; this.parentNode.updateCurrentBrowser();">
|
||||
<xul:hbox class="tabbrowser-strip chromeclass-toolbar" collapsed="true" tooltip="_child" context="_child">
|
||||
<xul:hbox id="tab-drop-indicator-bar">
|
||||
<xul:hbox id="tab-drop-indicator"/>
|
||||
</xul:hbox>
|
||||
<xul:hbox class="tabbrowser-strip chromeclass-toolbar" collapsed="true" tooltip="_child" context="_child"
|
||||
ondraggesture="nsDragAndDrop.startDrag(event, this.parentNode.parentNode); event.stopPropagation();"
|
||||
ondragover="nsDragAndDrop.dragOver(event, this.parentNode.parentNode); event.stopPropagation();"
|
||||
ondragdrop="nsDragAndDrop.drop(event, this.parentNode.parentNode); event.stopPropagation();"
|
||||
ondragexit="nsDragAndDrop.dragExit(event, this.parentNode.parentNode); event.stopPropagation();">
|
||||
<xul:tooltip onpopupshowing="event.preventBubble(); if (document.tooltipNode.hasAttribute('label')) { this.setAttribute('label', document.tooltipNode.getAttribute('label')); return true; } return false;"/>
|
||||
<xul:menupopup onpopupshowing="this.parentNode.parentNode.parentNode.updatePopupMenu(this);">
|
||||
<xul:menuitem label="&newTab.label;" accesskey="&newTab.accesskey;"
|
||||
|
@ -124,13 +132,16 @@
|
|||
document.getAnonymousNodes(this)[1]
|
||||
</field>
|
||||
<field name="mStrip">
|
||||
this.mTabBox.firstChild
|
||||
this.mTabBox.childNodes[1]
|
||||
</field>
|
||||
<field name="mTabContainer">
|
||||
this.mStrip.childNodes[2]
|
||||
</field>
|
||||
<field name="mPanelContainer">
|
||||
this.mTabBox.childNodes[1]
|
||||
this.mTabBox.childNodes[2]
|
||||
</field>
|
||||
<field name="mTabs">
|
||||
this.mTabContainer.childNodes
|
||||
</field>
|
||||
<field name="mStringBundle">
|
||||
document.getAnonymousNodes(this)[0]
|
||||
|
@ -165,12 +176,19 @@
|
|||
<field name="mModalDialogShowing">
|
||||
false
|
||||
</field>
|
||||
|
||||
<field name="arrowKeysShouldWrap" readonly="true">
|
||||
#ifdef XP_MACOSX
|
||||
true
|
||||
#else
|
||||
false
|
||||
#endif
|
||||
</field>
|
||||
|
||||
<method name="getBrowserAtIndex">
|
||||
<parameter name="aIndex"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
return this.mPanelContainer.childNodes[aIndex].firstChild.nextSibling;
|
||||
return this.mTabContainer.childNodes[aIndex].linkedBrowser;
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
@ -545,7 +563,7 @@
|
|||
<method name="updateCurrentBrowser">
|
||||
<body>
|
||||
<![CDATA[
|
||||
var newBrowser = this.getBrowserAtIndex(this.mPanelContainer.selectedIndex);
|
||||
var newBrowser = this.getBrowserAtIndex(this.mTabContainer.selectedIndex);
|
||||
if (this.mCurrentBrowser == newBrowser)
|
||||
return;
|
||||
|
||||
|
@ -620,7 +638,7 @@
|
|||
p.onLocationChange(webProgress, null, loc);
|
||||
if (securityUI)
|
||||
p.onSecurityChange(webProgress, null, securityUI.state);
|
||||
var listener = this.mTabListeners[this.mPanelContainer.selectedIndex];
|
||||
var listener = this.mTabListeners[this.mTabContainer.selectedIndex];
|
||||
if (listener.mIcon) {
|
||||
if (this.isFavIconKnownMissing(listener.mIcon))
|
||||
listener.mIcon = null;
|
||||
|
@ -819,7 +837,7 @@
|
|||
|
||||
var tabBrowser = this.parentNode.parentNode.parentNode.parentNode;
|
||||
|
||||
var tab = tabBrowser.mTabContainer.childNodes[i];
|
||||
var tab = document.getAnonymousElementByAttribute(tabBrowser, "linkedpanel", this.parentNode.id);
|
||||
tabBrowser.setTabTitle(tab);
|
||||
|
||||
if (tab == tabBrowser.mCurrentTab)
|
||||
|
@ -1021,6 +1039,14 @@
|
|||
|
||||
b._fastFind = this.fastFind;
|
||||
|
||||
var uniqueId = "panel" + Date.now() + position;
|
||||
this.mPanelContainer.lastChild.id = uniqueId;
|
||||
t.linkedPanel = uniqueId;
|
||||
t.linkedBrowser = b;
|
||||
t._tPos = position;
|
||||
if (t.previousSibling.selected)
|
||||
t.setAttribute("afterselected", true);
|
||||
|
||||
if (!blank) {
|
||||
// pretend the user typed this so it'll be available till
|
||||
// the document successfully loads
|
||||
|
@ -1145,7 +1171,7 @@
|
|||
|
||||
var index = -1;
|
||||
if (this.mCurrentTab == aTab)
|
||||
index = this.mPanelContainer.selectedIndex;
|
||||
index = this.mTabContainer.selectedIndex;
|
||||
else {
|
||||
// Find and locate the tab in our list.
|
||||
for (var i = 0; i < l; i++)
|
||||
|
@ -1168,7 +1194,7 @@
|
|||
oldBrowser.setAttribute("type", "content");
|
||||
|
||||
// Now select the new tab before nuking the old one.
|
||||
var currentIndex = this.mPanelContainer.selectedIndex;
|
||||
var currentIndex = this.mTabContainer.selectedIndex;
|
||||
|
||||
var newIndex = -1;
|
||||
if (currentIndex > index)
|
||||
|
@ -1198,11 +1224,16 @@
|
|||
oldBrowser.destroy();
|
||||
|
||||
this.mTabContainer.removeChild(oldTab);
|
||||
this.mPanelContainer.removeChild(this.mPanelContainer.childNodes[index]);
|
||||
this.mPanelContainer.removeChild(oldBrowser.parentNode);
|
||||
|
||||
this.selectedTab = this.mTabContainer.childNodes[newIndex];
|
||||
this.mPanelContainer.selectedIndex = newIndex;
|
||||
|
||||
var i;
|
||||
for (i = oldTab._tPos; i < this.mTabContainer.childNodes.length; i++) {
|
||||
this.mTabContainer.childNodes[i]._tPos = i;
|
||||
}
|
||||
this.mTabBox.selectedPanel = this.getBrowserForTab(this.mCurrentTab).parentNode;
|
||||
this.mCurrentTab.selected = true;
|
||||
|
||||
this.updateCurrentBrowser();
|
||||
|
||||
// see comment above destroy above
|
||||
|
@ -1317,16 +1348,7 @@
|
|||
<parameter name="aTab"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
if (this.mCurrentTab == aTab)
|
||||
return this.mCurrentBrowser;
|
||||
|
||||
for (var i = 0; i < this.mTabContainer.childNodes.length; i++) {
|
||||
if (this.mTabContainer.childNodes[i] == aTab) {
|
||||
return this.getBrowserAtIndex(i);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return aTab.linkedBrowser;
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
@ -1355,26 +1377,73 @@
|
|||
readonly="true"/>
|
||||
|
||||
|
||||
<property name="browsers"
|
||||
onget="return this.mPanelContainer.getElementsByTagName('browser');"
|
||||
readonly="true"/>
|
||||
<property name="browsers" readonly="true">
|
||||
<getter>
|
||||
<![CDATA[
|
||||
var browsers = [];
|
||||
var i;
|
||||
browsers.item = function(i) {return this[i];}
|
||||
for (i = 0; i < this.mTabContainer.childNodes.length; i++)
|
||||
browsers.push(this.mTabContainer.childNodes[i].linkedBrowser);
|
||||
return browsers;
|
||||
]]>
|
||||
</getter>
|
||||
</property>
|
||||
|
||||
<!-- Drag and drop observer API -->
|
||||
<!--<method name="onDragStart">
|
||||
<method name="onDragStart">
|
||||
<parameter name="aEvent"/>
|
||||
<parameter name="aXferData"/>
|
||||
<parameter name="aDragAction"/>
|
||||
<body/>
|
||||
</method>-->
|
||||
<body>
|
||||
<![CDATA[
|
||||
if (aEvent.target.localName == "tab") {
|
||||
aXferData.data = new TransferData();
|
||||
aXferData.data.addDataForFlavour("text/x-moz-tab", aEvent.target._tPos);
|
||||
|
||||
var URI = this.getBrowserForTab(aEvent.target).currentURI;
|
||||
if (URI) {
|
||||
aXferData.data.addDataForFlavour("text/unicode", URI.spec);
|
||||
aXferData.data.addDataForFlavour("text/x-moz-url", URI.spec + "\n" + aEvent.target.label);
|
||||
aXferData.data.addDataForFlavour("text/html", '<a href="' + URI.spec + '">' + aEvent.target.label + '</a>');
|
||||
}
|
||||
}
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<method name="onDragOver">
|
||||
<parameter name="aEvent"/>
|
||||
<parameter name="aFlavour"/>
|
||||
<parameter name="aDragSession"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
return; // Just having this makes our feedback correct.
|
||||
]]>
|
||||
<![CDATA[
|
||||
if (aDragSession.canDrop && aDragSession.sourceNode.parentNode == this.mTabContainer) {
|
||||
var newIndex = this.getNewIndex(aEvent);
|
||||
|
||||
var ib = document.getElementById('tab-drop-indicator-bar');
|
||||
var ind = document.getElementById('tab-drop-indicator');
|
||||
ib.setAttribute('dragging','true');
|
||||
|
||||
if (window.getComputedStyle(this.parentNode, null).direction == "ltr") {
|
||||
if (newIndex == this.mTabs.length) {
|
||||
ind.style.marginLeft = this.mTabs[newIndex-1].boxObject.x +
|
||||
this.mTabs[newIndex-1].boxObject.width - this.boxObject.x - 7 + 'px';
|
||||
} else {
|
||||
ind.style.marginLeft = this.mTabs[newIndex].boxObject.x - this.boxObject.x - 7 + 'px';
|
||||
}
|
||||
} else {
|
||||
if (newIndex == gBrowser.mTabs.length) {
|
||||
ind.style.marginRight = gBrowser.boxObject.width + gBrowser.boxObject.x -
|
||||
gBrowser.mTabs[newIndex-1].boxObject.x + 'px';
|
||||
} else {
|
||||
ind.style.marginRight = gBrowser.boxObject.width + gBrowser.boxObject.x -
|
||||
gBrowser.mTabs[newIndex].boxObject.x -
|
||||
gBrowser.mTabs[newIndex].boxObject.width + 'px';
|
||||
}
|
||||
}
|
||||
}
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
|
@ -1384,30 +1453,49 @@
|
|||
<parameter name="aDragSession"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
var url = transferUtils.retrieveURLFromData(aXferData.data, aXferData.flavour.contentType);
|
||||
if (aDragSession.sourceNode.parentNode == this.mTabContainer) {
|
||||
var newIndex = this.getNewIndex(aEvent);
|
||||
if (newIndex > aXferData.data)
|
||||
newIndex--;
|
||||
if (newIndex != aXferData.data)
|
||||
this.moveTabTo(this.mTabs[aXferData.data], newIndex);
|
||||
} else {
|
||||
var url = transferUtils.retrieveURLFromData(aXferData.data, aXferData.flavour.contentType);
|
||||
|
||||
// valid urls don't contain spaces ' '; if we have a space it isn't a valid url.
|
||||
// Also disallow dropping javascript: or data: urls--bail out
|
||||
if (!url || !url.length || url.indexOf(" ", 0) != -1 ||
|
||||
/^\s*(javascript|data):/.test(url))
|
||||
return;
|
||||
// valid urls don't contain spaces ' '; if we have a space it isn't a valid url.
|
||||
// Also disallow dropping javascript: or data: urls--bail out
|
||||
if (!url || !url.length || url.indexOf(" ", 0) != -1 ||
|
||||
/^\s*(javascript|data):/.test(url))
|
||||
return;
|
||||
|
||||
this.dragDropSecurityCheck(aEvent, aDragSession, url);
|
||||
this.dragDropSecurityCheck(aEvent, aDragSession, url);
|
||||
|
||||
var bgLoad = this.mPrefs.getBoolPref("browser.tabs.loadInBackground");
|
||||
var bgLoad = this.mPrefs.getBoolPref("browser.tabs.loadInBackground");
|
||||
|
||||
var tab = null;
|
||||
if (aEvent.originalTarget.localName != "tab") {
|
||||
// We're adding a new tab.
|
||||
tab = this.addTab(getShortcutOrURI(url));
|
||||
var tab = null;
|
||||
if (aEvent.originalTarget.localName != "tab") {
|
||||
// We're adding a new tab.
|
||||
tab = this.addTab(getShortcutOrURI(url));
|
||||
}
|
||||
else {
|
||||
// Load in an existing tab.
|
||||
tab = aEvent.originalTarget;
|
||||
this.getBrowserForTab(tab).loadURI(getShortcutOrURI(url));
|
||||
}
|
||||
if (this.mCurrentTab != tab && !bgLoad)
|
||||
this.selectedTab = tab;
|
||||
}
|
||||
else {
|
||||
// Load in an existing tab.
|
||||
tab = aEvent.originalTarget;
|
||||
this.getBrowserForTab(tab).loadURI(getShortcutOrURI(url));
|
||||
}
|
||||
if (this.mCurrentTab != tab && !bgLoad)
|
||||
this.selectedTab = tab;
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<method name="onDragExit">
|
||||
<parameter name="aEvent"/>
|
||||
<parameter name="aDragSession"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
var ib = document.getElementById('tab-drop-indicator-bar');
|
||||
ib.setAttribute('dragging','false');
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
@ -1416,6 +1504,7 @@
|
|||
<body>
|
||||
<![CDATA[
|
||||
var flavourSet = new FlavourSet();
|
||||
flavourSet.appendFlavour("text/x-moz-tab"); // this has to be first to support DnD reordering
|
||||
flavourSet.appendFlavour("text/x-moz-url");
|
||||
flavourSet.appendFlavour("text/unicode");
|
||||
flavourSet.appendFlavour("application/x-moz-file", "nsIFile");
|
||||
|
@ -1423,7 +1512,116 @@
|
|||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
|
||||
<method name="moveTabTo">
|
||||
<parameter name="aTab"/>
|
||||
<parameter name="aIndex"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
this.mTabFilters.splice(aIndex, 0, this.mTabFilters.splice(aTab._tPos, 1)[0]);
|
||||
this.mTabListeners.splice(aIndex, 0, this.mTabListeners.splice(aTab._tPos, 1)[0]);
|
||||
|
||||
aIndex = aIndex < aTab._tPos ? aIndex: aIndex+1;
|
||||
this.mCurrentTab.selected = false;
|
||||
this.mTabContainer.insertBefore(aTab, this.mTabContainer.childNodes[aIndex]);
|
||||
var i;
|
||||
for (i = 0; i < this.mTabContainer.childNodes.length; i++) {
|
||||
this.mTabContainer.childNodes[i]._tPos = i;
|
||||
}
|
||||
this.mCurrentTab.selected = true;
|
||||
return aTab;
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<method name="getNewIndex">
|
||||
<parameter name="aEvent"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
var i;
|
||||
if (window.getComputedStyle(this.parentNode, null).direction == "ltr") {
|
||||
for (i = aEvent.target.localName == "tab" ? aEvent.target._tPos : 0; i < this.mTabs.length; i++)
|
||||
if (aEvent.clientX < this.mTabs[i].boxObject.x + this.mTabs[i].boxObject.width / 2)
|
||||
return i;
|
||||
} else {
|
||||
for (i = aEvent.target.localName == "tab" ? aEvent.target._tPos : 0; i < this.mTabs.length; i++)
|
||||
if (aEvent.clientX > this.mTabs[i].boxObject.x + this.mTabs[i].boxObject.width / 2)
|
||||
return i;
|
||||
}
|
||||
|
||||
return this.mTabs.length;
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
|
||||
<method name="moveTabForward">
|
||||
<body>
|
||||
<![CDATA[
|
||||
var tabPos = this.mCurrentTab._tPos;
|
||||
if (tabPos < this.browsers.length - 1) {
|
||||
this.moveTabTo(this.mCurrentTab, tabPos + 1);
|
||||
this.mCurrentTab.focus();
|
||||
}
|
||||
else if (this.arrowKeysShouldWrap)
|
||||
this.moveTabToStart();
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<method name="moveTabBackward">
|
||||
<body>
|
||||
<![CDATA[
|
||||
var tabPos = this.mCurrentTab._tPos;
|
||||
if (tabPos > 0) {
|
||||
this.moveTabTo(this.mCurrentTab, tabPos - 1);
|
||||
this.mCurrentTab.focus();
|
||||
}
|
||||
else if (this.arrowKeysShouldWrap)
|
||||
this.moveTabToEnd();
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<method name="moveTabToStart">
|
||||
<body>
|
||||
<![CDATA[
|
||||
var tabPos = this.mCurrentTab._tPos;
|
||||
if (tabPos > 0) {
|
||||
this.moveTabTo(this.mCurrentTab, 0);
|
||||
this.mCurrentTab.focus();
|
||||
}
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<method name="moveTabToEnd">
|
||||
<body>
|
||||
<![CDATA[
|
||||
var tabPos = this.mCurrentTab._tPos;
|
||||
if (tabPos < this.browsers.length - 1) {
|
||||
this.moveTabTo(this.mCurrentTab,
|
||||
this.browsers.length - 1);
|
||||
this.mCurrentTab.focus();
|
||||
}
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<method name="moveTabOver">
|
||||
<parameter name="aEvent"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
var direction = window.getComputedStyle(this.parentNode, null).direction;
|
||||
if ((direction == "ltr" && aEvent.keyCode == KeyEvent.DOM_VK_RIGHT) ||
|
||||
(direction == "rtl" && aEvent.keyCode == KeyEvent.DOM_VK_LEFT))
|
||||
this.moveTabForward();
|
||||
else
|
||||
this.moveTabBackward();
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<!-- BEGIN FORWARDED BROWSER PROPERTIES. IF YOU ADD A PROPERTY TO THE BROWSER ELEMENT
|
||||
MAKE SURE TO ADD IT HERE AS WELL. -->
|
||||
<property name="canGoBack"
|
||||
|
@ -1689,15 +1887,44 @@
|
|||
<![CDATA[({
|
||||
tabbrowser: this,
|
||||
handleEvent: function handleEvent(aEvent) {
|
||||
if (!aEvent.isTrusted) {
|
||||
// Don't let untrusted events mess with tabs.
|
||||
return;
|
||||
}
|
||||
|
||||
#ifndef XP_MACOSX
|
||||
if (aEvent.ctrlKey && aEvent.keyCode == KeyEvent.DOM_VK_F4 && this.tabbrowser.mTabBox.handleCtrlPageUpDown)
|
||||
this.tabbrowser.removeCurrentTab();
|
||||
if (!aEvent.isTrusted) {
|
||||
// Don't let untrusted events mess with tabs.
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef XP_MACOSX
|
||||
if ('metaKey' in aEvent && aEvent.metaKey) {
|
||||
#else
|
||||
if ('ctrlKey' in aEvent && aEvent.ctrlKey) {
|
||||
if (aEvent.keyCode == KeyEvent.DOM_VK_F4 &&
|
||||
this.tabbrowser.mTabBox.handleCtrlPageUpDown) {
|
||||
this.tabbrowser.removeCurrentTab();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (aEvent.target.localName == "tabbrowser") {
|
||||
switch (aEvent.keyCode) {
|
||||
case KeyEvent.DOM_VK_UP:
|
||||
this.tabbrowser.moveTabBackward();
|
||||
break;
|
||||
case KeyEvent.DOM_VK_DOWN:
|
||||
this.tabbrowser.moveTabForward();
|
||||
break;
|
||||
case KeyEvent.DOM_VK_RIGHT:
|
||||
case KeyEvent.DOM_VK_LEFT:
|
||||
this.tabbrowser.moveTabOver(aEvent);
|
||||
break;
|
||||
case KeyEvent.DOM_VK_HOME:
|
||||
this.tabbrowser.moveTabToStart();
|
||||
break;
|
||||
case KeyEvent.DOM_VK_END:
|
||||
this.tabbrowser.moveTabToEnd();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})]]>
|
||||
</field>
|
||||
|
@ -1719,10 +1946,16 @@
|
|||
|
||||
<constructor>
|
||||
<![CDATA[
|
||||
this.mCurrentBrowser = this.getBrowserAtIndex(0);
|
||||
this.mCurrentBrowser = this.mPanelContainer.childNodes[0].firstChild.nextSibling;
|
||||
this.mCurrentTab = this.mTabContainer.firstChild;
|
||||
this.mTabBox.handleCtrlTab = !/Mac/.test(navigator.platform);
|
||||
document.addEventListener("keypress", this._keyEventHandler, false);
|
||||
|
||||
var uniqueId = "panel" + Date.now();
|
||||
this.mPanelContainer.childNodes[0].id = uniqueId;
|
||||
this.mTabContainer.childNodes[0].linkedPanel = uniqueId;
|
||||
this.mTabContainer.childNodes[0]._tPos = 0;
|
||||
this.mTabContainer.childNodes[0].linkedBrowser = this.mPanelContainer.childNodes[0].firstChild.nextSibling;
|
||||
]]>
|
||||
</constructor>
|
||||
|
||||
|
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 515 B |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 515 B |
Загрузка…
Ссылка в новой задаче