зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1859245 - part 3: Make `JoinNodesTransaction` and `SplitNodeTransaction` handle only the new direction mode r=m_kato
Depends on D191606 Differential Revision: https://phabricator.services.mozilla.com/D191607
This commit is contained in:
Родитель
22a3483011
Коммит
982ddb7a9a
|
@ -6751,8 +6751,8 @@ void EditorBase::TopLevelEditSubActionData::WillDeleteContent(
|
|||
}
|
||||
|
||||
void EditorBase::TopLevelEditSubActionData::DidSplitContent(
|
||||
EditorBase& aEditorBase, nsIContent& aSplitContent, nsIContent& aNewContent,
|
||||
SplitNodeDirection aSplitNodeDirection) {
|
||||
EditorBase& aEditorBase, nsIContent& aSplitContent,
|
||||
nsIContent& aNewContent) {
|
||||
MOZ_ASSERT(aEditorBase.AsHTMLEditor());
|
||||
|
||||
if (!aEditorBase.mInitSucceeded || aEditorBase.Destroyed()) {
|
||||
|
@ -6763,14 +6763,9 @@ void EditorBase::TopLevelEditSubActionData::DidSplitContent(
|
|||
return; // Temporarily disabled by edit sub-action handler.
|
||||
}
|
||||
|
||||
DebugOnly<nsresult> rvIgnored =
|
||||
aSplitNodeDirection == SplitNodeDirection::LeftNodeIsNewOne
|
||||
? AddRangeToChangedRange(*aEditorBase.AsHTMLEditor(),
|
||||
EditorRawDOMPoint(&aNewContent, 0),
|
||||
EditorRawDOMPoint(&aSplitContent, 0))
|
||||
: AddRangeToChangedRange(*aEditorBase.AsHTMLEditor(),
|
||||
EditorRawDOMPoint::AtEndOf(aSplitContent),
|
||||
EditorRawDOMPoint::AtEndOf(aNewContent));
|
||||
DebugOnly<nsresult> rvIgnored = AddRangeToChangedRange(
|
||||
*aEditorBase.AsHTMLEditor(), EditorRawDOMPoint::AtEndOf(aSplitContent),
|
||||
EditorRawDOMPoint::AtEndOf(aNewContent));
|
||||
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
|
||||
"TopLevelEditSubActionData::AddRangeToChangedRange() "
|
||||
"failed, but ignored");
|
||||
|
|
|
@ -849,8 +849,7 @@ class EditorBase : public nsIEditor,
|
|||
void WillDeleteContent(EditorBase& aEditorBase,
|
||||
nsIContent& aRemovingContent);
|
||||
void DidSplitContent(EditorBase& aEditorBase, nsIContent& aSplitContent,
|
||||
nsIContent& aNewContent,
|
||||
SplitNodeDirection aSplitNodeDirection);
|
||||
nsIContent& aNewContent);
|
||||
void DidJoinContents(EditorBase& aEditorBase,
|
||||
const EditorRawDOMPoint& aJoinedPoint);
|
||||
void DidInsertText(EditorBase& aEditorBase,
|
||||
|
|
|
@ -5109,8 +5109,8 @@ Result<SplitNodeResult, nsresult> HTMLEditor::SplitNodeWithTransaction(
|
|||
if (NS_WARN_IF(!newContent) || NS_WARN_IF(!splitContent)) {
|
||||
return Err(NS_ERROR_FAILURE);
|
||||
}
|
||||
TopLevelEditSubActionDataRef().DidSplitContent(
|
||||
*this, *splitContent, *newContent, transaction->GetSplitNodeDirection());
|
||||
TopLevelEditSubActionDataRef().DidSplitContent(*this, *splitContent,
|
||||
*newContent);
|
||||
if (NS_WARN_IF(!newContent->IsInComposedDoc()) ||
|
||||
NS_WARN_IF(!splitContent->IsInComposedDoc())) {
|
||||
return Err(NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE);
|
||||
|
@ -5554,8 +5554,7 @@ void HTMLEditor::DidJoinNodesTransaction(
|
|||
mTextServicesDocument) {
|
||||
textServicesDocument->DidJoinContents(
|
||||
aTransaction.CreateJoinedPoint<EditorRawDOMPoint>(),
|
||||
*aTransaction.GetRemovedContent(),
|
||||
aTransaction.GetJoinNodesDirection());
|
||||
*aTransaction.GetRemovedContent());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5563,9 +5562,7 @@ void HTMLEditor::DidJoinNodesTransaction(
|
|||
for (auto& listener : mActionListeners.Clone()) {
|
||||
DebugOnly<nsresult> rvIgnored = listener->DidJoinContents(
|
||||
aTransaction.CreateJoinedPoint<EditorRawDOMPoint>(),
|
||||
aTransaction.GetRemovedContent(),
|
||||
aTransaction.GetJoinNodesDirection() ==
|
||||
JoinNodesDirection::LeftNodeIntoRightNode);
|
||||
aTransaction.GetRemovedContent());
|
||||
NS_WARNING_ASSERTION(
|
||||
NS_SUCCEEDED(rvIgnored),
|
||||
"nsIEditActionListener::DidJoinContents() failed, but ignored");
|
||||
|
|
|
@ -5,10 +5,9 @@
|
|||
|
||||
#include "JoinNodesTransaction.h"
|
||||
|
||||
#include "EditorDOMPoint.h" // for EditorDOMPoint, etc.
|
||||
#include "HTMLEditHelpers.h" // for SplitNodeResult
|
||||
#include "JoinSplitNodeDirection.h" // JoinNodesDirection
|
||||
#include "HTMLEditor.h" // for HTMLEditor
|
||||
#include "EditorDOMPoint.h" // for EditorDOMPoint, etc.
|
||||
#include "HTMLEditHelpers.h" // for SplitNodeResult
|
||||
#include "HTMLEditor.h" // for HTMLEditor
|
||||
#include "HTMLEditorInlines.h"
|
||||
#include "HTMLEditUtils.h"
|
||||
|
||||
|
@ -42,14 +41,8 @@ JoinNodesTransaction::JoinNodesTransaction(HTMLEditor& aHTMLEditor,
|
|||
nsIContent& aLeftContent,
|
||||
nsIContent& aRightContent)
|
||||
: mHTMLEditor(&aHTMLEditor),
|
||||
mRemovedContent(aHTMLEditor.GetJoinNodesDirection() ==
|
||||
JoinNodesDirection::LeftNodeIntoRightNode
|
||||
? &aLeftContent
|
||||
: &aRightContent),
|
||||
mKeepingContent(aHTMLEditor.GetJoinNodesDirection() ==
|
||||
JoinNodesDirection::LeftNodeIntoRightNode
|
||||
? &aRightContent
|
||||
: &aLeftContent) {
|
||||
mRemovedContent(&aRightContent),
|
||||
mKeepingContent(&aLeftContent) {
|
||||
// printf("JoinNodesTransaction size: %zu\n", sizeof(JoinNodesTransaction));
|
||||
static_assert(sizeof(JoinNodesTransaction) <= 64,
|
||||
"Transaction classes may be created a lot and may be alive "
|
||||
|
@ -71,9 +64,7 @@ std::ostream& operator<<(std::ostream& aStream,
|
|||
aStream << " (" << *aTransaction.mKeepingContent << ")";
|
||||
}
|
||||
aStream << ", mJoinedOffset=" << aTransaction.mJoinedOffset
|
||||
<< ", mHTMLEditor=" << aTransaction.mHTMLEditor.get()
|
||||
<< ", GetJoinNodesDirection()="
|
||||
<< aTransaction.GetJoinNodesDirection() << " }";
|
||||
<< ", mHTMLEditor=" << aTransaction.mHTMLEditor.get() << " }";
|
||||
return aStream;
|
||||
}
|
||||
|
||||
|
@ -84,16 +75,6 @@ NS_IMPL_CYCLE_COLLECTION_INHERITED(JoinNodesTransaction, EditTransactionBase,
|
|||
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(JoinNodesTransaction)
|
||||
NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
|
||||
|
||||
SplitNodeDirection JoinNodesTransaction::GetSplitNodeDirection() const {
|
||||
return MOZ_LIKELY(mHTMLEditor) ? mHTMLEditor->GetSplitNodeDirection()
|
||||
: SplitNodeDirection::LeftNodeIsNewOne;
|
||||
}
|
||||
|
||||
JoinNodesDirection JoinNodesTransaction::GetJoinNodesDirection() const {
|
||||
return MOZ_LIKELY(mHTMLEditor) ? mHTMLEditor->GetJoinNodesDirection()
|
||||
: JoinNodesDirection::LeftNodeIntoRightNode;
|
||||
}
|
||||
|
||||
bool JoinNodesTransaction::CanDoIt() const {
|
||||
if (NS_WARN_IF(!mKeepingContent) || NS_WARN_IF(!mRemovedContent) ||
|
||||
NS_WARN_IF(!mHTMLEditor) ||
|
||||
|
@ -140,22 +121,16 @@ nsresult JoinNodesTransaction::DoTransactionInternal(
|
|||
// For now, setting mJoinedOffset to removed content length so that
|
||||
// CreateJoinedPoint returns a point in mKeepingContent whose offset is
|
||||
// the result if all content in mRemovedContent are moved to start or end of
|
||||
// mKeepingContent without any intervation. The offset will be adjusted
|
||||
// mKeepingContent without any intervention. The offset will be adjusted
|
||||
// below.
|
||||
mJoinedOffset =
|
||||
GetJoinNodesDirection() == JoinNodesDirection::LeftNodeIntoRightNode
|
||||
? mRemovedContent->Length()
|
||||
: mKeepingContent->Length();
|
||||
mJoinedOffset = mKeepingContent->Length();
|
||||
|
||||
const OwningNonNull<HTMLEditor> htmlEditor = *mHTMLEditor;
|
||||
const OwningNonNull<nsIContent> removingContent = *mRemovedContent;
|
||||
const OwningNonNull<nsIContent> keepingContent = *mKeepingContent;
|
||||
nsresult rv;
|
||||
// Let's try to get actual joined point with the tacker.
|
||||
EditorDOMPoint joinNodesPoint =
|
||||
GetJoinNodesDirection() == JoinNodesDirection::LeftNodeIntoRightNode
|
||||
? EditorDOMPoint(keepingContent, 0u)
|
||||
: EditorDOMPoint::AtEndOf(keepingContent);
|
||||
auto joinNodesPoint = EditorDOMPoint::AtEndOf(keepingContent);
|
||||
{
|
||||
AutoTrackDOMPoint trackJoinNodePoint(htmlEditor->RangeUpdaterRef(),
|
||||
&joinNodesPoint);
|
||||
|
|
|
@ -60,11 +60,6 @@ class JoinNodesTransaction final : public EditTransactionBase {
|
|||
|
||||
MOZ_CAN_RUN_SCRIPT NS_IMETHOD RedoTransaction() override;
|
||||
|
||||
// Note that we don't support join/split node direction switching per
|
||||
// transaction.
|
||||
[[nodiscard]] SplitNodeDirection GetSplitNodeDirection() const;
|
||||
[[nodiscard]] JoinNodesDirection GetJoinNodesDirection() const;
|
||||
|
||||
/**
|
||||
* GetExistingContent() and GetRemovedContent() never returns nullptr
|
||||
* unless the cycle collector clears them out.
|
||||
|
|
|
@ -10,8 +10,7 @@
|
|||
#include "HTMLEditor.h" // for HTMLEditor
|
||||
#include "HTMLEditorInlines.h"
|
||||
#include "HTMLEditUtils.h"
|
||||
#include "JoinSplitNodeDirection.h" // for SplitNodeDirection
|
||||
#include "SelectionState.h" // for AutoTrackDOMPoint and RangeUpdater
|
||||
#include "SelectionState.h" // for AutoTrackDOMPoint and RangeUpdater
|
||||
|
||||
#include "mozilla/Logging.h"
|
||||
#include "mozilla/Maybe.h"
|
||||
|
@ -71,9 +70,7 @@ std::ostream& operator<<(std::ostream& aStream,
|
|||
aStream << " (" << *aTransaction.mSplitContent << ")";
|
||||
}
|
||||
aStream << ", mSplitOffset=" << aTransaction.mSplitOffset
|
||||
<< ", mHTMLEditor=" << aTransaction.mHTMLEditor.get()
|
||||
<< ", GetSplitNodeDirection()="
|
||||
<< aTransaction.GetSplitNodeDirection() << " }";
|
||||
<< ", mHTMLEditor=" << aTransaction.mHTMLEditor.get() << " }";
|
||||
return aStream;
|
||||
}
|
||||
|
||||
|
@ -86,16 +83,6 @@ NS_IMPL_RELEASE_INHERITED(SplitNodeTransaction, EditTransactionBase)
|
|||
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SplitNodeTransaction)
|
||||
NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase)
|
||||
|
||||
SplitNodeDirection SplitNodeTransaction::GetSplitNodeDirection() const {
|
||||
return MOZ_LIKELY(mHTMLEditor) ? mHTMLEditor->GetSplitNodeDirection()
|
||||
: SplitNodeDirection::LeftNodeIsNewOne;
|
||||
}
|
||||
|
||||
JoinNodesDirection SplitNodeTransaction::GetJoinNodesDirection() const {
|
||||
return MOZ_LIKELY(mHTMLEditor) ? mHTMLEditor->GetJoinNodesDirection()
|
||||
: JoinNodesDirection::LeftNodeIntoRightNode;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP SplitNodeTransaction::DoTransaction() {
|
||||
MOZ_LOG(GetLogModule(), LogLevel::Info,
|
||||
("%p SplitNodeTransaction::%s this=%s", this, __FUNCTION__,
|
||||
|
@ -183,20 +170,12 @@ NS_IMETHODIMP SplitNodeTransaction::UndoTransaction() {
|
|||
const OwningNonNull<HTMLEditor> htmlEditor = *mHTMLEditor;
|
||||
const OwningNonNull<nsIContent> keepingContent = *mSplitContent;
|
||||
const OwningNonNull<nsIContent> removingContent = *mNewContent;
|
||||
nsresult rv;
|
||||
EditorDOMPoint joinedPoint;
|
||||
{
|
||||
// Unfortunately, we cannot track joining point if moving right node content
|
||||
// into left node since it cannot track changes from web apps and HTMLEditor
|
||||
// never removes the content of the left node. So it should be true that
|
||||
// we don't need to track the point in the direction.
|
||||
Maybe<AutoTrackDOMPoint> trackJoinedPoint;
|
||||
if (GetJoinNodesDirection() == JoinNodesDirection::LeftNodeIntoRightNode) {
|
||||
joinedPoint.Set(keepingContent, 0u);
|
||||
trackJoinedPoint.emplace(htmlEditor->RangeUpdaterRef(), &joinedPoint);
|
||||
}
|
||||
rv = htmlEditor->DoJoinNodes(keepingContent, removingContent);
|
||||
}
|
||||
// Unfortunately, we cannot track joining point if moving right node content
|
||||
// into left node since it cannot track changes from web apps and HTMLEditor
|
||||
// never removes the content of the left node. So it should be true that
|
||||
// we don't need to track the point in this case.
|
||||
nsresult rv = htmlEditor->DoJoinNodes(keepingContent, removingContent);
|
||||
if (NS_SUCCEEDED(rv)) {
|
||||
// Adjust split offset for redo here
|
||||
if (joinedPoint.IsSet()) {
|
||||
|
|
|
@ -53,11 +53,6 @@ class SplitNodeTransaction final : public EditTransactionBase {
|
|||
|
||||
MOZ_CAN_RUN_SCRIPT NS_IMETHOD RedoTransaction() override;
|
||||
|
||||
// Note that we don't support join/split node direction switching per
|
||||
// transaction.
|
||||
[[nodiscard]] SplitNodeDirection GetSplitNodeDirection() const;
|
||||
[[nodiscard]] JoinNodesDirection GetJoinNodesDirection() const;
|
||||
|
||||
nsIContent* GetSplitContent() const { return mSplitContent; }
|
||||
nsIContent* GetNewContent() const { return mNewContent; }
|
||||
nsINode* GetParentNode() const { return mParentNode; }
|
||||
|
|
|
@ -50,16 +50,10 @@ interface nsIEditActionListener : nsISupports
|
|||
* right node. Otherwise, it points start of inserted
|
||||
* right node content in the left node.
|
||||
* @param aRemovedNode The removed node.
|
||||
* @param aLeftNodeWasRemoved
|
||||
* true if left node is removed and its contents were
|
||||
* moved into start of the right node.
|
||||
* false if right node is removed and its contents were
|
||||
* moved into end of the left node.
|
||||
*/
|
||||
[noscript]
|
||||
void DidJoinContents([const] in EditorRawDOMPointRef aJoinedPoint,
|
||||
[const] in Node aRemovedNode,
|
||||
in bool aLeftNodeWasRemoved);
|
||||
[const] in Node aRemovedNode);
|
||||
|
||||
/**
|
||||
* Called after the editor inserts text.
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
#include "FilteredContentIterator.h" // for FilteredContentIterator
|
||||
#include "HTMLEditHelpers.h" // for BlockInlineCheck
|
||||
#include "HTMLEditUtils.h" // for HTMLEditUtils
|
||||
#include "JoinSplitNodeDirection.h" // for JoinNodesDirection
|
||||
|
||||
#include "mozilla/Assertions.h" // for MOZ_ASSERT, etc
|
||||
#include "mozilla/IntegerRange.h" // for IntegerRange
|
||||
|
@ -1338,8 +1337,7 @@ void TextServicesDocument::DidDeleteContent(const nsIContent& aChildContent) {
|
|||
}
|
||||
|
||||
void TextServicesDocument::DidJoinContents(
|
||||
const EditorRawDOMPoint& aJoinedPoint, const nsIContent& aRemovedContent,
|
||||
JoinNodesDirection aJoinNodesDirection) {
|
||||
const EditorRawDOMPoint& aJoinedPoint, const nsIContent& aRemovedContent) {
|
||||
// Make sure that both nodes are text nodes -- otherwise we don't care.
|
||||
if (!aJoinedPoint.IsInTextNode() || !aRemovedContent.IsText()) {
|
||||
return;
|
||||
|
@ -1367,30 +1365,19 @@ void TextServicesDocument::DidJoinContents(
|
|||
const size_t removedIndex = *maybeRemovedIndex;
|
||||
const size_t joinedIndex = *maybeJoinedIndex;
|
||||
|
||||
if (aJoinNodesDirection == JoinNodesDirection::LeftNodeIntoRightNode) {
|
||||
if (MOZ_UNLIKELY(removedIndex > joinedIndex)) {
|
||||
NS_ASSERTION(removedIndex < joinedIndex, "Indexes out of order.");
|
||||
return;
|
||||
}
|
||||
NS_ASSERTION(mOffsetTable[joinedIndex]->mOffsetInTextNode == 0,
|
||||
"Unexpected offset value for joinedIndex.");
|
||||
} else {
|
||||
if (MOZ_UNLIKELY(joinedIndex > removedIndex)) {
|
||||
NS_ASSERTION(joinedIndex < removedIndex, "Indexes out of order.");
|
||||
return;
|
||||
}
|
||||
NS_ASSERTION(mOffsetTable[removedIndex]->mOffsetInTextNode == 0,
|
||||
"Unexpected offset value for rightIndex.");
|
||||
if (MOZ_UNLIKELY(joinedIndex > removedIndex)) {
|
||||
NS_ASSERTION(joinedIndex < removedIndex, "Indexes out of order.");
|
||||
return;
|
||||
}
|
||||
NS_ASSERTION(mOffsetTable[removedIndex]->mOffsetInTextNode == 0,
|
||||
"Unexpected offset value for rightIndex.");
|
||||
|
||||
// Run through the table and change all entries referring to
|
||||
// the removed node so that they now refer to the joined node,
|
||||
// and adjust offsets if necessary.
|
||||
const uint32_t movedTextDataLength =
|
||||
aJoinNodesDirection == JoinNodesDirection::LeftNodeIntoRightNode
|
||||
? aJoinedPoint.Offset()
|
||||
: aJoinedPoint.ContainerAs<Text>()->TextDataLength() -
|
||||
aJoinedPoint.Offset();
|
||||
aJoinedPoint.ContainerAs<Text>()->TextDataLength() -
|
||||
aJoinedPoint.Offset();
|
||||
for (uint32_t i = removedIndex; i < mOffsetTable.Length(); i++) {
|
||||
const UniquePtr<OffsetEntry>& entry = mOffsetTable[i];
|
||||
LockOffsetEntryArrayLengthInDebugBuild(observer, mOffsetTable);
|
||||
|
@ -1399,26 +1386,9 @@ void TextServicesDocument::DidJoinContents(
|
|||
}
|
||||
if (entry->mIsValid) {
|
||||
entry->mTextNode = aJoinedPoint.ContainerAs<Text>();
|
||||
if (aJoinNodesDirection == JoinNodesDirection::RightNodeIntoLeftNode) {
|
||||
// The text was moved from aRemovedContent to end of the container of
|
||||
// aJoinedPoint.
|
||||
entry->mOffsetInTextNode += movedTextDataLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (aJoinNodesDirection == JoinNodesDirection::LeftNodeIntoRightNode) {
|
||||
// The text was moved from aRemovedContent to start of the container of
|
||||
// aJoinedPoint.
|
||||
for (uint32_t i = joinedIndex; i < mOffsetTable.Length(); i++) {
|
||||
const UniquePtr<OffsetEntry>& entry = mOffsetTable[i];
|
||||
LockOffsetEntryArrayLengthInDebugBuild(observer, mOffsetTable);
|
||||
if (entry->mTextNode != aJoinedPoint.ContainerAs<Text>()) {
|
||||
break;
|
||||
}
|
||||
if (entry->mIsValid) {
|
||||
entry->mOffsetInTextNode += movedTextDataLength;
|
||||
}
|
||||
// The text was moved from aRemovedContent to end of the container of
|
||||
// aJoinedPoint.
|
||||
entry->mOffsetInTextNode += movedTextDataLength;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2784,16 +2754,12 @@ TextServicesDocument::DidDeleteNode(nsINode* aChild, nsresult aResult) {
|
|||
}
|
||||
|
||||
NS_IMETHODIMP TextServicesDocument::DidJoinContents(
|
||||
const EditorRawDOMPoint& aJoinedPoint, const nsINode* aRemovedNode,
|
||||
bool aLeftNodeWasRemoved) {
|
||||
const EditorRawDOMPoint& aJoinedPoint, const nsINode* aRemovedNode) {
|
||||
if (MOZ_UNLIKELY(NS_WARN_IF(!aJoinedPoint.IsSetAndValid()) ||
|
||||
NS_WARN_IF(!aRemovedNode->IsContent()))) {
|
||||
return NS_OK;
|
||||
}
|
||||
DidJoinContents(aJoinedPoint, *aRemovedNode->AsContent(),
|
||||
aLeftNodeWasRemoved
|
||||
? JoinNodesDirection::LeftNodeIntoRightNode
|
||||
: JoinNodesDirection::RightNodeIntoLeftNode);
|
||||
DidJoinContents(aJoinedPoint, *aRemovedNode->AsContent());
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,6 @@ namespace mozilla {
|
|||
class EditorBase;
|
||||
class FilteredContentIterator;
|
||||
class OffsetEntry;
|
||||
enum class JoinNodesDirection; // Declared in HTMLEditHelpers.h
|
||||
|
||||
namespace dom {
|
||||
class AbstractRange;
|
||||
|
@ -374,8 +373,7 @@ class TextServicesDocument final : public nsIEditActionListener {
|
|||
*/
|
||||
void DidDeleteContent(const nsIContent& aChildContent);
|
||||
void DidJoinContents(const EditorRawDOMPoint& aJoinedPoint,
|
||||
const nsIContent& aRemovedContent,
|
||||
JoinNodesDirection aJoinNodesDirection);
|
||||
const nsIContent& aRemovedContent);
|
||||
|
||||
private:
|
||||
// TODO: We should get rid of this method since `aAbstractRange` has
|
||||
|
|
Загрузка…
Ссылка в новой задаче