From fad94160c76d469cd092e0cc5f75e420496fba03 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Mon, 2 May 2022 17:10:26 +0200 Subject: [PATCH] FIX: uses tippy for popover (#15409) Note this commit also introduce a new {{d-popover}} component, example usage: ```hbs {{#d-popover |state|}} {{d-button label="foo.things" class="d-popover-trigger"}}
Some content
{{/d-popover}} ``` --- app/assets/javascripts/discourse-shims.js | 4 + .../discourse/app/components/d-popover.js | 61 + .../discourse/app/initializers/d-popover.js | 27 +- .../discourse/app/lib/d-popover.js | 196 +- .../app/templates/components/d-popover.hbs | 3 + app/assets/javascripts/discourse/package.json | 1 + .../integration/components/d-popover-test.js | 76 + app/assets/javascripts/vendor-common.js | 1 + app/assets/javascripts/wizard-shims.js | 4 + app/assets/javascripts/wizard-vendor.js | 1 + app/assets/javascripts/yarn.lock | 12 + app/assets/stylesheets/common.scss | 2 + .../stylesheets/common/base/d-popover.scss | 126 +- .../common/components/download-calendar.scss | 2 +- app/assets/stylesheets/vendor/svg-arrow.css | 1 + app/assets/stylesheets/vendor/tippy.css | 1 + lib/svg_sprite.rb | 3 +- lib/tasks/javascript.rake | 32 +- package.json | 1 + .../initializers/discourse-local-dates.js | 38 +- .../common/discourse-local-dates.scss | 2 +- public/tippy.umd.js.map | 1 + vendor/assets/javascripts/tippy.umd.js | 2496 +++++++++++++++++ .../assets/svg-icons/discourse-additional.svg | 5 + yarn.lock | 12 + 25 files changed, 2801 insertions(+), 307 deletions(-) create mode 100644 app/assets/javascripts/discourse/app/components/d-popover.js create mode 100644 app/assets/javascripts/discourse/app/templates/components/d-popover.hbs create mode 100644 app/assets/javascripts/discourse/tests/integration/components/d-popover-test.js create mode 100644 app/assets/stylesheets/vendor/svg-arrow.css create mode 100644 app/assets/stylesheets/vendor/tippy.css create mode 100644 public/tippy.umd.js.map create mode 100644 vendor/assets/javascripts/tippy.umd.js diff --git a/app/assets/javascripts/discourse-shims.js b/app/assets/javascripts/discourse-shims.js index 5870456b45f..7794b099b82 100644 --- a/app/assets/javascripts/discourse-shims.js +++ b/app/assets/javascripts/discourse-shims.js @@ -25,6 +25,10 @@ define("@popperjs/core", ["exports"], function (__exports__) { __exports__.popperGenerator = window.Popper.popperGenerator; }); +define("tippy.js", ["exports"], function (__exports__) { + __exports__.default = window.tippy; +}); + define("@uppy/core", ["exports"], function (__exports__) { __exports__.default = window.Uppy.Core; __exports__.BasePlugin = window.Uppy.Core.BasePlugin; diff --git a/app/assets/javascripts/discourse/app/components/d-popover.js b/app/assets/javascripts/discourse/app/components/d-popover.js new file mode 100644 index 00000000000..697d922fdfe --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/d-popover.js @@ -0,0 +1,61 @@ +import Component from "@ember/component"; +import { iconHTML } from "discourse-common/lib/icon-library"; +import tippy from "tippy.js"; +import { guidFor } from "@ember/object/internals"; + +export default class DiscoursePopover extends Component { + tagName = ""; + + isExpanded = false; + + options = null; + + didInsertElement() { + this._super(...arguments); + + this._setupTippy(); + } + + get componentId() { + return guidFor(this); + } + + _setupTippy() { + const baseOptions = { + trigger: "click", + zIndex: 1400, + arrow: iconHTML("tippy-rounded-arrow"), + interactive: true, + allowHTML: false, + appendTo: "parent", + content: + this.options?.content || + document + .getElementById(this.componentId) + .querySelector( + ":scope > .d-popover-content, :scope > div, :scope > ul" + ), + onShow: () => { + if (this.isDestroyed || this.isDestroying) { + return; + } + this.set("isExpanded", true); + }, + onHide: () => { + if (this.isDestroyed || this.isDestroying) { + return; + } + this.set("isExpanded", false); + }, + }; + + tippy( + document + .getElementById(this.componentId) + .querySelector( + ':scope > .d-popover-trigger, :scope > .btn, :scope > [role="button"]' + ), + Object.assign({}, baseOptions, this.options || {}) + ); + } +} diff --git a/app/assets/javascripts/discourse/app/initializers/d-popover.js b/app/assets/javascripts/discourse/app/initializers/d-popover.js index d3a96f24908..3a46a104807 100644 --- a/app/assets/javascripts/discourse/app/initializers/d-popover.js +++ b/app/assets/javascripts/discourse/app/initializers/d-popover.js @@ -1,20 +1,19 @@ -import { - POPOVER_SELECTORS, - hidePopover, - showPopover, -} from "discourse/lib/d-popover"; +import { showPopover } from "discourse/lib/d-popover"; export default { name: "d-popover", - initialize(container) { - const router = container.lookup("router:main"); - router.on("routeWillChange", hidePopover); - - $("#main") - .on("click.d-popover mouseenter.d-popover", POPOVER_SELECTORS, (e) => - showPopover(e) - ) - .on("mouseleave.d-popover", POPOVER_SELECTORS, (e) => hidePopover(e)); + initialize() { + ["click", "mouseover"].forEach((eventType) => { + document.addEventListener(eventType, (e) => { + if (e.target.dataset.tooltip || e.target.dataset.popover) { + showPopover(e, { + interactive: false, + content: (reference) => + reference.dataset.tooltip || reference.dataset.popover, + }); + } + }); + }); }, }; diff --git a/app/assets/javascripts/discourse/app/lib/d-popover.js b/app/assets/javascripts/discourse/app/lib/d-popover.js index 5438fc885ea..103faf96bf5 100644 --- a/app/assets/javascripts/discourse/app/lib/d-popover.js +++ b/app/assets/javascripts/discourse/app/lib/d-popover.js @@ -1,168 +1,48 @@ -import { siteDir } from "discourse/lib/text-direction"; +import { isLegacyEmber } from "discourse-common/config/environment"; +import { run } from "@ember/runloop"; +import tippy from "tippy.js"; +import { iconHTML } from "discourse-common/lib/icon-library"; -const D_POPOVER_ID = "d-popover"; - -const D_POPOVER_TEMPLATE = ` -
-
-
-
-
-
-
-`; - -const D_ARROW_HEIGHT = 10; - -const D_HORIZONTAL_MARGIN = 5; - -export const POPOVER_SELECTORS = "[data-popover], [data-tooltip]"; - -export function hidePopover() { - getPopover().fadeOut().remove(); - - return getPopover(); +export function hidePopover(event) { + if (event?.target?._tippy) { + showPopover(event); + } } +// options accepts all tippy.js options as defined in their documentation +// https://atomiks.github.io/tippyjs/v6/all-props/ export function showPopover(event, options = {}) { - let $enteredElement = $(event.target).closest(POPOVER_SELECTORS).first(); + const tippyOptions = Object.assign( + { + arrow: iconHTML("tippy-rounded-arrow"), + content: options.textContent || options.htmlContent, + allowHTML: options?.htmlContent?.length, + trigger: "mouseenter click", + hideOnClick: true, + zIndex: 1400, + }, + options + ); - if (!$enteredElement.length) { - $enteredElement = $(event.target); + // legacy support + delete tippyOptions.textContent; + delete tippyOptions.htmlContent; + + const instance = event.target._tippy + ? event.target._tippy + : tippy(event.target, tippyOptions); + + // hangs on legacy ember + if (!isLegacyEmber) { + run.begin(); + instance.popper.addEventListener("transitionend", run.end, { + once: true, + }); } - if (isRetina()) { - getPopover().addClass("retina"); - } - - if (!getPopover().length) { - $("body").append($(D_POPOVER_TEMPLATE)); - } - - setPopoverHtmlContent($enteredElement, options.htmlContent); - setPopoverTextContent($enteredElement, options.textContent); - - getPopover().fadeIn(); - - positionPopover($enteredElement); - - return { - html: (content) => replaceHtmlContent($enteredElement, content), - text: (content) => replaceTextContent($enteredElement, content), - hide: hidePopover, - }; -} - -function setPopoverHtmlContent($enteredElement, content) { - replaceHtmlContent($enteredElement, content); -} - -function setPopoverTextContent($enteredElement, content) { - content = - content || - $enteredElement.attr("data-popover") || - $enteredElement.attr("data-tooltip"); - - replaceTextContent($enteredElement, content); -} - -function replaceTextContent($enteredElement, content) { - if (content) { - getPopover().find(".d-popover-content").text(content); - window.requestAnimationFrame(() => positionPopover($enteredElement)); - } -} - -function replaceHtmlContent($enteredElement, content) { - if (content) { - getPopover().find(".d-popover-content").html(content); - window.requestAnimationFrame(() => positionPopover($enteredElement)); - } -} - -function positionPopover($element) { - const $popover = getPopover(); - $popover.removeClass("is-above is-under is-left-aligned is-right-aligned"); - - const $dHeader = $(".d-header"); - const windowRect = { - left: 0, - top: $dHeader.length ? $dHeader[0].getBoundingClientRect().bottom : 0, - width: $(window).width(), - height: $(window).height(), - }; - - const popoverRect = { - width: $popover.width(), - height: $popover.height(), - left: null, - right: null, - }; - - if (popoverRect.width > windowRect.width - D_HORIZONTAL_MARGIN * 2) { - popoverRect.width = windowRect.width - D_HORIZONTAL_MARGIN * 2; - $popover.width(popoverRect.width); - } - - const targetRect = $element[0].getBoundingClientRect(); - const underSpace = windowRect.height - targetRect.bottom - D_ARROW_HEIGHT; - const topSpace = targetRect.top - windowRect.top - D_ARROW_HEIGHT; - - if ( - underSpace > popoverRect.height + D_HORIZONTAL_MARGIN || - underSpace > topSpace - ) { - $popover - .css("top", targetRect.bottom + window.pageYOffset + D_ARROW_HEIGHT) - .addClass("is-under"); + if (instance.state.isShown) { + instance.hide(); } else { - $popover - .css( - "top", - targetRect.top + - window.pageYOffset - - popoverRect.height - - D_ARROW_HEIGHT - ) - .addClass("is-above"); + instance.show(); } - - const leftSpace = targetRect.left + targetRect.width / 2; - - if (siteDir() === "ltr") { - if (leftSpace > popoverRect.width / 2 + D_HORIZONTAL_MARGIN) { - popoverRect.left = leftSpace - popoverRect.width / 2; - $popover.css("left", popoverRect.left); - } else { - popoverRect.left = D_HORIZONTAL_MARGIN; - $popover.css("left", popoverRect.left).addClass("is-left-aligned"); - } - } else { - const rightSpace = windowRect.width - targetRect.right; - - if (rightSpace > popoverRect.width / 2 + D_HORIZONTAL_MARGIN) { - popoverRect.left = leftSpace - popoverRect.width / 2; - $popover.css("left", popoverRect.left); - } else { - popoverRect.left = - windowRect.width - popoverRect.width - D_HORIZONTAL_MARGIN * 2; - $popover.css("left", popoverRect.left).addClass("is-right-aligned"); - } - } - - let arrowPosition; - if (siteDir() === "ltr") { - arrowPosition = Math.abs(targetRect.left - popoverRect.left); - } else { - arrowPosition = targetRect.left - popoverRect.left + targetRect.width / 2; - } - $popover.find(".d-popover-arrow").css("left", arrowPosition); -} - -function isRetina() { - return window.devicePixelRatio && window.devicePixelRatio > 1; -} - -function getPopover() { - return $(document.getElementById(D_POPOVER_ID)); } diff --git a/app/assets/javascripts/discourse/app/templates/components/d-popover.hbs b/app/assets/javascripts/discourse/app/templates/components/d-popover.hbs new file mode 100644 index 00000000000..011a176bdaa --- /dev/null +++ b/app/assets/javascripts/discourse/app/templates/components/d-popover.hbs @@ -0,0 +1,3 @@ +
+ {{yield (hash isExpanded=isExpanded)}} +
diff --git a/app/assets/javascripts/discourse/package.json b/app/assets/javascripts/discourse/package.json index 4cacc1a6c72..7c064732097 100644 --- a/app/assets/javascripts/discourse/package.json +++ b/app/assets/javascripts/discourse/package.json @@ -23,6 +23,7 @@ "@ember/test-helpers": "^2.2.0", "@glimmer/component": "^1.0.4", "@glimmer/tracking": "^1.0.4", + "tippy.js": "^6.3.7", "@popperjs/core": "2.10.2", "@uppy/aws-s3": "^2.0.8", "@uppy/aws-s3-multipart": "^2.2.1", diff --git a/app/assets/javascripts/discourse/tests/integration/components/d-popover-test.js b/app/assets/javascripts/discourse/tests/integration/components/d-popover-test.js new file mode 100644 index 00000000000..bc6ce8ff2b5 --- /dev/null +++ b/app/assets/javascripts/discourse/tests/integration/components/d-popover-test.js @@ -0,0 +1,76 @@ +import componentTest, { + setupRenderingTest, +} from "discourse/tests/helpers/component-test"; +import { + discourseModule, + exists, + query, +} from "discourse/tests/helpers/qunit-helpers"; +import hbs from "htmlbars-inline-precompile"; +import { showPopover } from "discourse/lib/d-popover"; +import { click } from "@ember/test-helpers"; + +discourseModule("Integration | Component | d-popover", function (hooks) { + setupRenderingTest(hooks); + + componentTest("show/hide popover from lib", { + template: hbs`{{d-button translatedLabel="test" action=onButtonClick forwardEvent=true}}`, + + beforeEach() { + this.set("onButtonClick", (_, event) => { + showPopover(event, { content: "test", trigger: "click", duration: 0 }); + }); + }, + + async test(assert) { + assert.notOk(document.querySelector("div[data-tippy-root]")); + + await click(".btn"); + + assert.equal( + document.querySelector("div[data-tippy-root]").innerText.trim(), + "test" + ); + + await click(".btn"); + + assert.notOk(document.querySelector("div[data-tippy-root]")); + }, + }); + + componentTest("show/hide popover from component", { + template: hbs`{{#d-popover}}{{d-button icon="chevron-down"}}
  • foo
{{/d-popover}}`, + + async test(assert) { + assert.notOk(exists(".d-popover.is-expanded")); + assert.notOk(exists(".test")); + + await click(".btn"); + + assert.ok(exists(".d-popover.is-expanded")); + assert.equal(query(".test").innerText.trim(), "foo"); + }, + }); + + componentTest("using options with component", { + template: hbs`{{#d-popover options=(hash content="bar")}}{{d-button icon="chevron-down"}}{{/d-popover}}`, + + async test(assert) { + await click(".btn"); + + assert.equal(query(".tippy-content").innerText.trim(), "bar"); + }, + }); + + componentTest("d-popover component accepts a block", { + template: hbs`{{#d-popover as |state|}}{{d-button icon=(if state.isExpanded "chevron-up" "chevron-down")}}{{/d-popover}}`, + + async test(assert) { + assert.ok(exists(".d-icon-chevron-down")); + + await click(".btn"); + + assert.ok(exists(".d-icon-chevron-up")); + }, + }); +}); diff --git a/app/assets/javascripts/vendor-common.js b/app/assets/javascripts/vendor-common.js index b6da5bc39ed..9e1d806ec68 100644 --- a/app/assets/javascripts/vendor-common.js +++ b/app/assets/javascripts/vendor-common.js @@ -6,6 +6,7 @@ //= require Markdown.Converter.js //= require bootbox.js //= require popper.js +//= require tippy.umd.js //= require bootstrap-modal.js //= require caret_position //= require itsatrap.js diff --git a/app/assets/javascripts/wizard-shims.js b/app/assets/javascripts/wizard-shims.js index fcfaa05c041..b506393302f 100644 --- a/app/assets/javascripts/wizard-shims.js +++ b/app/assets/javascripts/wizard-shims.js @@ -5,6 +5,10 @@ define("@popperjs/core", ["exports"], function (__exports__) { __exports__.popperGenerator = window.Popper.popperGenerator; }); +define("tippy.js", ["exports"], function (__exports__) { + __exports__.default = window.tippy; +}); + define("@uppy/core", ["exports"], function (__exports__) { __exports__.default = window.Uppy.Core; __exports__.BasePlugin = window.Uppy.Core.BasePlugin; diff --git a/app/assets/javascripts/wizard-vendor.js b/app/assets/javascripts/wizard-vendor.js index fae10ff315d..5d175a6bb59 100644 --- a/app/assets/javascripts/wizard-vendor.js +++ b/app/assets/javascripts/wizard-vendor.js @@ -6,4 +6,5 @@ //= require virtual-dom //= require virtual-dom-amd //= require popper.js +//= require tippy.umd.js //= require wizard-shims diff --git a/app/assets/javascripts/yarn.lock b/app/assets/javascripts/yarn.lock index 5504af2710d..63cc3ced6ab 100644 --- a/app/assets/javascripts/yarn.lock +++ b/app/assets/javascripts/yarn.lock @@ -2120,6 +2120,11 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.2.tgz#0798c03351f0dea1a5a4cabddf26a55a7cbee590" integrity sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ== +"@popperjs/core@^2.9.0": + version "2.11.0" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.0.tgz#6734f8ebc106a0860dff7f92bf90df193f0935d7" + integrity sha512-zrsUxjLOKAzdewIDRWy9nsV1GQsKBCWaGwsZQlCgr6/q+vjyZhFgqedLfFBuI9anTPEUT4APq9Mu0SZBTzIcGQ== + "@simple-dom/interface@^1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@simple-dom/interface/-/interface-1.4.0.tgz#e8feea579232017f89b0138e2726facda6fbb71f" @@ -12905,6 +12910,13 @@ tiny-lr@^2.0.0: object-assign "^4.1.0" qs "^6.4.0" +tippy.js@^6.3.7: + version "6.3.7" + resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c" + integrity sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ== + dependencies: + "@popperjs/core" "^2.9.0" + tmp@0.0.28: version "0.0.28" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.28.tgz#172735b7f614ea7af39664fa84cf0de4e515d120" diff --git a/app/assets/stylesheets/common.scss b/app/assets/stylesheets/common.scss index 8a75aa67fde..2636b095d32 100644 --- a/app/assets/stylesheets/common.scss +++ b/app/assets/stylesheets/common.scss @@ -2,6 +2,8 @@ @import "vendor/normalize"; @import "vendor/normalize-ext"; @import "vendor/pikaday"; +@import "vendor/tippy"; +@import "vendor/svg-arrow"; @import "common/foundation/helpers"; @import "common/foundation/base"; @import "common/select-kit/_index"; diff --git a/app/assets/stylesheets/common/base/d-popover.scss b/app/assets/stylesheets/common/base/d-popover.scss index df9e4840754..738c888d4fa 100644 --- a/app/assets/stylesheets/common/base/d-popover.scss +++ b/app/assets/stylesheets/common/base/d-popover.scss @@ -1,112 +1,30 @@ $d-popover-background: var(--secondary); $d-popover-border: var(--primary-medium); -@-webkit-keyframes popoverFadeIn { - from { - opacity: 0; +.tippy-box { + color: var(--primary); + background: $d-popover-background; + box-shadow: shadow("card"); + border: 1px solid $d-popover-border; +} + +.tippy-box[data-placement^="top"] .tippy-svg-arrow > svg { + top: 12px; +} +.tippy-box[data-placement^="bottom"] .tippy-svg-arrow > svg { + top: -10px; +} +.tippy-rounded-arrow { + fill: $d-popover-background; + .svg-arrow { + fill: $d-popover-border; } - to { - opacity: 1; + .svg-content { + fill: $d-popover-background; } } -@keyframes popoverFadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -#d-popover { - background-color: $d-popover-background; - position: absolute; - z-index: z("modal", "tooltip"); - border-color: $d-popover-border; - border-style: solid; - border-width: 1px; - max-width: 300px; - -webkit-animation: popoverFadeIn 0.5s; - animation: popoverFadeIn 0.5s; - background-clip: padding-box; - display: block; - box-shadow: shadow("dropdown"); - border-radius: 2px; - - &.is-under { - margin-top: 0px; - - .d-popover-top-arrow { - display: block; - } - - .d-popover-bottom-arrow { - display: none; - } - } - - &.is-above { - margin-top: 0px; - - .d-popover-bottom-arrow { - display: block; - } - - .d-popover-top-arrow { - display: none; - } - } - - &.retina { - border-width: 0.5px; - } - - .d-popover-content { - padding: 0.5em; - font-size: $font-down-1; - overflow-wrap: break-word; - -webkit-animation: popoverFadeIn 0.5s; - animation: popoverFadeIn 0.5s; - } - - .d-popover-arrow { - border-style: solid; - color: transparent; - content: ""; - position: absolute; - z-index: z("tooltip") - 100; - } - - .d-popover-top-arrow { - border-color: transparent transparent $d-popover-border; - top: -8px; - border-width: 0 8px 8px; - - &:after { - border-color: transparent transparent $d-popover-background; - border-style: solid; - border-width: 0 7px 7px; - bottom: -8.5px; - margin-left: -7px; - position: absolute; - content: ""; - } - } - - .d-popover-bottom-arrow { - border-color: $d-popover-border transparent transparent; - top: 100%; - border-width: 8px 8px 0; - - &:after { - position: absolute; - content: ""; - border-color: $d-popover-background transparent transparent; - border-style: solid; - border-width: 7px 7px 0; - bottom: 1.5px; - transform: translate(-7px, 0); - } - } +[data-tooltip] > *, +[data-popover] > * { + pointer-events: none; } diff --git a/app/assets/stylesheets/common/components/download-calendar.scss b/app/assets/stylesheets/common/components/download-calendar.scss index 7425ec06b6f..2acfb340f80 100644 --- a/app/assets/stylesheets/common/components/download-calendar.scss +++ b/app/assets/stylesheets/common/components/download-calendar.scss @@ -2,7 +2,7 @@ margin-top: 2em; } -#d-popover .download-calendar { +div[data-tippy-root] .download-calendar { color: var(--primary-med-or-secondary-med); } diff --git a/app/assets/stylesheets/vendor/svg-arrow.css b/app/assets/stylesheets/vendor/svg-arrow.css new file mode 100644 index 00000000000..5e5b389c63c --- /dev/null +++ b/app/assets/stylesheets/vendor/svg-arrow.css @@ -0,0 +1 @@ +.tippy-box[data-placement^=top]>.tippy-svg-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-svg-arrow:after,.tippy-box[data-placement^=top]>.tippy-svg-arrow>svg{top:16px;transform:rotate(180deg)}.tippy-box[data-placement^=bottom]>.tippy-svg-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-svg-arrow>svg{bottom:16px}.tippy-box[data-placement^=left]>.tippy-svg-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-svg-arrow:after,.tippy-box[data-placement^=left]>.tippy-svg-arrow>svg{transform:rotate(90deg);top:calc(50% - 3px);left:11px}.tippy-box[data-placement^=right]>.tippy-svg-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-svg-arrow:after,.tippy-box[data-placement^=right]>.tippy-svg-arrow>svg{transform:rotate(-90deg);top:calc(50% - 3px);right:11px}.tippy-svg-arrow{width:16px;height:16px;fill:#333;text-align:initial}.tippy-svg-arrow,.tippy-svg-arrow>svg{position:absolute} diff --git a/app/assets/stylesheets/vendor/tippy.css b/app/assets/stylesheets/vendor/tippy.css new file mode 100644 index 00000000000..e6ae635cb1f --- /dev/null +++ b/app/assets/stylesheets/vendor/tippy.css @@ -0,0 +1 @@ +.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{position:relative;background-color:#333;color:#fff;border-radius:4px;font-size:14px;line-height:1.4;white-space:normal;outline:0;transition-property:transform,visibility,opacity}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-7px;left:0;border-width:8px 8px 0;border-top-color:initial;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-7px;left:0;border-width:0 8px 8px;border-bottom-color:initial;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:initial;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-7px;border-width:8px 8px 8px 0;border-right-color:initial;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{width:16px;height:16px;color:#333}.tippy-arrow:before{content:"";position:absolute;border-color:transparent;border-style:solid}.tippy-content{position:relative;padding:5px 9px;z-index:1} \ No newline at end of file diff --git a/lib/svg_sprite.rb b/lib/svg_sprite.rb index 7d02349be15..68e783c03f3 100644 --- a/lib/svg_sprite.rb +++ b/lib/svg_sprite.rb @@ -213,7 +213,8 @@ module SvgSprite "user-times", "users", "wrench", - "spinner" + "spinner", + "tippy-rounded-arrow" ]) FA_ICON_MAP = { 'far fa-' => 'far-', 'fab fa-' => 'fab-', 'fas fa-' => '', 'fa-' => '' } diff --git a/lib/tasks/javascript.rake b/lib/tasks/javascript.rake index 82de0510b2b..7f2ceb59059 100644 --- a/lib/tasks/javascript.rake +++ b/lib/tasks/javascript.rake @@ -148,8 +148,18 @@ def dependencies }, { source: '@popperjs/core/dist/umd/popper.js.map', public_root: true - }, - { + }, { + source: 'tippy.js/dist/tippy.umd.js' + }, { + source: 'tippy.js/dist/tippy.umd.js.map', + public_root: true + }, { + source: 'tippy.js/dist/tippy.css', + destination: '../../../app/assets/stylesheets/vendor' + }, { + source: 'tippy.js/dist/svg-arrow.css', + destination: '../../../app/assets/stylesheets/vendor' + }, { source: 'route-recognizer/dist/route-recognizer.js' }, { source: 'route-recognizer/dist/route-recognizer.js.map', @@ -206,6 +216,14 @@ def public_path_name(f) f[:destination] || node_package_name(f) end +def absolute_sourcemap(dest) + File.open(dest) do |file| + contents = file.read + contents.gsub!(/sourceMappingURL=(.*)/, 'sourceMappingURL=/\1') + File.open(dest, "w+") { |d| d.write(contents) } + end +end + task 'javascript:update_constants' => :environment do task_name = 'update_constants' @@ -330,14 +348,12 @@ task 'javascript:update' => 'clean_up' do FileUtils.cp_r(src, dest) end - # use absolute path for popper.js's sourcemap # avoids noisy console warnings in dev environment for non-homepage paths if dest.end_with? "popper.js" - File.open(dest) do |file| - contents = file.read - contents.gsub!("sourceMappingURL=popper", "sourceMappingURL=/popper") - File.open(dest, "w+") { |d| d.write(contents) } - end + absolute_sourcemap(dest) + end + if dest.end_with? "tippy.umd.js" + absolute_sourcemap(dest) end end diff --git a/package.json b/package.json index 356c2b0bde3..fc217dafda2 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@discourse/moment-timezone-names-translations": "^1.0.0", "@highlightjs/cdn-assets": "^10.7.0", "@json-editor/json-editor": "^2.6.1", + "tippy.js": "^6.3.7", "@popperjs/core": "v2.10.2", "@uppy/aws-s3": "^2.0.8", "@uppy/aws-s3-multipart": "^2.2.1", diff --git a/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js b/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js index 5c2aa6a2dfa..d73c671140f 100644 --- a/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js +++ b/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js @@ -1,12 +1,12 @@ import deprecated from "discourse-common/lib/deprecated"; import { getOwner } from "discourse-common/lib/get-owner"; -import { hidePopover, showPopover } from "discourse/lib/d-popover"; import LocalDateBuilder from "../lib/local-date-builder"; import { withPluginApi } from "discourse/lib/plugin-api"; import showModal from "discourse/lib/show-modal"; import { downloadCalendar } from "discourse/lib/download-calendar"; import { renderIcon } from "discourse-common/lib/icon-library"; import I18n from "I18n"; +import { hidePopover, showPopover } from "discourse/lib/d-popover"; // Import applyLocalDates from discourse/lib/local-dates instead export function applyLocalDates(dates, siteSettings) { @@ -238,39 +238,37 @@ export default { return; } - const siteSettings = owner.lookup("site-settings:main"); - if (event?.target?.classList?.contains("discourse-local-date")) { - if ($(document.getElementById("d-popover"))[0]) { - hidePopover(event); - } else { - showPopover(event, { - htmlContent: buildHtmlPreview(event.target, siteSettings), - }); - } - } else if (event?.target?.classList?.contains("download-calendar")) { + if (event?.target?.classList?.contains("download-calendar")) { const dataset = event.target.dataset; - hidePopover(event); downloadCalendar(dataset.title, [ { startsAt: dataset.startsAt, endsAt: dataset.endsAt, }, ]); - } else { - hidePopover(event); + return; } + + if (!event?.target?.classList?.contains("discourse-local-date")) { + return; + } + + const siteSettings = owner.lookup("site-settings:main"); + + showPopover(event, { + trigger: "click", + content: buildHtmlPreview(event.target, siteSettings), + allowHTML: true, + interactive: true, + appendTo: "parent", + }); }, hideDatePopover(event) { - if (event?.target?.classList?.contains("discourse-local-date")) { - hidePopover(event); - } + hidePopover(event); }, initialize(container) { - const router = container.lookup("router:main"); - router.on("routeWillChange", hidePopover); - window.addEventListener("click", this.showDatePopover); const siteSettings = container.lookup("site-settings:main"); diff --git a/plugins/discourse-local-dates/assets/stylesheets/common/discourse-local-dates.scss b/plugins/discourse-local-dates/assets/stylesheets/common/discourse-local-dates.scss index ee45d9826e7..22cce6ab5dd 100644 --- a/plugins/discourse-local-dates/assets/stylesheets/common/discourse-local-dates.scss +++ b/plugins/discourse-local-dates/assets/stylesheets/common/discourse-local-dates.scss @@ -23,7 +23,7 @@ } } -#d-popover { +div[data-tippy-root] { .locale-dates-previews { max-width: 360px; .preview { diff --git a/public/tippy.umd.js.map b/public/tippy.umd.js.map new file mode 100644 index 00000000000..82b7f970663 --- /dev/null +++ b/public/tippy.umd.js.map @@ -0,0 +1 @@ +{"version":3,"file":"tippy.umd.js","sources":["../src/constants.ts","../src/utils.ts","../src/dom-utils.ts","../src/bindGlobalEventListeners.ts","../src/browser.ts","../src/validation.ts","../src/props.ts","../src/template.ts","../src/createTippy.ts","../src/index.ts","../src/addons/createSingleton.ts","../src/addons/delegate.ts","../src/plugins/animateFill.ts","../src/plugins/followCursor.ts","../src/plugins/inlinePositioning.ts","../src/plugins/sticky.ts","../build/base-umd.js"],"sourcesContent":["export const ROUND_ARROW =\n '';\n\nexport const BOX_CLASS = `__NAMESPACE_PREFIX__-box`;\nexport const CONTENT_CLASS = `__NAMESPACE_PREFIX__-content`;\nexport const BACKDROP_CLASS = `__NAMESPACE_PREFIX__-backdrop`;\nexport const ARROW_CLASS = `__NAMESPACE_PREFIX__-arrow`;\nexport const SVG_ARROW_CLASS = `__NAMESPACE_PREFIX__-svg-arrow`;\n\nexport const TOUCH_OPTIONS = {passive: true, capture: true};\n\nexport const TIPPY_DEFAULT_APPEND_TO = () => document.body;\n","import {BasePlacement, Placement} from './types';\n\nexport function hasOwnProperty(\n obj: Record,\n key: string\n): boolean {\n return {}.hasOwnProperty.call(obj, key);\n}\n\nexport function getValueAtIndexOrReturn(\n value: T | [T | null, T | null],\n index: number,\n defaultValue: T | [T, T]\n): T {\n if (Array.isArray(value)) {\n const v = value[index];\n return v == null\n ? Array.isArray(defaultValue)\n ? defaultValue[index]\n : defaultValue\n : v;\n }\n\n return value;\n}\n\nexport function isType(value: any, type: string): boolean {\n const str = {}.toString.call(value);\n return str.indexOf('[object') === 0 && str.indexOf(`${type}]`) > -1;\n}\n\nexport function invokeWithArgsOrReturn(value: any, args: any[]): any {\n return typeof value === 'function' ? value(...args) : value;\n}\n\nexport function debounce(\n fn: (arg: T) => void,\n ms: number\n): (arg: T) => void {\n // Avoid wrapping in `setTimeout` if ms is 0 anyway\n if (ms === 0) {\n return fn;\n }\n\n let timeout: any;\n\n return (arg): void => {\n clearTimeout(timeout);\n timeout = setTimeout(() => {\n fn(arg);\n }, ms);\n };\n}\n\nexport function removeProperties(obj: T, keys: string[]): Partial {\n const clone = {...obj};\n keys.forEach((key) => {\n delete (clone as any)[key];\n });\n return clone;\n}\n\nexport function splitBySpaces(value: string): string[] {\n return value.split(/\\s+/).filter(Boolean);\n}\n\nexport function normalizeToArray(value: T | T[]): T[] {\n return ([] as T[]).concat(value);\n}\n\nexport function pushIfUnique(arr: T[], value: T): void {\n if (arr.indexOf(value) === -1) {\n arr.push(value);\n }\n}\n\nexport function appendPxIfNumber(value: string | number): string {\n return typeof value === 'number' ? `${value}px` : value;\n}\n\nexport function unique(arr: T[]): T[] {\n return arr.filter((item, index) => arr.indexOf(item) === index);\n}\n\nexport function getNumber(value: string | number): number {\n return typeof value === 'number' ? value : parseFloat(value);\n}\n\nexport function getBasePlacement(placement: Placement): BasePlacement {\n return placement.split('-')[0] as BasePlacement;\n}\n\nexport function arrayFrom(value: ArrayLike): any[] {\n return [].slice.call(value);\n}\n\nexport function removeUndefinedProps(\n obj: Record\n): Partial> {\n return Object.keys(obj).reduce((acc, key) => {\n if (obj[key] !== undefined) {\n (acc as any)[key] = obj[key];\n }\n\n return acc;\n }, {});\n}\n","import {ReferenceElement, Targets} from './types';\nimport {PopperTreeData} from './types-internal';\nimport {arrayFrom, isType, normalizeToArray, getBasePlacement} from './utils';\n\nexport function div(): HTMLDivElement {\n return document.createElement('div');\n}\n\nexport function isElement(value: unknown): value is Element | DocumentFragment {\n return ['Element', 'Fragment'].some((type) => isType(value, type));\n}\n\nexport function isNodeList(value: unknown): value is NodeList {\n return isType(value, 'NodeList');\n}\n\nexport function isMouseEvent(value: unknown): value is MouseEvent {\n return isType(value, 'MouseEvent');\n}\n\nexport function isReferenceElement(value: any): value is ReferenceElement {\n return !!(value && value._tippy && value._tippy.reference === value);\n}\n\nexport function getArrayOfElements(value: Targets): Element[] {\n if (isElement(value)) {\n return [value];\n }\n\n if (isNodeList(value)) {\n return arrayFrom(value);\n }\n\n if (Array.isArray(value)) {\n return value;\n }\n\n return arrayFrom(document.querySelectorAll(value));\n}\n\nexport function setTransitionDuration(\n els: (HTMLDivElement | null)[],\n value: number\n): void {\n els.forEach((el) => {\n if (el) {\n el.style.transitionDuration = `${value}ms`;\n }\n });\n}\n\nexport function setVisibilityState(\n els: (HTMLDivElement | null)[],\n state: 'visible' | 'hidden'\n): void {\n els.forEach((el) => {\n if (el) {\n el.setAttribute('data-state', state);\n }\n });\n}\n\nexport function getOwnerDocument(\n elementOrElements: Element | Element[]\n): Document {\n const [element] = normalizeToArray(elementOrElements);\n\n // Elements created via a