174 lines
5.5 KiB
JavaScript
174 lines
5.5 KiB
JavaScript
import computed from 'ember-addons/ember-computed-decorators';
|
|
import { selectedText } from 'discourse/lib/utilities';
|
|
|
|
// we don't want to deselect when we click on buttons that use it
|
|
function ignoreElements(e) {
|
|
const $target = $(e.target);
|
|
return $target.hasClass('quote-button') ||
|
|
$target.closest('.create').length ||
|
|
$target.closest('.reply-new').length ||
|
|
$target.closest('.share').length;
|
|
}
|
|
|
|
export default Ember.Component.extend({
|
|
classNames: ['quote-button'],
|
|
classNameBindings: ['visible'],
|
|
isMouseDown: false,
|
|
_isTouchInProgress: false,
|
|
|
|
@computed('quoteState.buffer')
|
|
visible: buffer => buffer && buffer.length > 0,
|
|
|
|
/**
|
|
Binds to the following global events:
|
|
- `mousedown` to clear the quote button if they click elsewhere.
|
|
- `mouseup` to trigger the display of the quote button.
|
|
- `selectionchange` to make the selection work under iOS
|
|
|
|
@method didInsertElement
|
|
**/
|
|
didInsertElement() {
|
|
let onSelectionChanged = () => this._selectText(window.getSelection().anchorNode);
|
|
|
|
// Windows Phone hack, it is not firing the touch events
|
|
// best we can do is debounce this so we dont keep locking up
|
|
// the selection when we add the caret to measure where we place
|
|
// the quote reply widget
|
|
//
|
|
// Same hack applied to Android cause it has unreliable touchend
|
|
const isAndroid = this.capabilities.isAndroid;
|
|
if (this.capabilities.isWinphone || isAndroid) {
|
|
onSelectionChanged = _.debounce(onSelectionChanged, 500);
|
|
}
|
|
|
|
$(document).on("mousedown.quote-button", e => {
|
|
this.set('isMouseDown', true);
|
|
|
|
if (ignoreElements(e)) { return; }
|
|
|
|
// deselects only when the user left click
|
|
// (allows anyone to `extend` their selection using shift+click)
|
|
if (!window.getSelection().isCollapsed &&
|
|
e.which === 1 &&
|
|
!e.shiftKey) {
|
|
this.sendAction('deselectText');
|
|
}
|
|
}).on('mouseup.quote-button', e => {
|
|
if (ignoreElements(e)) { return; }
|
|
|
|
this._selectText(e.target);
|
|
this.set('isMouseDown', false);
|
|
}).on('selectionchange', () => {
|
|
// there is no need to handle this event when the mouse is down
|
|
// or if there a touch in progress
|
|
if (this.get('isMouseDown') || this._isTouchInProgress) { return; }
|
|
// `selection.anchorNode` is used as a target
|
|
onSelectionChanged();
|
|
});
|
|
|
|
// Android is dodgy, touchend often will not fire
|
|
// https://code.google.com/p/android/issues/detail?id=19827
|
|
if (!this.isAndroid) {
|
|
$(document).on('touchstart.quote-button', () => {
|
|
this._isTouchInProgress = true;
|
|
return true;
|
|
});
|
|
|
|
$(document).on('touchend.quote-button', () => {
|
|
this._isTouchInProgress = false;
|
|
return true;
|
|
});
|
|
}
|
|
},
|
|
|
|
_selectText(target) {
|
|
// anonymous users cannot "quote-reply"
|
|
if (!this.currentUser) return;
|
|
|
|
const quoteState = this.get('quoteState');
|
|
|
|
const $target = $(target);
|
|
const postId = $target.closest('.boxed, .reply').data('post-id');
|
|
|
|
const details = this.get('topic.details');
|
|
if (!(details.get('can_reply_as_new_topic') || details.get('can_create_post'))) {
|
|
return;
|
|
}
|
|
|
|
const selection = window.getSelection();
|
|
if (selection.isCollapsed) {
|
|
return;
|
|
}
|
|
|
|
const range = selection.getRangeAt(0),
|
|
cloned = range.cloneRange(),
|
|
$ancestor = $(range.commonAncestorContainer);
|
|
|
|
if ($ancestor.closest('.cooked').length === 0) {
|
|
return this.sendAction('deselectText');
|
|
}
|
|
|
|
const selVal = selectedText();
|
|
if (quoteState.get('buffer') === selVal) { return; }
|
|
quoteState.setProperties({ postId, buffer: selVal });
|
|
|
|
// create a marker element containing a single invisible character
|
|
const markerElement = document.createElement("span");
|
|
markerElement.appendChild(document.createTextNode("\ufeff"));
|
|
|
|
const isMobileDevice = this.site.isMobileDevice;
|
|
const capabilities = this.capabilities;
|
|
const isIOS = capabilities.isIOS;
|
|
const isAndroid = capabilities.isAndroid;
|
|
|
|
// collapse the range at the beginning/end of the selection
|
|
// and insert it at the start of our selection range
|
|
range.collapse(!isMobileDevice);
|
|
range.insertNode(markerElement);
|
|
|
|
// retrieve the position of the marker
|
|
const $markerElement = $(markerElement);
|
|
const markerOffset = $markerElement.offset();
|
|
const parentScrollLeft = $markerElement.parent().scrollLeft();
|
|
const $quoteButton = this.$();
|
|
|
|
// remove the marker
|
|
markerElement.parentNode.removeChild(markerElement);
|
|
|
|
// work around Chrome that would sometimes lose the selection
|
|
const sel = window.getSelection();
|
|
sel.removeAllRanges();
|
|
sel.addRange(cloned);
|
|
|
|
Ember.run.scheduleOnce('afterRender', function() {
|
|
let topOff = markerOffset.top;
|
|
let leftOff = markerOffset.left;
|
|
|
|
if (parentScrollLeft > 0) leftOff += parentScrollLeft;
|
|
|
|
if (isMobileDevice || isIOS || isAndroid) {
|
|
topOff = topOff + 20;
|
|
leftOff = Math.min(leftOff + 10, $(window).width() - $quoteButton.outerWidth());
|
|
} else {
|
|
topOff = topOff - $quoteButton.outerHeight() - 5;
|
|
}
|
|
|
|
$quoteButton.offset({ top: topOff, left: leftOff });
|
|
});
|
|
},
|
|
|
|
willDestroyElement() {
|
|
$(document)
|
|
.off("mousedown.quote-button")
|
|
.off("mouseup.quote-button")
|
|
.off("touchstart.quote-button")
|
|
.off("touchend.quote-button")
|
|
.off("selectionchange");
|
|
},
|
|
|
|
click(e) {
|
|
e.stopPropagation();
|
|
this.sendAction('selectText');
|
|
}
|
|
});
|