mirror of
https://github.com/discourse/discourse.git
synced 2025-02-17 08:45:05 +00:00
Replace quote button with a component
This commit is contained in:
parent
93403b0af6
commit
cc93cd76ef
173
app/assets/javascripts/discourse/components/quote-button.js.es6
Normal file
173
app/assets/javascripts/discourse/components/quote-button.js.es6
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
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');
|
||||||
|
}
|
||||||
|
});
|
@ -1,160 +0,0 @@
|
|||||||
import Quote from 'discourse/lib/quote';
|
|
||||||
import computed from 'ember-addons/ember-computed-decorators';
|
|
||||||
import { selectedText } from 'discourse/lib/utilities';
|
|
||||||
|
|
||||||
export default Ember.Controller.extend({
|
|
||||||
topic: Ember.inject.controller(),
|
|
||||||
composer: Ember.inject.controller(),
|
|
||||||
|
|
||||||
@computed('buffer', 'postId')
|
|
||||||
post(buffer, postId) {
|
|
||||||
if (!postId || Ember.isEmpty(buffer)) { return null; }
|
|
||||||
|
|
||||||
const postStream = this.get('topic.model.postStream');
|
|
||||||
const post = postStream.findLoadedPost(postId);
|
|
||||||
|
|
||||||
return post;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Save the currently selected text and displays the
|
|
||||||
// "quote reply" button
|
|
||||||
selectText(postId) {
|
|
||||||
// anonymous users cannot "quote-reply"
|
|
||||||
if (!this.currentUser) return;
|
|
||||||
|
|
||||||
// don't display the "quote-reply" button if we can't reply
|
|
||||||
const topicDetails = this.get('topic.model.details');
|
|
||||||
if (!(topicDetails.get('can_reply_as_new_topic') || topicDetails.get('can_create_post'))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selection = window.getSelection();
|
|
||||||
|
|
||||||
// no selections
|
|
||||||
if (selection.isCollapsed) {
|
|
||||||
this.set('buffer', '');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// retrieve the selected range
|
|
||||||
const range = selection.getRangeAt(0),
|
|
||||||
cloned = range.cloneRange(),
|
|
||||||
$ancestor = $(range.commonAncestorContainer);
|
|
||||||
|
|
||||||
if ($ancestor.closest('.cooked').length === 0) {
|
|
||||||
this.set('buffer', '');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selVal = selectedText();
|
|
||||||
if (this.get('buffer') === selVal) return;
|
|
||||||
|
|
||||||
// we need to retrieve the post data from the posts collection in the topic controller
|
|
||||||
this.set('postId', postId);
|
|
||||||
this.set('buffer', selVal);
|
|
||||||
|
|
||||||
// create a marker element
|
|
||||||
const markerElement = document.createElement("span");
|
|
||||||
// containing a single invisible character
|
|
||||||
markerElement.appendChild(document.createTextNode("\ufeff"));
|
|
||||||
|
|
||||||
const isMobileDevice = this.site.isMobileDevice;
|
|
||||||
const capabilities = this.capabilities,
|
|
||||||
isIOS = capabilities.isIOS,
|
|
||||||
isAndroid = capabilities.isAndroid;
|
|
||||||
|
|
||||||
// collapse the range at the beginning/end of the selection
|
|
||||||
range.collapse(!isMobileDevice);
|
|
||||||
// and insert it at the start of our selection range
|
|
||||||
range.insertNode(markerElement);
|
|
||||||
|
|
||||||
// retrieve the position of the marker
|
|
||||||
const $markerElement = $(markerElement),
|
|
||||||
markerOffset = $markerElement.offset(),
|
|
||||||
parentScrollLeft = $markerElement.parent().scrollLeft(),
|
|
||||||
$quoteButton = $('.quote-button');
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// move the quote button above the marker
|
|
||||||
Em.run.schedule('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 });
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
quoteText() {
|
|
||||||
const Composer = require('discourse/models/composer').default;
|
|
||||||
const postId = this.get('postId');
|
|
||||||
const post = this.get('post');
|
|
||||||
|
|
||||||
// defer load if needed, if in an expanded replies section
|
|
||||||
if (!post) {
|
|
||||||
const postStream = this.get('topic.model.postStream');
|
|
||||||
return postStream.loadPost(postId).then(p => {
|
|
||||||
this.set('post', p);
|
|
||||||
return this.quoteText();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we can't create a post, delegate to reply as new topic
|
|
||||||
if (!this.get('topic.model.details.can_create_post')) {
|
|
||||||
this.get('topic').send('replyAsNewTopic', post);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const composerController = this.get('composer');
|
|
||||||
const composerOpts = {
|
|
||||||
action: Composer.REPLY,
|
|
||||||
draftKey: post.get('topic.draft_key')
|
|
||||||
};
|
|
||||||
|
|
||||||
if (post.get('post_number') === 1) {
|
|
||||||
composerOpts.topic = post.get("topic");
|
|
||||||
} else {
|
|
||||||
composerOpts.post = post;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the composer is associated with a different post, we don't change it.
|
|
||||||
const composerPost = composerController.get('content.post');
|
|
||||||
if (composerPost && (composerPost.get('id') !== this.get('post.id'))) {
|
|
||||||
composerOpts.post = composerPost;
|
|
||||||
}
|
|
||||||
|
|
||||||
const buffer = this.get('buffer');
|
|
||||||
const quotedText = Quote.build(post, buffer);
|
|
||||||
composerOpts.quote = quotedText;
|
|
||||||
if (composerController.get('content.viewOpen') || composerController.get('content.viewDraft')) {
|
|
||||||
this.appEvents.trigger('composer:insert-text', quotedText);
|
|
||||||
} else {
|
|
||||||
composerController.open(composerOpts);
|
|
||||||
}
|
|
||||||
this.set('buffer', '');
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
deselectText() {
|
|
||||||
// clear selected text
|
|
||||||
window.getSelection().removeAllRanges();
|
|
||||||
// clean up the buffer
|
|
||||||
this.set('buffer', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
@ -14,9 +14,7 @@ import isElementInViewport from "discourse/lib/is-element-in-viewport";
|
|||||||
|
|
||||||
export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||||
composer: Ember.inject.controller(),
|
composer: Ember.inject.controller(),
|
||||||
quoteButton: Ember.inject.controller('quote-button'),
|
|
||||||
application: Ember.inject.controller(),
|
application: Ember.inject.controller(),
|
||||||
|
|
||||||
multiSelect: false,
|
multiSelect: false,
|
||||||
allPostsSelected: false,
|
allPostsSelected: false,
|
||||||
editingTopic: false,
|
editingTopic: false,
|
||||||
@ -30,9 +28,9 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
|||||||
userTriggeredProgress: null,
|
userTriggeredProgress: null,
|
||||||
_progressIndex: null,
|
_progressIndex: null,
|
||||||
hasScrolled: null,
|
hasScrolled: null,
|
||||||
|
|
||||||
username_filters: null,
|
username_filters: null,
|
||||||
filter: null,
|
filter: null,
|
||||||
|
quoteState: null,
|
||||||
|
|
||||||
topicDelegated: [
|
topicDelegated: [
|
||||||
'toggleMultiSelect',
|
'toggleMultiSelect',
|
||||||
@ -141,10 +139,12 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
|||||||
I18n.t("suggested_topics.title");
|
I18n.t("suggested_topics.title");
|
||||||
},
|
},
|
||||||
|
|
||||||
_clearSelected: function() {
|
init() {
|
||||||
|
this._super();
|
||||||
this.set('selectedPosts', []);
|
this.set('selectedPosts', []);
|
||||||
this.set('selectedReplies', []);
|
this.set('selectedReplies', []);
|
||||||
}.on('init'),
|
this.set('quoteState', Ember.Object.create({ buffer: null, postId: null }));
|
||||||
|
},
|
||||||
|
|
||||||
showCategoryChooser: Ember.computed.not("model.isPrivateMessage"),
|
showCategoryChooser: Ember.computed.not("model.isPrivateMessage"),
|
||||||
|
|
||||||
@ -167,6 +167,52 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
|||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
|
||||||
|
deselectText() {
|
||||||
|
const quoteState = this.get('quoteState');
|
||||||
|
quoteState.setProperties({ buffer: null, postId: null });
|
||||||
|
},
|
||||||
|
|
||||||
|
selectText() {
|
||||||
|
const quoteState = this.get('quoteState');
|
||||||
|
const postStream = this.get('model.postStream');
|
||||||
|
|
||||||
|
const postId = quoteState.get('postId');
|
||||||
|
postStream.loadPost(postId).then(post => {
|
||||||
|
// If we can't create a post, delegate to reply as new topic
|
||||||
|
if (!this.get('model.details.can_create_post')) {
|
||||||
|
this.send('replyAsNewTopic', post);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const composer = this.get('composer');
|
||||||
|
const composerOpts = {
|
||||||
|
action: Composer.REPLY,
|
||||||
|
draftKey: post.get('topic.draft_key')
|
||||||
|
};
|
||||||
|
|
||||||
|
if (post.get('post_number') === 1) {
|
||||||
|
composerOpts.topic = post.get("topic");
|
||||||
|
} else {
|
||||||
|
composerOpts.post = post;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the composer is associated with a different post, we don't change it.
|
||||||
|
const composerPost = composer.get('content.post');
|
||||||
|
if (composerPost && (composerPost.get('id') !== this.get('post.id'))) {
|
||||||
|
composerOpts.post = composerPost;
|
||||||
|
}
|
||||||
|
|
||||||
|
const quotedText = Quote.build(post, quoteState.get('buffer'));
|
||||||
|
composerOpts.quote = quotedText;
|
||||||
|
if (composer.get('content.viewOpen') || composer.get('content.viewDraft')) {
|
||||||
|
this.appEvents.trigger('composer:insert-text', quotedText);
|
||||||
|
} else {
|
||||||
|
composer.open(composerOpts);
|
||||||
|
}
|
||||||
|
this.send('deselectText');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
fillGapBefore(args) {
|
fillGapBefore(args) {
|
||||||
return this.get('model.postStream').fillGapBefore(args.post, args.gap);
|
return this.get('model.postStream').fillGapBefore(args.post, args.gap);
|
||||||
},
|
},
|
||||||
@ -265,12 +311,14 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
|||||||
|
|
||||||
// Post related methods
|
// Post related methods
|
||||||
replyToPost(post) {
|
replyToPost(post) {
|
||||||
const composerController = this.get('composer'),
|
const composerController = this.get('composer');
|
||||||
quoteController = this.get('quoteButton'),
|
const topic = post ? post.get('topic') : this.get('model');
|
||||||
quotedText = Quote.build(quoteController.get('post'), quoteController.get('buffer')),
|
|
||||||
topic = post ? post.get('topic') : this.get('model');
|
|
||||||
|
|
||||||
quoteController.set('buffer', '');
|
const quoteState = this.get('quoteState');
|
||||||
|
const postStream = this.get('model.postStream');
|
||||||
|
const quotedPost = postStream.findLoadedPost(quoteState.get('postId'));
|
||||||
|
const quotedText = Quote.build(quotedPost, quoteState.get('buffer'));
|
||||||
|
this.send('deselectText');
|
||||||
|
|
||||||
if (composerController.get('content.topic.id') === topic.get('id') &&
|
if (composerController.get('content.topic.id') === topic.get('id') &&
|
||||||
composerController.get('content.action') === Composer.REPLY) {
|
composerController.get('content.action') === Composer.REPLY) {
|
||||||
@ -597,11 +645,11 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
|||||||
|
|
||||||
replyAsNewTopic(post) {
|
replyAsNewTopic(post) {
|
||||||
const composerController = this.get('composer');
|
const composerController = this.get('composer');
|
||||||
const quoteController = this.get('quoteButton');
|
|
||||||
post = post || quoteController.get('post');
|
|
||||||
const quotedText = Quote.build(post, quoteController.get('buffer'));
|
|
||||||
|
|
||||||
quoteController.deselectText();
|
const quoteState = this.get('quoteState');
|
||||||
|
post = post || quoteState.get('post');
|
||||||
|
const quotedText = Quote.build(post, quoteState.get('buffer'));
|
||||||
|
this.send('deselectText');
|
||||||
|
|
||||||
composerController.open({
|
composerController.open({
|
||||||
action: Composer.CREATE_TOPIC,
|
action: Composer.CREATE_TOPIC,
|
||||||
@ -633,7 +681,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
|||||||
togglePostType(post) {
|
togglePostType(post) {
|
||||||
const regular = this.site.get('post_types.regular');
|
const regular = this.site.get('post_types.regular');
|
||||||
const moderator = this.site.get('post_types.moderator_action');
|
const moderator = this.site.get('post_types.moderator_action');
|
||||||
|
|
||||||
return post.updatePostField('post_type', post.get('post_type') === moderator ? regular : moderator);
|
return post.updatePostField('post_type', post.get('post_type') === moderator ? regular : moderator);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -922,7 +969,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
_showFooter: function() {
|
_showFooter: function() {
|
||||||
const showFooter = this.get("model.postStream.loaded") && this.get("model.postStream.loadedAllPosts");
|
const showFooter = this.get("model.postStream.loaded") && this.get("model.postStream.loadedAllPosts");
|
||||||
this.set("application.showFooter", showFooter);
|
this.set("application.showFooter", showFooter);
|
||||||
|
@ -117,7 +117,7 @@ const TopicRoute = Discourse.Route.extend({
|
|||||||
|
|
||||||
willTransition() {
|
willTransition() {
|
||||||
this._super();
|
this._super();
|
||||||
this.controllerFor("quote-button").deselectText();
|
this.controllerFor("topic").send('deselectText');
|
||||||
Em.run.cancel(scheduledReplace);
|
Em.run.cancel(scheduledReplace);
|
||||||
isTransitioning = true;
|
isTransitioning = true;
|
||||||
return true;
|
return true;
|
||||||
|
@ -224,6 +224,9 @@
|
|||||||
{{share-popup topic=model replyAsNewTopic="replyAsNewTopic"}}
|
{{share-popup topic=model replyAsNewTopic="replyAsNewTopic"}}
|
||||||
|
|
||||||
{{#if currentUser.enable_quoting}}
|
{{#if currentUser.enable_quoting}}
|
||||||
{{render "quote-button"}}
|
{{quote-button topic=model
|
||||||
|
quoteState=quoteState
|
||||||
|
selectText="selectText"
|
||||||
|
deselectText="deselectText"}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
@ -1,106 +0,0 @@
|
|||||||
// 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.View.extend({
|
|
||||||
classNames: ['quote-button'],
|
|
||||||
classNameBindings: ['visible'],
|
|
||||||
isMouseDown: false,
|
|
||||||
_isTouchInProgress: false,
|
|
||||||
|
|
||||||
// The button is visible whenever there is something in the buffer
|
|
||||||
// (ie. something has been selected)
|
|
||||||
visible: Em.computed.notEmpty('controller.buffer'),
|
|
||||||
|
|
||||||
/**
|
|
||||||
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() {
|
|
||||||
const controller = this.get('controller');
|
|
||||||
|
|
||||||
let onSelectionChanged = () => this.selectText(window.getSelection().anchorNode, controller);
|
|
||||||
|
|
||||||
// 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) controller.deselectText();
|
|
||||||
}).on('mouseup.quote-button', e => {
|
|
||||||
if (ignoreElements(e)) { return; }
|
|
||||||
|
|
||||||
this.selectText(e.target, controller);
|
|
||||||
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, controller) {
|
|
||||||
const $target = $(target);
|
|
||||||
// breaks if quoting has been disabled by the user
|
|
||||||
if (!Discourse.User.currentProp('enable_quoting')) return;
|
|
||||||
// retrieve the post id from the DOM
|
|
||||||
const postId = $target.closest('.boxed, .reply').data('post-id');
|
|
||||||
// select the text
|
|
||||||
if (postId) controller.selectText(postId);
|
|
||||||
},
|
|
||||||
|
|
||||||
willDestroyElement() {
|
|
||||||
$(document)
|
|
||||||
.off("mousedown.quote-button")
|
|
||||||
.off("mouseup.quote-button")
|
|
||||||
.off("touchstart.quote-button")
|
|
||||||
.off("touchend.quote-button")
|
|
||||||
.off("selectionchange");
|
|
||||||
},
|
|
||||||
|
|
||||||
click(e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
return this.get('controller').quoteText(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
@ -2,8 +2,7 @@ import { blank, present } from 'helpers/qunit-helpers';
|
|||||||
import { mapRoutes } from 'discourse/mapping-router';
|
import { mapRoutes } from 'discourse/mapping-router';
|
||||||
|
|
||||||
moduleFor('controller:topic', 'controller:topic', {
|
moduleFor('controller:topic', 'controller:topic', {
|
||||||
needs: ['controller:modal', 'controller:composer', 'controller:quote-button',
|
needs: ['controller:modal', 'controller:composer', 'controller:application'],
|
||||||
'controller:application'],
|
|
||||||
setup() {
|
setup() {
|
||||||
this.registry.register('router:main', mapRoutes());
|
this.registry.register('router:main', mapRoutes());
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user