This repository has been archived on 2023-03-18. You can view files and clone it, but cannot push or open issues or pull requests.
osr-discourse-src/app/assets/javascripts/discourse/app/components/quote-button.js
Roman Rizzi 142e0ae062
Revert "Revert "DEV: Wrap Ember.run.debounce. (#11352)"" (#11509)
* Revert "Revert "DEV: Wrap `Ember.run.debounce`. (#11352)" (#11465)"

This reverts commit aa0d4ea764.

* Correctly debounce onScroll function
2020-12-18 10:18:52 -03:00

279 lines
7.9 KiB
JavaScript

import {
postUrl,
selectedElement,
selectedText,
} from "discourse/lib/utilities";
import Component from "@ember/component";
import { INPUT_DELAY } from "discourse-common/config/environment";
import Sharing from "discourse/lib/sharing";
import { action } from "@ember/object";
import { alias } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";
import discourseDebounce from "discourse-common/lib/debounce";
import { getAbsoluteURL } from "discourse-common/lib/get-url";
import { schedule } from "@ember/runloop";
import toMarkdown from "discourse/lib/to-markdown";
function getQuoteTitle(element) {
const titleEl = element.querySelector(".title");
if (!titleEl) {
return;
}
const titleLink = titleEl.querySelector("a:not(.back)");
if (titleLink) {
return titleLink.textContent.trim();
}
return titleEl.textContent.trim().replace(/:$/, "");
}
export default Component.extend({
classNames: ["quote-button"],
classNameBindings: ["visible"],
visible: false,
privateCategory: alias("topic.category.read_restricted"),
_isMouseDown: false,
_reselected: false,
_hideButton() {
this.quoteState.clear();
this.set("visible", false);
},
_selectionChanged() {
const quoteState = this.quoteState;
const selection = window.getSelection();
if (selection.isCollapsed) {
if (this.visible) {
this._hideButton();
}
return;
}
// ensure we selected content inside 1 post *only*
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];
const postBody = toMarkdown(cooked.innerHTML);
let opts = {
full: _selectedText === postBody,
};
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;
}
}
quoteState.selected(postId, _selectedText, opts);
this.set("visible", quoteState.buffer.length > 0);
// avoid hard loops in quote selection unconditionally
// this can happen if you triple click text in firefox
if (this._prevSelection === _selectedText) {
return;
}
this._prevSelection = _selectedText;
// on Desktop, shows the button at the beginning of the selection
// on Mobile, shows the button at the end of the selection
const isMobileDevice = this.site.isMobileDevice;
const { isIOS, isAndroid, isSafari, isOpera } = this.capabilities;
const showAtEnd = isMobileDevice || isIOS || isAndroid || isOpera;
// Don't mess with the original range as it results in weird behaviours
// where certain browsers will deselect the selection
const clone = firstRange.cloneRange();
// create a marker element containing a single invisible character
const markerElement = document.createElement("span");
markerElement.appendChild(document.createTextNode("\ufeff"));
// on mobile, collapse the range at the end of the selection
if (showAtEnd) {
clone.collapse();
}
// insert the marker
clone.insertNode(markerElement);
// retrieve the position of the marker
const $markerElement = $(markerElement);
const markerOffset = $markerElement.offset();
const parentScrollLeft = $markerElement.parent().scrollLeft();
const $quoteButton = $(this.element);
// remove the marker
const parent = markerElement.parentNode;
parent.removeChild(markerElement);
// merge back all text nodes so they don't get messed up
parent.normalize();
// work around Safari that would sometimes lose the selection
if (isSafari) {
this._reselected = true;
selection.removeAllRanges();
selection.addRange(clone);
}
// change the position of the button
schedule("afterRender", () => {
if (!this.element || this.isDestroying || this.isDestroyed) {
return;
}
let top = markerOffset.top;
let left = markerOffset.left + Math.max(0, parentScrollLeft);
if (showAtEnd) {
const nearRightEdgeOfScreen =
$(window).width() - $quoteButton.outerWidth() < left + 10;
top = nearRightEdgeOfScreen ? top + 50 : top + 20;
left = Math.min(
left + 10,
$(window).width() - $quoteButton.outerWidth() - 10
);
} else {
top = top - $quoteButton.outerHeight() - 5;
}
$quoteButton.offset({ top, left });
});
},
didInsertElement() {
this._super(...arguments);
const { isWinphone, isAndroid } = this.capabilities;
const wait = isWinphone || isAndroid ? INPUT_DELAY : 25;
const onSelectionChanged = () => {
discourseDebounce(this, this._selectionChanged, wait);
};
$(document)
.on("mousedown.quote-button", (e) => {
this._prevSelection = null;
this._isMouseDown = true;
this._reselected = false;
if (
$(e.target).closest(".quote-button, .create, .share, .reply-new")
.length === 0
) {
this._hideButton();
}
})
.on("mouseup.quote-button", () => {
this._prevSelection = null;
this._isMouseDown = false;
onSelectionChanged();
})
.on("selectionchange.quote-button", () => {
if (!this._isMouseDown && !this._reselected) {
onSelectionChanged();
}
});
},
willDestroyElement() {
$(document)
.off("mousedown.quote-button")
.off("mouseup.quote-button")
.off("selectionchange.quote-button");
},
@discourseComputed("topic.{isPrivateMessage,invisible,category}")
quoteSharingEnabled(topic) {
if (
this.site.mobileView ||
this.siteSettings.share_quote_visibility === "none" ||
(this.currentUser &&
this.siteSettings.share_quote_visibility === "anonymous") ||
this.quoteSharingSources.length === 0 ||
this.privateCategory ||
(this.currentUser && topic.invisible)
) {
return false;
}
return true;
},
@discourseComputed("topic.isPrivateMessage")
quoteSharingSources(isPM) {
return Sharing.activeSources(
this.siteSettings.share_quote_buttons,
this.siteSettings.login_required || isPM
);
},
@discourseComputed("topic.{isPrivateMessage,invisible,category}")
quoteSharingShowLabel() {
return this.quoteSharingSources.length > 1;
},
@discourseComputed("topic.{id,slug}", "quoteState")
shareUrl(topic, quoteState) {
return getAbsoluteURL(postUrl(topic.slug, topic.id, quoteState.postId));
},
@discourseComputed("topic.details.can_create_post", "composerVisible")
embedQuoteButton(canCreatePost, composerOpened) {
return (
(canCreatePost || composerOpened) &&
this.currentUser &&
this.currentUser.get("enable_quoting")
);
},
@action
insertQuote() {
this.attrs.selectText().then(() => this._hideButton());
},
@action
share(source) {
Sharing.shareSource(source, {
url: this.shareUrl,
title: this.topic.title,
quote: window.getSelection().toString(),
});
},
});