From d04c4bf8e7ca4b01980cea656d35183e1ed73bb3 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Tue, 26 Feb 2019 14:15:25 +0100 Subject: [PATCH] UX: puts back share-panel as floating pane on post actions (#7066) --- .../discourse/components/share-panel.js.es6 | 46 +---- .../discourse/components/share-popup.js.es6 | 173 ++++++++++++++++++ .../javascripts/discourse/lib/sharing.js.es6 | 21 +++ .../templates/components/share-popup.hbs | 24 +++ .../javascripts/discourse/templates/topic.hbs | 2 + .../javascripts/discourse/widgets/post.js.es6 | 110 ++--------- app/assets/stylesheets/common/base/modal.scss | 4 +- .../stylesheets/common/base/share_link.scss | 87 +++++++++ config/locales/client.en.yml | 2 +- .../share-and-invite-desktop-test.js.es6 | 31 +--- .../share-and-invite-mobile-test.js.es6 | 17 +- test/javascripts/acceptance/topic-test.js.es6 | 7 +- .../components/share-button-test.js.es6 | 16 ++ .../components/share-button.js.es6 | 13 ++ 14 files changed, 367 insertions(+), 186 deletions(-) create mode 100644 app/assets/javascripts/discourse/components/share-popup.js.es6 create mode 100644 app/assets/javascripts/discourse/templates/components/share-popup.hbs create mode 100644 app/assets/stylesheets/common/base/share_link.scss create mode 100644 test/javascripts/components/share-button-test.js.es6 create mode 100644 test/javascripts/components/share-button.js.es6 diff --git a/app/assets/javascripts/discourse/components/share-panel.js.es6 b/app/assets/javascripts/discourse/components/share-panel.js.es6 index 234e8d70a24..18a48011145 100644 --- a/app/assets/javascripts/discourse/components/share-panel.js.es6 +++ b/app/assets/javascripts/discourse/components/share-panel.js.es6 @@ -1,15 +1,12 @@ import { escapeExpression } from "discourse/lib/utilities"; -import { longDateNoYear } from "discourse/lib/formatter"; import { default as computed } from "ember-addons/ember-computed-decorators"; import Sharing from "discourse/lib/sharing"; export default Ember.Component.extend({ tagName: null, - date: Ember.computed.alias("panel.model.date"), type: Ember.computed.alias("panel.model.type"), - postNumber: Ember.computed.alias("panel.model.postNumber"), - postId: Ember.computed.alias("panel.model.postId"), + topic: Ember.computed.alias("panel.model.topic"), @computed @@ -17,21 +14,9 @@ export default Ember.Component.extend({ return Sharing.activeSources(this.siteSettings.share_links); }, - @computed("date") - postDate(date) { - return date ? longDateNoYear(new Date(date)) : null; - }, - - @computed("type", "postNumber", "postDate", "topic.title") - shareTitle(type, postNumber, postDate, topicTitle) { + @computed("type", "topic.title") + shareTitle(type, topicTitle) { topicTitle = escapeExpression(topicTitle); - - if (type === "topic") { - return I18n.t("share.topic_html", { topicTitle }); - } - if (postNumber) { - return I18n.t("share.post_html", { postNumber, postDate }); - } return I18n.t("share.topic_html", { topicTitle }); }, @@ -81,27 +66,10 @@ export default Ember.Component.extend({ actions: { share(source) { - const url = source.generateUrl( - this.get("shareUrl"), - this.get("topic.title") - ); - const options = { - menubar: "no", - toolbar: "no", - resizable: "yes", - scrollbars: "yes", - width: 600, - height: source.popupHeight || 315 - }; - const stringOptions = Object.keys(options) - .map(k => `${k}=${options[k]}`) - .join(","); - - if (source.shouldOpenInPopup) { - window.open(url, "", stringOptions); - } else { - window.open(url, "_blank"); - } + Sharing.shareSource(source, { + url: this.get("shareUrl"), + title: this.get("topic.title") + }); } } }); diff --git a/app/assets/javascripts/discourse/components/share-popup.js.es6 b/app/assets/javascripts/discourse/components/share-popup.js.es6 new file mode 100644 index 00000000000..5b0ff9745e9 --- /dev/null +++ b/app/assets/javascripts/discourse/components/share-popup.js.es6 @@ -0,0 +1,173 @@ +import { wantsNewWindow } from "discourse/lib/intercept-click"; +import { longDateNoYear } from "discourse/lib/formatter"; +import computed from "ember-addons/ember-computed-decorators"; +import Sharing from "discourse/lib/sharing"; +import { nativeShare } from "discourse/lib/pwa-utils"; + +export default Ember.Component.extend({ + elementId: "share-link", + classNameBindings: ["visible"], + link: null, + visible: null, + + @computed + sources() { + return Sharing.activeSources(this.siteSettings.share_links); + }, + + @computed("type", "postNumber") + shareTitle(type, postNumber) { + if (type === "topic") { + return I18n.t("share.topic"); + } + if (postNumber) { + return I18n.t("share.post", { postNumber }); + } + return I18n.t("share.topic"); + }, + + @computed("date") + displayDate(date) { + return longDateNoYear(new Date(date)); + }, + + _focusUrl() { + const link = this.get("link"); + if (!this.capabilities.touch) { + const $linkInput = $("#share-link input"); + $linkInput.val(link); + + // Wait for the fade-in transition to finish before selecting the link: + window.setTimeout(() => $linkInput.select().focus(), 160); + } else { + const $linkForTouch = $("#share-link .share-for-touch a"); + $linkForTouch.attr("href", link); + $linkForTouch.text(link); + const range = window.document.createRange(); + range.selectNode($linkForTouch[0]); + window.getSelection().addRange(range); + } + }, + + _showUrl($target, url) { + const $currentTargetOffset = $target.offset(); + const $this = this.$(); + + if (Ember.isEmpty(url)) { + return; + } + + // Relative urls + if (url.indexOf("/") === 0) { + url = window.location.protocol + "//" + window.location.host + url; + } + + const shareLinkWidth = $this.width(); + let x = $currentTargetOffset.left - shareLinkWidth / 2; + if (x < 25) { + x = 25; + } + if (x + shareLinkWidth > $(window).width()) { + x -= shareLinkWidth / 2; + } + + const header = $(".d-header"); + let y = $currentTargetOffset.top - ($this.height() + 20); + if (y < header.offset().top + header.height()) { + y = $currentTargetOffset.top + 10; + } + + $this.css({ top: "" + y + "px" }); + + if (!this.site.mobileView) { + $this.css({ left: "" + x + "px" }); + } + this.set("link", encodeURI(url)); + this.set("visible", true); + + Ember.run.scheduleOnce("afterRender", this, this._focusUrl); + }, + + didInsertElement() { + this._super(...arguments); + + const $html = $("html"); + $html.on("mousedown.outside-share-link", e => { + // Use mousedown instead of click so this event is handled before routing occurs when a + // link is clicked (which is a click event) while the share dialog is showing. + if (this.$().has(e.target).length !== 0) { + return; + } + this.send("close"); + return true; + }); + + $html.on( + "click.discourse-share-link", + "button[data-share-url], .post-info .post-date[data-share-url]", + e => { + // if they want to open in a new tab, let it so + if (wantsNewWindow(e)) { + return true; + } + + e.preventDefault(); + + const $currentTarget = $(e.currentTarget); + const url = $currentTarget.data("share-url"); + const postNumber = $currentTarget.data("post-number"); + const postId = $currentTarget.closest("article").data("post-id"); + const date = $currentTarget.children().data("time"); + + this.setProperties({ postNumber, date, postId }); + + // use native webshare only when the user clicks on the "chain" icon + if (!$currentTarget.hasClass("post-date")) { + nativeShare({ url }).then(null, () => + this._showUrl($currentTarget, url) + ); + } else { + this._showUrl($currentTarget, url); + } + + return false; + } + ); + + $html.on("keydown.share-view", e => { + if (e.keyCode === 27) { + this.send("close"); + } + }); + + this.appEvents.on("share:url", (url, $target) => + this._showUrl($target, url) + ); + }, + + willDestroyElement() { + this._super(...arguments); + $("html") + .off("click.discourse-share-link") + .off("mousedown.outside-share-link") + .off("keydown.share-view"); + }, + + actions: { + close() { + this.setProperties({ + link: null, + postNumber: null, + postId: null, + visible: false + }); + }, + + share(source) { + Sharing.shareSource(source, { + url: this.get("link"), + title: this.get("topic.title") + }); + } + } +}); diff --git a/app/assets/javascripts/discourse/lib/sharing.js.es6 b/app/assets/javascripts/discourse/lib/sharing.js.es6 index cdd6ec506e6..4e57b9dfa5e 100644 --- a/app/assets/javascripts/discourse/lib/sharing.js.es6 +++ b/app/assets/javascripts/discourse/lib/sharing.js.es6 @@ -47,6 +47,27 @@ export default { _sources[source.id] = source; }, + shareSource(source, data) { + const url = source.generateUrl(data.url, data.title); + const options = { + menubar: "no", + toolbar: "no", + resizable: "yes", + scrollbars: "yes", + width: 600, + height: source.popupHeight || 315 + }; + const stringOptions = Object.keys(options) + .map(k => `${k}=${options[k]}`) + .join(","); + + if (source.shouldOpenInPopup) { + window.open(url, "", stringOptions); + } else { + window.open(url, "_blank"); + } + }, + activeSources(linksSetting = "") { return linksSetting .split("|") diff --git a/app/assets/javascripts/discourse/templates/components/share-popup.hbs b/app/assets/javascripts/discourse/templates/components/share-popup.hbs new file mode 100644 index 00000000000..84b946fcbbc --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/share-popup.hbs @@ -0,0 +1,24 @@ +
+

{{{shareTitle}}}

+ + {{#if date}} + {{displayDate}} + {{/if}} +
+ +
+ + +
+ +
+ {{#each sources as |s|}} + {{share-source source=s title=model.title action=(action "share")}} + {{/each}} + + +
diff --git a/app/assets/javascripts/discourse/templates/topic.hbs b/app/assets/javascripts/discourse/templates/topic.hbs index 14b06cf69e4..0c9629ca9d7 100644 --- a/app/assets/javascripts/discourse/templates/topic.hbs +++ b/app/assets/javascripts/discourse/templates/topic.hbs @@ -318,6 +318,8 @@ {{/if}} + {{share-popup topic=model}} + {{#if embedQuoteButton}} {{quote-button quoteState=quoteState selectText=(action "selectText")}} {{/if}} diff --git a/app/assets/javascripts/discourse/widgets/post.js.es6 b/app/assets/javascripts/discourse/widgets/post.js.es6 index 6eb11139a5a..5aa17d79c99 100644 --- a/app/assets/javascripts/discourse/widgets/post.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post.js.es6 @@ -2,7 +2,6 @@ import PostCooked from "discourse/widgets/post-cooked"; import DecoratorHelper from "discourse/widgets/decorator-helper"; import { createWidget, applyDecorators } from "discourse/widgets/widget"; import { iconNode } from "discourse-common/lib/icon-library"; -import { nativeShare } from "discourse/lib/pwa-utils"; import { transformBasicPost } from "discourse/lib/transform-post"; import { postTransformCallbacks } from "discourse/widgets/post-stream"; import { h } from "virtual-dom"; @@ -14,7 +13,6 @@ import { formatUsername } from "discourse/lib/utilities"; import hbs from "discourse/widgets/hbs-compiler"; -import showModal from "discourse/lib/show-modal"; function transformWithCallbacks(post) { let transformed = transformBasicPost(post); @@ -221,72 +219,6 @@ function showReplyTab(attrs, siteSettings) { ); } -createWidget("post-date", { - tagName: "div.post-info.post-date", - - buildClasses(attrs) { - let classes = "post-date"; - - const lastWikiEdit = - attrs.wiki && attrs.lastWikiEdit && new Date(attrs.lastWikiEdit); - - if (lastWikiEdit) { - classes = `${classes} last-wiki-edit`; - } - - return classes; - }, - - html(attrs) { - return h( - "a", - { - attributes: { - class: "post-date", - href: attrs.shareUrl, - "data-share-url": attrs.shareUrl, - "data-post-number": attrs.post_number - } - }, - dateNode(this._date(attrs)) - ); - }, - - _date(attrs) { - const lastWikiEdit = - attrs.wiki && attrs.lastWikiEdit && new Date(attrs.lastWikiEdit); - const createdAt = new Date(attrs.created_at); - return lastWikiEdit ? lastWikiEdit : createdAt; - }, - - click(event) { - event.preventDefault(); - - const post = this.findAncestorModel(); - - const modalFallback = () => { - showModal("share-and-invite", { - modalClass: "share-and-invite", - panels: [ - { - id: "share", - title: "topic.share.extended_title", - model: { - postNumber: this.attrs.post_number, - shareUrl: this.attrs.shareUrl, - date: this._date(this.attrs), - postId: post.get("id"), - topic: post.get("topic") - } - } - ] - }); - }; - - nativeShare({ url: this.attrs.shareUrl }).then(null, modalFallback); - } -}); - createWidget("post-meta-data", { tagName: "div.topic-meta-data", @@ -309,6 +241,21 @@ createWidget("post-meta-data", { ); } + const lastWikiEdit = + attrs.wiki && attrs.lastWikiEdit && new Date(attrs.lastWikiEdit); + const createdAt = new Date(attrs.created_at); + const date = lastWikiEdit ? dateNode(lastWikiEdit) : dateNode(createdAt); + const attributes = { + class: "post-date", + href: attrs.shareUrl, + "data-share-url": attrs.shareUrl, + "data-post-number": attrs.post_number + }; + + if (lastWikiEdit) { + attributes["class"] += " last-wiki-edit"; + } + if (attrs.via_email) { postInfo.push(this.attach("post-email-indicator", attrs)); } @@ -329,7 +276,7 @@ createWidget("post-meta-data", { postInfo.push(this.attach("reply-to-tab", attrs)); } - postInfo.push(this.attach("post-date", attrs)); + postInfo.push(h("div.post-info.post-date", h("a", { attributes }, date))); postInfo.push( h( @@ -451,31 +398,6 @@ createWidget("post-contents", { return lastWikiEdit ? lastWikiEdit : createdAt; }, - share() { - const post = this.findAncestorModel(); - - const modalFallback = () => { - showModal("share-and-invite", { - modalClass: "share-and-invite", - panels: [ - { - id: "share", - title: "topic.share.extended_title", - model: { - postNumber: this.attrs.post_number, - shareUrl: this.attrs.shareUrl, - date: this._date(this.attrs), - postId: post.get("id"), - topic: post.get("topic") - } - } - ] - }); - }; - - nativeShare({ url: this.attrs.shareUrl }).then(null, modalFallback); - }, - toggleRepliesBelow(goToPost = "false") { if (this.state.repliesBelow.length) { this.state.repliesBelow = []; diff --git a/app/assets/stylesheets/common/base/modal.scss b/app/assets/stylesheets/common/base/modal.scss index 659b86bb295..4fea95eb2ec 100644 --- a/app/assets/stylesheets/common/base/modal.scss +++ b/app/assets/stylesheets/common/base/modal.scss @@ -646,7 +646,9 @@ background: $danger; &.single-tab { - display: none; + background: none; + color: $primary; + padding: s(1 0); } } } diff --git a/app/assets/stylesheets/common/base/share_link.scss b/app/assets/stylesheets/common/base/share_link.scss new file mode 100644 index 00000000000..3abd01db9e4 --- /dev/null +++ b/app/assets/stylesheets/common/base/share_link.scss @@ -0,0 +1,87 @@ +// styles that apply to the "share" popup when sharing a link to a post or topic + +#share-link { + position: absolute; + left: 20px; + z-index: z("dropdown"); + box-shadow: shadow("card"); + background-color: $secondary; + padding: s(2 2 1 2); + width: 300px; + display: none; + &.visible { + display: block; + } + input[type="text"] { + width: 100%; + } + .share-for-touch .overflow-ellipsis { + clear: both; + } + .share-for-touch { + margin: 14px 0; + } + + .title { + margin-bottom: s(1); + align-items: center; + display: flex; + justify-content: space-between; + + h3 { + font-size: $font-0; + margin: 0; + } + + .date { + font-weight: normal; + color: dark-light-choose($primary-medium, $secondary-medium); + } + } + + .copy-text { + display: inline-block; + position: absolute; + margin: 5px 5px 5px 15px; + color: $success; + opacity: 1; + transition: opacity 0.25s; + font-size: $font-0; + &:not(.success) { + opacity: 0; + } + } + .social-link { + margin-right: s(2); + font-size: $font-up-4; + } + .link { + font-size: $font-up-3; + a { + color: dark-light-choose($primary-medium, $secondary-medium); + } + } + + input[type="text"] { + font-size: $font-up-1; + margin-bottom: 0; + } + + .actions { + display: flex; + align-items: center; + margin-top: s(2); + + .link { + margin-left: auto; + } + } +} + +.discourse-no-touch #share-link .share-for-touch { + display: none; +} + +.discourse-touch #share-link input[type="text"] { + display: none; +} diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 135cec512dc..9ee306acad0 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -136,7 +136,7 @@ en: placeholder: date share: topic_html: 'Topic: %{topicTitle}' - post_html: 'Post #%{postNumber}, %{postDate}' + post: "post #%{postNumber}" close: "close" twitter: "Share this link on Twitter" facebook: "Share this link on Facebook" diff --git a/test/javascripts/acceptance/share-and-invite-desktop-test.js.es6 b/test/javascripts/acceptance/share-and-invite-desktop-test.js.es6 index def39dc4b6c..cdca5f7caf2 100644 --- a/test/javascripts/acceptance/share-and-invite-desktop-test.js.es6 +++ b/test/javascripts/acceptance/share-and-invite-desktop-test.js.es6 @@ -68,34 +68,5 @@ QUnit.test("Post date link", async assert => { await visit("/t/internationalization-localization/280"); await click("#post_2 .post-info.post-date a"); - assert.ok(exists(".share-and-invite.modal"), "it shows the modal"); - - assert.ok( - exists(".share-and-invite.modal .modal-tab.share"), - "it shows the share tab" - ); - - assert.ok( - exists(".share-and-invite.modal .modal-tab.share.single-tab"), - "it shows only one tab" - ); - - assert.ok( - !exists(".share-and-invite.modal .modal-tab.invite"), - "it doesn’t show the invite tab" - ); - - assert.ok( - find(".share-and-invite.modal .modal-panel.share .title") - .text() - .includes("Post #2, Feb"), - "it shows the post number with the date" - ); - - assert.ok( - find(".share-and-invite.modal .modal-panel.share .topic-share-url") - .val() - .includes("/t/internationalization-localization/280/2?u=eviltrout"), - "it shows the topic sharing url including the post number" - ); + assert.ok(exists("#share-link"), "it shows the share modal"); }); diff --git a/test/javascripts/acceptance/share-and-invite-mobile-test.js.es6 b/test/javascripts/acceptance/share-and-invite-mobile-test.js.es6 index 0bd3a744bea..561ab00ec6d 100644 --- a/test/javascripts/acceptance/share-and-invite-mobile-test.js.es6 +++ b/test/javascripts/acceptance/share-and-invite-mobile-test.js.es6 @@ -57,20 +57,5 @@ QUnit.test("Post date link", async assert => { await visit("/t/internationalization-localization/280"); await click("#post_2 .post-info.post-date a"); - assert.ok(exists(".share-and-invite.modal"), "it shows the modal"); - - assert.ok( - exists(".share-and-invite.modal .modal-tab.share"), - "it shows the share tab" - ); - - assert.ok( - exists(".share-and-invite.modal .modal-tab.share.single-tab"), - "it shows only one tab" - ); - - assert.ok( - !exists(".share-and-invite.modal .modal-tab.invite"), - "it doesn’t show the invite tab" - ); + assert.ok(exists("#share-link"), "it shows the share modal"); }); diff --git a/test/javascripts/acceptance/topic-test.js.es6 b/test/javascripts/acceptance/topic-test.js.es6 index cf043848d33..c38e5743452 100644 --- a/test/javascripts/acceptance/topic-test.js.es6 +++ b/test/javascripts/acceptance/topic-test.js.es6 @@ -24,12 +24,9 @@ acceptance("Topic", { QUnit.test("Share Modal", async assert => { await visit("/t/internationalization-localization/280"); - await click(".topic-post:first-child button.share"); - assert.ok( - exists(".modal.share-and-invite"), - "it shows the share and invite modal" - ); + + assert.ok(exists("#share-link"), "it shows the share modal"); }); QUnit.test("Showing and hiding the edit controls", async assert => { diff --git a/test/javascripts/components/share-button-test.js.es6 b/test/javascripts/components/share-button-test.js.es6 new file mode 100644 index 00000000000..35032d2751b --- /dev/null +++ b/test/javascripts/components/share-button-test.js.es6 @@ -0,0 +1,16 @@ +import componentTest from "helpers/component-test"; + +moduleForComponent("share-button", { integration: true }); + +componentTest("share button", { + template: '{{share-button url="https://eviltrout.com"}}', + + test(assert) { + assert.ok(find(`button.share`).length, "it has all the classes"); + + assert.ok( + find('button[data-share-url="https://eviltrout.com"]').length, + "it has the data attribute for sharing" + ); + } +}); diff --git a/test/javascripts/components/share-button.js.es6 b/test/javascripts/components/share-button.js.es6 new file mode 100644 index 00000000000..958f821ce6a --- /dev/null +++ b/test/javascripts/components/share-button.js.es6 @@ -0,0 +1,13 @@ +import Button from "discourse/components/d-button"; + +export default Button.extend({ + classNames: ["btn-default", "share"], + icon: "link", + title: "topic.share.help", + label: "topic.share.title", + attributeBindings: ["url:data-share-url"], + + click() { + return true; + } +});