Я пытаюсь сделать анонимные посты ещё более анонимными, переопределив способ отображения цитат в категориях, где включена только анонимная публикация. Таким образом, вместо стандартного
anonymous4302934
какой-то цитируемый текст
я хочу получить
Аноним:
какой-то цитируемый текст
Мне удалось реализовать это для текста, который цитируется через кнопку на панели инструментов, основываясь на How to override the buildQuote function? - #7 by Canapin (мне пришлось добавить поиск контейнера в конце, чтобы это работало, следуя обходному пути здесь: api.modifyClass sometimes(!) not working - #12 by RGJ).
Я просмотрел основные файлы, но не могу точно определить, где формируется цитата, когда она вставляется в композитор через выделение и нажатие кнопки цитирования. В _selectionChanged() в quote-button.js я вижу, что opts.username устанавливается, если выделенный текст находится в блоке цитирования, но если я пытаюсь изменить это, вручную устанавливая opts.username, это ни на что не влияет.
Правильно ли я пытаюсь это переопределить? Является ли modifyClass правильным подходом?
<script type="text/discourse-plugin" version="0.8">
const controller = api.container.lookup('controller:topic');
const anon_categories = [5, 6, 14, 15, 16, 17, 18]
function buildQuote(post, contents, opts = {}) {
if (!post || !contents) {
return "";
}
let anon_name = ``;
// console.log(anon_categories, controller.get("model.category_id"))
if (anon_categories.includes(controller.get("model.category_id"))) {
anon_name = `Аноним`;
} else {
anon_name = opts.username || post.username;
}
const params = [
anon_name,
`post:${opts.post || post.post_number}`,
`topic:${opts.topic || post.topic_id}`
];
if (opts.full) params.push("full:true");
return `\n[quote="${params.join(", ")}"]\n${contents.trim()}\n[/quote]\n\n`;
}
api.modifyClass('controller:composer', {
pluginId: 'anonymize-quotes',
actions: {
importQuote(toolbarEvent) {
const postStream = this.get("topic.postStream");
let postId = this.get("model.post.id");
// Если текущий пост отсутствует, используем ID первого поста из потока
if (!postId && postStream) {
postId = postStream.get("stream.firstObject");
}
// Если мы редактируем пост, при импорте цитаты получаем ответ
if (this.get("model.editingPost")) {
const replyToPostNumber = this.get("model.post.reply_to_post_number");
if (replyToPostNumber) {
const replyPost = postStream.posts.findBy(
"post_number",
replyToPostNumber
);
if (replyPost) {
postId = replyPost.id;
}
}
}
if (postId) {
this.set("model.loading", true);
return this.store.find("post", postId).then(post => {
const quote = buildQuote(post, post.raw, {
full: true
});
toolbarEvent.addText(quote);
this.set("model.loading", false);
});
}
}
}
});
api.modifyClass('component:quote-button', {
pluginId: 'anonymize-quotes',
actions: {
_selectionChanged() {
if (this._displayFastEditInput) {
return;
}
const quoteState = this.quoteState;
const selection = window.getSelection();
if (selection.isCollapsed) {
if (this.visible) {
this._hideButton();
}
return;
}
// гарантируем, что выделен контент только из одного поста
let firstRange, postId;
for (let r = 0; r < selection.rangeCount; r++) {
const range = selection.getRangeAt(r);
const $selectionStart = $(range.startContainer);
const $ancestor = $(range.commonAncestorContainer);
if ($selectionStart.closest(".cooked").length === 0) {
return;
}
firstRange = firstRange || range;
postId = postId || $ancestor.closest(".boxed, .reply").data("post-id");
if ($ancestor.closest(".contents").length === 0 || !postId) {
if (this.visible) {
this._hideButton();
}
return;
}
}
const _selectedElement = selectedElement();
const _selectedText = selectedText();
const $selectedElement = $(_selectedElement);
const cooked =
$selectedElement.find(".cooked")[0] ||
$selectedElement.closest(".cooked")[0];
// вычисление markdown занимает много времени для длинных постов
// этот код пытается вычислить его только тогда, когда нельзя использовать быстрый путь
let opts = {
full:
selectedRange().startOffset > 0
? false
: _selectedText === toMarkdown(cooked.innerHTML),
};
for (
let element = _selectedElement;
element && element.tagName !== "ARTICLE";
element = element.parentElement
) {
if (element.tagName === "ASIDE" && element.classList.contains("quote")) {
opts.username = element.dataset.username || getQuoteTitle(element);
opts.post = element.dataset.post;
opts.topic = element.dataset.topic;
break;
}
}
opts.username = `Аноним`
quoteState.selected(postId, _selectedText, opts);
this.set("visible", quoteState.buffer.length > 0);
if (this.siteSettings.enable_fast_edit) {
this.set(
"_canEditPost",
this.topic.postStream.findLoadedPost(postId)?.can_edit
);
if (this._canEditPost) {
const regexp = new RegExp(regexSafeStr(quoteState.buffer), "gi");
const matches = cooked.innerHTML.match(regexp);
if (
quoteState.buffer.length < 1 ||
quoteState.buffer.includes("|") || // таблицы слишком сложны
quoteState.buffer.match(/\n/g) || // переносы строк слишком сложны
matches?.length > 1 // дубликаты слишком сложны
) {
this.set("_isFastEditable", false);
this.set("_fastEditInitalSelection", null);
this.set("_fastEditNewSelection", null);
} else if (matches?.length === 1) {
this.set("_isFastEditable", true);
this.set("_fastEditInitalSelection", quoteState.buffer);
this.set("_fastEditNewSelection", quoteState.buffer);
}
}
}
// избегаем бесконечных циклов при выборе цитаты без условий
// это может произойти, если трижды кликнуть по тексту в Firefox
if (this._prevSelection === _selectedText) {
return;
}
this._prevSelection = _selectedText;
// на десктопе показывает кнопку в начале выделения
// на мобильных устройствах показывает кнопку в конце выделения
const isMobileDevice = this.site.isMobileDevice;
const { isIOS, isAndroid, isOpera } = this.capabilities;
const showAtEnd = isMobileDevice || isIOS || isAndroid || isOpera;
const boundaryPosition = this._getRangeBoundaryRect(firstRange, showAtEnd);
// изменяем положение кнопки
schedule("afterRender", () => {
if (!this.element || this.isDestroying || this.isDestroyed) {
return;
}
let top = 0;
let left = 0;
const pxFromSelection = 5;
if (showAtEnd) {
// Ручки выделения на iOS имеют область нажатия радиусом ~50px,
// поэтому нам нужно убедиться, что наши кнопки находятся за пределами этого радиуса
// Применяем ту же логику ко всем мобильным устройствам для согласованности
top = boundaryPosition.bottom + pxFromSelection;
left = boundaryPosition.left;
const safeRadius = 50;
const topicArea = document
.querySelector(".topic-area")
.getBoundingClientRect();
topicArea.x += document.documentElement.scrollLeft;
topicArea.y += document.documentElement.scrollTop;
const endHandlePosition = boundaryPosition;
const width = this.element.clientWidth;
const possiblePositions = [
{
// переместить влево
top,
left: left - width - safeRadius,
},
{
// переместить вправо
top,
left: left + safeRadius,
},
{
// по центру ниже конца ручки
top: top + safeRadius,
left: left - width / 2,
},
];
for (const pos of possiblePositions) {
// Убедитесь, что кнопки полностью находятся внутри .topic-area
pos.left = Math.max(topicArea.left, pos.left);
pos.left = Math.min(topicArea.right - width, pos.left);
let clearOfStartHandle = true;
if (isAndroid) {
// На Android ручка начала выделения выступает ниже строки, поэтому её тоже нужно избегать:
const startHandlePosition = this._getRangeBoundaryRect(
firstRange,
false
);
clearOfStartHandle =
pos.top - startHandlePosition.bottom >= safeRadius ||
pos.left + width <= startHandlePosition.left - safeRadius ||
pos.left >= startHandlePosition.left + safeRadius;
}
const clearOfEndHandle =
pos.top - endHandlePosition.top >= safeRadius ||
pos.left + width <= endHandlePosition.left - safeRadius ||
pos.left >= endHandlePosition.left + safeRadius;
if (clearOfStartHandle && clearOfEndHandle) {
left = pos.left;
top = pos.top;
break;
}
}
} else {
// Десктоп
top =
boundaryPosition.top - this.element.clientHeight - pxFromSelection;
left = boundaryPosition.left;
}
Object.assign(this.element.style, { top: `${top}px`, left: `${left}px` });
if (!this.animated) {
// Включаем CSS-переходы только после начального позиционирования,
// иначе кнопка может появиться, будто летит с экрана
next(() => this.set("animated", true));
}
});
}
}
});
const composerController = api.container.lookup("controller:composer");
const componentQuoteButton = api.container.lookup("component:quote-button");
</script>