diff --git a/app/assets/javascripts/discourse/components/quote-button.js.es6 b/app/assets/javascripts/discourse/components/quote-button.js.es6 index 6e3ad3502ab..f8aea968c83 100644 --- a/app/assets/javascripts/discourse/components/quote-button.js.es6 +++ b/app/assets/javascripts/discourse/components/quote-button.js.es6 @@ -2,129 +2,53 @@ 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) { +function willQuote(e) { const $target = $(e.target); - return $target.hasClass('quote-button') || - $target.closest('.create').length || - $target.closest('.reply-new').length || - $target.closest('.share').length; + return $target.hasClass('quote-button') || $target.closest('.create, .share, .reply-new').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 => { - this.set('isMouseDown', false); - if (ignoreElements(e)) { return; } - - this._selectText(e.target); - }).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 (!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; - } + _isMouseDown: false, + _selectionChanged() { const selection = window.getSelection(); - if (selection.isCollapsed) { - return; + if (selection.isCollapsed) { return; } + + // ensure we selected content inside 1 post + let firstRange, postId; + for (let r = 0; r < selection.rangeCount; r++) { + const range = selection.getRangeAt(r); + firstRange = firstRange || range; + + const $ancestor = $(range.commonAncestorContainer); + if ($ancestor.closest(".contents").length === 0) { return; } + + postId = postId || $ancestor.closest('.boxed, .reply').data('post-id'); + if (!postId) { return; } } - const range = selection.getRangeAt(0), - cloned = range.cloneRange(), - $ancestor = $(range.commonAncestorContainer); + this.get("quoteState").setProperties({ postId, buffer: selectedText() }); - if ($ancestor.closest('.cooked').length === 0) { - return this.sendAction('deselectText'); - } - - const selVal = selectedText(); - if (quoteState.get('buffer') === selVal) { return; } - quoteState.setProperties({ postId, buffer: selVal }); + // 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 } = this.capabilities; + const showAtEnd = isMobileDevice || isIOS || isAndroid; // 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); + // on mobile, collapse the range at the end of the selection + if (showAtEnd) { firstRange.collapse(); } + // insert the marker + firstRange.insertNode(markerElement); // retrieve the position of the marker const $markerElement = $(markerElement); @@ -135,41 +59,51 @@ export default Ember.Component.extend({ // 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); + // change the position of the button + Ember.run.scheduleOnce("afterRender", () => { + let top = markerOffset.top; + let left = markerOffset.left + Math.max(0, parentScrollLeft); - 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()); + if (showAtEnd) { + top = top + 20; + left = Math.min(left + 10, $(window).width() - $quoteButton.outerWidth()); } else { - topOff = topOff - $quoteButton.outerHeight() - 5; + top = top - $quoteButton.outerHeight() - 5; } - $quoteButton.offset({ top: topOff, left: leftOff }); + $quoteButton.offset({ top, left }); + }); + + }, + + didInsertElement() { + const { isWinphone, isAndroid } = this.capabilities; + const wait = (isWinphone || isAndroid) ? 250 : 25; + const onSelectionChanged = _.debounce(() => this._selectionChanged(), wait); + + $(document).on("mousedown.quote-button", (e) => { + this._isMouseDown = true; + if (!willQuote(e)) { + this.sendAction("deselectText"); + } + }).on("mouseup.quote-button", () => { + this._isMouseDown = false; + onSelectionChanged(); + }).on("selectionchange.quote-button", () => { + if (!this._isMouseDown) { + onSelectionChanged(); + } }); }, willDestroyElement() { - $(document) - .off("mousedown.quote-button") - .off("mouseup.quote-button") - .off("touchstart.quote-button") - .off("touchend.quote-button") - .off("selectionchange"); - - this.sendAction('deselectText'); + $(document).off("mousedown.quote-button") + .off("mouseup.quote-button") + .off("selectionchange.quote-button"); }, click(e) { - e.stopPropagation(); - this.sendAction('selectText'); + this.sendAction("selectText"); + return false; } }); diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index f457bea90e6..377aebd76d5 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -168,23 +168,21 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { actions: { deselectText() { - const quoteState = this.get('quoteState'); - quoteState.setProperties({ buffer: null, postId: null }); + this.get('quoteState').setProperties({ buffer: null, postId: null }); }, selectText() { - const quoteState = this.get('quoteState'); - const postStream = this.get('model.postStream'); + const { postId, buffer } = this.get('quoteState'); - const postId = quoteState.get('postId'); - postStream.loadPost(postId).then(post => { + this.send('deselectText'); + + this.get('model.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') @@ -197,19 +195,19 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { } // If the composer is associated with a different post, we don't change it. + const composer = this.get('composer'); 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')); + const quotedText = Quote.build(post, 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'); }); }, diff --git a/app/assets/javascripts/discourse/lib/utilities.js.es6 b/app/assets/javascripts/discourse/lib/utilities.js.es6 index 0385fa65550..579e7dbc6d9 100644 --- a/app/assets/javascripts/discourse/lib/utilities.js.es6 +++ b/app/assets/javascripts/discourse/lib/utilities.js.es6 @@ -85,37 +85,27 @@ export function extractDomainFromUrl(url) { } export function selectedText() { - var html = ''; + const selection = window.getSelection(); + if (selection.isCollapsed) { return ""; } - if (typeof window.getSelection !== "undefined") { - var sel = window.getSelection(); - if (sel.rangeCount) { - var container = document.createElement("div"); - for (var i = 0, len = sel.rangeCount; i < len; ++i) { - container.appendChild(sel.getRangeAt(i).cloneContents()); - } - html = container.innerHTML; - } - } else if (typeof document.selection !== "undefined") { - if (document.selection.type === "Text") { - html = document.selection.createRange().htmlText; - } + const $div = $("