mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
DEV: FloatKit (#23312)
This PR introduces three new UI elements to Discourse codebase through an addon called "FloatKit":
- menu
- tooltip
- toast
Simple cases can be express with an API similar to DButton:
```hbs
<DTooltip
@label={{i18n "foo.bar"}}
@icon="check"
@content="Something"
/>
```
More complex cases can use blocks:
```hbs
<DTooltip>
<:trigger>
{{d-icon "check"}}
<span>{{i18n "foo.bar"}}</span>
</:trigger>
<:content>
Something
</:content>
</DTooltip>
```
You can manually show a tooltip using the `tooltip` service:
```javascript
const tooltipInstance = await this.tooltip.show(
document.querySelector(".my-span"),
options
)
// and later manually close or destroy it
tooltipInstance.close();
tooltipInstance.destroy();
// you can also just close any open tooltip through the service
this.tooltip.close();
```
The service also allows you to register event listeners on a trigger, it removes the need for you to manage open/close of a tooltip started through the service:
```javascript
const tooltipInstance = this.tooltip.register(
document.querySelector(".my-span"),
options
)
// when done you can destroy the instance to remove the listeners
tooltipInstance.destroy();
```
Note that the service also allows you to use a custom component as content which will receive `@data` and `@close` as args:
```javascript
const tooltipInstance = await this.tooltip.show(
document.querySelector(".my-span"),
{
component: MyComponent,
data: { foo: 1 }
}
)
```
Menus are very similar to tooltips and provide the same kind of APIs:
```hbs
<DMenu @icon="plus" @label={{i18n "foo.bar"}}>
<ul>
<li>Foo</li>
<li>Bat</li>
<li>Baz</li>
</ul>
</DMenu>
```
They also support blocks:
```hbs
<DMenu>
<:trigger>
{{d-icon "plus"}}
<span>{{i18n "foo.bar"}}</span>
</:trigger>
<:content>
<ul>
<li>Foo</li>
<li>Bat</li>
<li>Baz</li>
</ul>
</:content>
</DMenu>
```
You can manually show a menu using the `menu` service:
```javascript
const menuInstance = await this.menu.show(
document.querySelector(".my-span"),
options
)
// and later manually close or destroy it
menuInstance.close();
menuInstance.destroy();
// you can also just close any open tooltip through the service
this.menu.close();
```
The service also allows you to register event listeners on a trigger, it removes the need for you to manage open/close of a tooltip started through the service:
```javascript
const menuInstance = this.menu.register(
document.querySelector(".my-span"),
options
)
// when done you can destroy the instance to remove the listeners
menuInstance.destroy();
```
Note that the service also allows you to use a custom component as content which will receive `@data` and `@close` as args:
```javascript
const menuInstance = await this.menu.show(
document.querySelector(".my-span"),
{
component: MyComponent,
data: { foo: 1 }
}
)
```
Interacting with toasts is made only through the `toasts` service.
A default component is provided (DDefaultToast) and can be used through dedicated service methods:
- this.toasts.success({ ... });
- this.toasts.warning({ ... });
- this.toasts.info({ ... });
- this.toasts.error({ ... });
- this.toasts.default({ ... });
```javascript
this.toasts.success({
data: {
title: "Foo",
message: "Bar",
actions: [
{
label: "Ok",
class: "btn-primary",
action: (componentArgs) => {
// eslint-disable-next-line no-alert
alert("Closing toast:" + componentArgs.data.title);
componentArgs.close();
},
}
]
},
});
```
You can also provide your own component:
```javascript
this.toasts.show(MyComponent, {
autoClose: false,
class: "foo",
data: { baz: 1 },
})
```
Co-authored-by: Martin Brennan <mjrbrennan@gmail.com>
Co-authored-by: Isaac Janzen <50783505+janzenisaac@users.noreply.github.com>
Co-authored-by: David Taylor <david@taylorhq.com>
Co-authored-by: Jarek Radosz <jradosz@gmail.com>
This commit is contained in:
@@ -1,30 +1,24 @@
|
||||
{{#if @buttons.length}}
|
||||
<Chat::Composer::Button
|
||||
{{on "click" this.toggleExpand}}
|
||||
@icon="plus"
|
||||
title={{i18n "chat.composer.toggle_toolbar"}}
|
||||
disabled={{@isDisabled}}
|
||||
{{did-insert this.setupTrigger}}
|
||||
<DMenu
|
||||
class={{concat-class
|
||||
"chat-composer-dropdown__trigger-btn"
|
||||
"btn-flat"
|
||||
(if @hasActivePanel "has-active-panel")
|
||||
}}
|
||||
aria-expanded={{if this.isExpanded "true" "false"}}
|
||||
aria-controls={{this.ariaControls}}
|
||||
@title={{i18n "chat.composer.toggle_toolbar"}}
|
||||
@icon="plus"
|
||||
@disabled={{@isDisabled}}
|
||||
@arrow={{true}}
|
||||
@placements={{array "top" "bottom"}}
|
||||
...attributes
|
||||
/>
|
||||
{{#if this.isExpanded}}
|
||||
<ul
|
||||
id="chat-composer-dropdown__list"
|
||||
class="chat-composer-dropdown__list"
|
||||
{{did-insert this.setupPanel}}
|
||||
{{will-destroy this.teardownPanel}}
|
||||
>
|
||||
as |menu|
|
||||
>
|
||||
<ul class="chat-composer-dropdown__list">
|
||||
{{#each @buttons as |button|}}
|
||||
<li class={{concat-class "chat-composer-dropdown__item" button.id}}>
|
||||
<DButton
|
||||
@icon={{button.icon}}
|
||||
@action={{fn this.onButtonClick button}}
|
||||
@action={{fn this.onButtonClick button menu.close}}
|
||||
@label={{button.label}}
|
||||
class={{concat-class
|
||||
"chat-composer-dropdown__action-btn"
|
||||
@@ -34,5 +28,5 @@
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
</DMenu>
|
||||
{{/if}}
|
||||
@@ -1,68 +1,10 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||
import tippy from "tippy.js";
|
||||
import { action } from "@ember/object";
|
||||
import { hideOnEscapePlugin } from "discourse/lib/d-popover";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
|
||||
export default class ChatComposerDropdown extends Component {
|
||||
@tracked isExpanded = false;
|
||||
@tracked tippyInstance = null;
|
||||
|
||||
trigger = null;
|
||||
|
||||
@action
|
||||
setupTrigger(element) {
|
||||
this.trigger = element;
|
||||
}
|
||||
|
||||
get ariaControls() {
|
||||
return this.tippyInstance?.popper?.id;
|
||||
}
|
||||
|
||||
@action
|
||||
toggleExpand() {
|
||||
if (this.args.hasActivePanel) {
|
||||
this.args.onCloseActivePanel?.();
|
||||
} else {
|
||||
this.isExpanded = !this.isExpanded;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
onButtonClick(button) {
|
||||
this.tippyInstance.hide();
|
||||
onButtonClick(button, closeFn) {
|
||||
closeFn();
|
||||
button.action();
|
||||
}
|
||||
|
||||
@action
|
||||
setupPanel(element) {
|
||||
this.tippyInstance = tippy(this.trigger, {
|
||||
theme: "chat-composer-dropdown",
|
||||
trigger: "click",
|
||||
zIndex: 1400,
|
||||
arrow: iconHTML("tippy-rounded-arrow"),
|
||||
interactive: true,
|
||||
allowHTML: false,
|
||||
appendTo: "parent",
|
||||
hideOnClick: true,
|
||||
plugins: [hideOnEscapePlugin],
|
||||
content: element,
|
||||
onShow: () => {
|
||||
this.isExpanded = true;
|
||||
return true;
|
||||
},
|
||||
onHide: () => {
|
||||
this.isExpanded = false;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
this.tippyInstance.show();
|
||||
}
|
||||
|
||||
@action
|
||||
teardownPanel() {
|
||||
this.tippyInstance?.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,13 +35,6 @@
|
||||
<ChatComposerDropdown
|
||||
@buttons={{this.dropdownButtons}}
|
||||
@isDisabled={{this.disabled}}
|
||||
@hasActivePanel={{eq
|
||||
this.chatEmojiPickerManager.picker.context
|
||||
this.context
|
||||
}}
|
||||
@onCloseActivePanel={{this.chatEmojiPickerManager.close}}
|
||||
{{on "focus" (fn this.computeIsFocused true)}}
|
||||
{{on "blur" (fn this.computeIsFocused false)}}
|
||||
/>
|
||||
|
||||
<div
|
||||
|
||||
@@ -425,7 +425,7 @@ export default class ChatComposer extends Component {
|
||||
user.cssClasses = "is-online";
|
||||
}
|
||||
});
|
||||
initUserStatusHtml(result.users);
|
||||
initUserStatusHtml(getOwner(this), result.users);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
import { emojiUnescape, emojiUrlFor } from "discourse/lib/text";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { cached, tracked } from "@glimmer/tracking";
|
||||
import { getReactionText } from "discourse/plugins/chat/discourse/lib/get-reaction-text";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { modifier } from "ember-modifier";
|
||||
import { on } from "@ember/modifier";
|
||||
import and from "truth-helpers/helpers/and";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
|
||||
export default class ChatMessageReaction extends Component {
|
||||
<template>
|
||||
{{! template-lint-disable modifier-name-case }}
|
||||
{{#if (and @reaction this.emojiUrl)}}
|
||||
<button
|
||||
type="button"
|
||||
tabindex="0"
|
||||
class={{concatClass
|
||||
"chat-message-reaction"
|
||||
(if @reaction.reacted "reacted")
|
||||
(if this.isActive "-active")
|
||||
}}
|
||||
data-emoji-name={{@reaction.emoji}}
|
||||
title={{this.emojiString}}
|
||||
{{on "click" this.handleClick passive=true}}
|
||||
{{this.registerTooltip}}
|
||||
>
|
||||
<img
|
||||
loading="lazy"
|
||||
class="emoji"
|
||||
width="20"
|
||||
height="20"
|
||||
alt={{this.emojiString}}
|
||||
src={{this.emojiUrl}}
|
||||
/>
|
||||
|
||||
{{#if (and this.showCount @reaction.count)}}
|
||||
<span class="count">{{@reaction.count}}</span>
|
||||
{{/if}}
|
||||
</button>
|
||||
{{/if}}
|
||||
</template>
|
||||
|
||||
@service capabilities;
|
||||
@service currentUser;
|
||||
@service tooltip;
|
||||
@service site;
|
||||
|
||||
@tracked isActive = false;
|
||||
|
||||
registerTooltip = modifier((element) => {
|
||||
if (!this.popoverContent?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const instance = this.tooltip.register(element, {
|
||||
content: htmlSafe(this.popoverContent),
|
||||
identifier: "chat-message-reaction-tooltip",
|
||||
animated: false,
|
||||
placement: "top",
|
||||
fallbackPlacements: ["bottom"],
|
||||
triggers: this.site.mobileView ? ["hold"] : ["hover"],
|
||||
});
|
||||
|
||||
return () => {
|
||||
instance?.destroy();
|
||||
};
|
||||
});
|
||||
|
||||
get showCount() {
|
||||
return this.args.showCount ?? true;
|
||||
}
|
||||
|
||||
get emojiString() {
|
||||
return `:${this.args.reaction.emoji}:`;
|
||||
}
|
||||
|
||||
get emojiUrl() {
|
||||
return emojiUrlFor(this.args.reaction.emoji);
|
||||
}
|
||||
|
||||
@action
|
||||
handleClick(event) {
|
||||
event.stopPropagation();
|
||||
|
||||
this.args.onReaction?.(
|
||||
this.args.reaction.emoji,
|
||||
this.args.reaction.reacted ? "remove" : "add"
|
||||
);
|
||||
|
||||
this.tooltip.close();
|
||||
}
|
||||
|
||||
@cached
|
||||
get popoverContent() {
|
||||
if (!this.args.reaction.count || !this.args.reaction.users?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
return emojiUnescape(getReactionText(this.args.reaction, this.currentUser));
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
{{#if (and @reaction this.emojiUrl)}}
|
||||
<button
|
||||
type="button"
|
||||
tabindex="0"
|
||||
class={{concat-class
|
||||
"chat-message-reaction"
|
||||
(if @reaction.reacted "reacted")
|
||||
(if this.isActive "-active")
|
||||
}}
|
||||
data-emoji-name={{@reaction.emoji}}
|
||||
data-tippy-content={{this.popoverContent}}
|
||||
title={{this.emojiString}}
|
||||
{{did-insert this.setup}}
|
||||
{{will-destroy this.teardown}}
|
||||
{{did-update this.refreshTooltip this.popoverContent}}
|
||||
>
|
||||
<img
|
||||
loading="lazy"
|
||||
class="emoji"
|
||||
width="20"
|
||||
height="20"
|
||||
alt={{this.emojiString}}
|
||||
src={{this.emojiUrl}}
|
||||
/>
|
||||
|
||||
{{#if (and this.showCount @reaction.count)}}
|
||||
<span class="count">{{@reaction.count}}</span>
|
||||
{{/if}}
|
||||
</button>
|
||||
{{/if}}
|
||||
@@ -1,156 +0,0 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
import { emojiUnescape, emojiUrlFor } from "discourse/lib/text";
|
||||
import { cancel } from "@ember/runloop";
|
||||
import { inject as service } from "@ember/service";
|
||||
import setupPopover from "discourse/lib/d-popover";
|
||||
import discourseLater from "discourse-common/lib/later";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { getReactionText } from "discourse/plugins/chat/discourse/lib/get-reaction-text";
|
||||
|
||||
export default class ChatMessageReaction extends Component {
|
||||
@service capabilities;
|
||||
@service currentUser;
|
||||
|
||||
@tracked isActive = false;
|
||||
|
||||
get showCount() {
|
||||
return this.args.showCount ?? true;
|
||||
}
|
||||
|
||||
@action
|
||||
setup(element) {
|
||||
this.setupListeners(element);
|
||||
this.setupTooltip(element);
|
||||
}
|
||||
|
||||
@action
|
||||
teardown() {
|
||||
cancel(this.longPressHandler);
|
||||
this.teardownTooltip();
|
||||
}
|
||||
|
||||
@action
|
||||
setupListeners(element) {
|
||||
this.element = element;
|
||||
|
||||
if (this.capabilities.touch) {
|
||||
this.element.addEventListener("touchstart", this.onTouchStart, {
|
||||
passive: true,
|
||||
});
|
||||
this.element.addEventListener("touchmove", this.cancelTouch, {
|
||||
passive: true,
|
||||
});
|
||||
this.element.addEventListener("touchend", this.onTouchEnd);
|
||||
this.element.addEventListener("touchCancel", this.cancelTouch);
|
||||
}
|
||||
|
||||
this.element.addEventListener("click", this.handleClick, {
|
||||
passive: true,
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
teardownListeners() {
|
||||
if (this.capabilities.touch) {
|
||||
this.element.removeEventListener("touchstart", this.onTouchStart, {
|
||||
passive: true,
|
||||
});
|
||||
this.element.removeEventListener("touchmove", this.cancelTouch, {
|
||||
passive: true,
|
||||
});
|
||||
this.element.removeEventListener("touchend", this.onTouchEnd);
|
||||
this.element.removeEventListener("touchCancel", this.cancelTouch);
|
||||
}
|
||||
|
||||
this.element.removeEventListener("click", this.handleClick, {
|
||||
passive: true,
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
onTouchStart(event) {
|
||||
event.stopPropagation();
|
||||
this.isActive = true;
|
||||
|
||||
this.longPressHandler = discourseLater(() => {
|
||||
this.touching = false;
|
||||
}, 400);
|
||||
|
||||
this.touching = true;
|
||||
}
|
||||
|
||||
@action
|
||||
cancelTouch() {
|
||||
cancel(this.longPressHandler);
|
||||
this._tippyInstance?.hide();
|
||||
this.touching = false;
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
@action
|
||||
onTouchEnd(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (this.touching) {
|
||||
this.handleClick(event);
|
||||
}
|
||||
|
||||
cancel(this.longPressHandler);
|
||||
this._tippyInstance?.hide();
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
@action
|
||||
setupTooltip(element) {
|
||||
this._tippyInstance = setupPopover(element, {
|
||||
trigger: "mouseenter",
|
||||
interactive: false,
|
||||
allowHTML: true,
|
||||
offset: [0, 10],
|
||||
onShow(instance) {
|
||||
if (instance.props.content === "") {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
teardownTooltip() {
|
||||
this._tippyInstance?.destroy();
|
||||
}
|
||||
|
||||
@action
|
||||
refreshTooltip() {
|
||||
this._tippyInstance?.setContent(this.popoverContent || "");
|
||||
}
|
||||
|
||||
get emojiString() {
|
||||
return `:${this.args.reaction.emoji}:`;
|
||||
}
|
||||
|
||||
get emojiUrl() {
|
||||
return emojiUrlFor(this.args.reaction.emoji);
|
||||
}
|
||||
|
||||
@action
|
||||
handleClick(event) {
|
||||
event.stopPropagation();
|
||||
|
||||
this.args.onReaction?.(
|
||||
this.args.reaction.emoji,
|
||||
this.args.reaction.reacted ? "remove" : "add"
|
||||
);
|
||||
|
||||
this._tippyInstance?.clearDelayTimeouts();
|
||||
}
|
||||
|
||||
get popoverContent() {
|
||||
if (!this.args.reaction.count || !this.args.reaction.users?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
return emojiUnescape(getReactionText(this.args.reaction, this.currentUser));
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,6 @@ import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
|
||||
import ChatOnLongPress from "discourse/plugins/chat/discourse/modifiers/chat/on-long-press";
|
||||
|
||||
let _chatMessageDecorators = [];
|
||||
let _tippyInstances = [];
|
||||
|
||||
export function addChatMessageDecorator(decorator) {
|
||||
_chatMessageDecorators.push(decorator);
|
||||
@@ -297,13 +296,6 @@ export default class ChatMessage extends Component {
|
||||
this.#teardownMentionedUsers();
|
||||
}
|
||||
|
||||
#destroyTippyInstances() {
|
||||
_tippyInstances.forEach((instance) => {
|
||||
instance.destroy();
|
||||
});
|
||||
_tippyInstances = [];
|
||||
}
|
||||
|
||||
@action
|
||||
refreshStatusOnMentions() {
|
||||
schedule("afterRender", () => {
|
||||
@@ -314,7 +306,7 @@ export default class ChatMessage extends Component {
|
||||
);
|
||||
|
||||
mentions.forEach((mention) => {
|
||||
updateUserStatusOnMention(mention, user.status, _tippyInstances);
|
||||
updateUserStatusOnMention(getOwner(this), mention, user.status);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -596,6 +588,5 @@ export default class ChatMessage extends Component {
|
||||
user.stopTrackingStatus();
|
||||
user.off("status-changed", this, "refreshStatusOnMentions");
|
||||
});
|
||||
this.#destroyTippyInstances();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { helper } from "@ember/component/helper";
|
||||
|
||||
export default helper(function noop() {
|
||||
return () => {};
|
||||
});
|
||||
@@ -1,11 +1,3 @@
|
||||
[data-theme="chat-composer-dropdown"] {
|
||||
margin-left: 0.2rem;
|
||||
|
||||
.tippy-content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-composer.is-disabled {
|
||||
.no-touch & {
|
||||
.chat-composer-dropdown__trigger-btn:hover {
|
||||
@@ -18,7 +10,9 @@
|
||||
}
|
||||
|
||||
.chat-composer-dropdown__trigger-btn {
|
||||
margin-left: 0.2rem;
|
||||
transition: transform 0.25s ease-in-out;
|
||||
|
||||
.d-icon {
|
||||
padding: 5px;
|
||||
transition: transform 0.1s ease-in-out;
|
||||
@@ -26,14 +20,25 @@
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:hover,
|
||||
&:active {
|
||||
.d-icon {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
|
||||
background: none !important;
|
||||
background-image: none !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&[aria-expanded="true"] {
|
||||
&.-expanded {
|
||||
.d-icon {
|
||||
transform: rotate(135deg);
|
||||
transform-origin: center;
|
||||
transform-origin: center center;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,9 +50,9 @@
|
||||
}
|
||||
|
||||
.chat-composer-dropdown__action-btn {
|
||||
background: none;
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
background: none;
|
||||
|
||||
.d-icon {
|
||||
color: var(--primary);
|
||||
|
||||
@@ -24,6 +24,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
[data-content][data-identifier="chat-message-reaction-tooltip"] {
|
||||
font-size: var(--font-down-1);
|
||||
|
||||
.emoji {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message {
|
||||
align-items: flex-start;
|
||||
padding: 0.25em 0.5em 0.25em 0.75em;
|
||||
@@ -226,10 +234,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
&:has(.tippy-box) {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.chat-message-reaction-list .chat-message-react-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -166,9 +166,6 @@ RSpec.describe "React to message", type: :system do
|
||||
channel.click_reaction(message_1, "female_detective")
|
||||
|
||||
expect(channel).to have_reaction(message_1, "female_detective", "1")
|
||||
expect(
|
||||
channel.find_reaction(message_1, "female_detective")["data-tippy-content"],
|
||||
).to include(other_user.username)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -61,7 +61,9 @@ module(
|
||||
});
|
||||
|
||||
test("it shows status on mentions", async function (assert) {
|
||||
await render(hbs`<ChatChannel @channel={{this.channel}} />`);
|
||||
await render(
|
||||
hbs`<ChatChannel @channel={{this.channel}} /><DInlineTooltip />`
|
||||
);
|
||||
|
||||
assertStatusIsRendered(
|
||||
assert,
|
||||
@@ -76,7 +78,9 @@ module(
|
||||
});
|
||||
|
||||
test("it updates status on mentions", async function (assert) {
|
||||
await render(hbs`<ChatChannel @channel={{this.channel}} />`);
|
||||
await render(
|
||||
hbs`<ChatChannel @channel={{this.channel}} /><DInlineTooltip />`
|
||||
);
|
||||
|
||||
const newStatus = {
|
||||
description: "off to dentist",
|
||||
@@ -89,11 +93,13 @@ module(
|
||||
|
||||
const selector = statusSelector(mentionedUser.username);
|
||||
await waitFor(selector);
|
||||
|
||||
assertStatusIsRendered(
|
||||
assert,
|
||||
statusSelector(mentionedUser.username),
|
||||
newStatus
|
||||
);
|
||||
|
||||
await assertStatusTooltipIsRendered(
|
||||
assert,
|
||||
statusSelector(mentionedUser.username),
|
||||
@@ -102,7 +108,9 @@ module(
|
||||
});
|
||||
|
||||
test("it deletes status on mentions", async function (assert) {
|
||||
await render(hbs`<ChatChannel @channel={{this.channel}} />`);
|
||||
await render(
|
||||
hbs`<ChatChannel @channel={{this.channel}} /><DInlineTooltip />`
|
||||
);
|
||||
|
||||
this.appEvents.trigger("user-status:changed", {
|
||||
[mentionedUser.id]: null,
|
||||
@@ -114,7 +122,9 @@ module(
|
||||
});
|
||||
|
||||
test("it shows status on mentions on messages that came from Message Bus", async function (assert) {
|
||||
await render(hbs`<ChatChannel @channel={{this.channel}} />`);
|
||||
await render(
|
||||
hbs`<ChatChannel @channel={{this.channel}} /><DInlineTooltip />`
|
||||
);
|
||||
|
||||
await receiveChatMessageViaMessageBus();
|
||||
|
||||
@@ -131,7 +141,9 @@ module(
|
||||
});
|
||||
|
||||
test("it updates status on mentions on messages that came from Message Bus", async function (assert) {
|
||||
await render(hbs`<ChatChannel @channel={{this.channel}} />`);
|
||||
await render(
|
||||
hbs`<ChatChannel @channel={{this.channel}} /><DInlineTooltip />`
|
||||
);
|
||||
await receiveChatMessageViaMessageBus();
|
||||
|
||||
const newStatus = {
|
||||
@@ -157,7 +169,9 @@ module(
|
||||
});
|
||||
|
||||
test("it deletes status on mentions on messages that came from Message Bus", async function (assert) {
|
||||
await render(hbs`<ChatChannel @channel={{this.channel}} />`);
|
||||
await render(
|
||||
hbs`<ChatChannel @channel={{this.channel}} /><DInlineTooltip />`
|
||||
);
|
||||
await receiveChatMessageViaMessageBus();
|
||||
|
||||
this.appEvents.trigger("user-status:changed", {
|
||||
@@ -181,7 +195,7 @@ module(
|
||||
}
|
||||
|
||||
async function assertStatusTooltipIsRendered(assert, selector, status) {
|
||||
await triggerEvent(selector, "mouseenter");
|
||||
await triggerEvent(selector, "mousemove");
|
||||
|
||||
assert.equal(
|
||||
document
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import deprecated from "discourse-common/lib/deprecated";
|
||||
import { getOwner } from "discourse-common/lib/get-owner";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
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 {
|
||||
addTagDecorateCallback,
|
||||
addTextDecorateCallback,
|
||||
} from "discourse/lib/to-markdown";
|
||||
import generateDateMarkup from "discourse/plugins/discourse-local-dates/lib/local-date-markup-generator";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
|
||||
// Import applyLocalDates from discourse/lib/local-dates instead
|
||||
export function applyLocalDates(dates, siteSettings) {
|
||||
@@ -348,11 +347,9 @@ function _calculateDuration(element) {
|
||||
export default {
|
||||
name: "discourse-local-dates",
|
||||
|
||||
@bind
|
||||
showDatePopover(event) {
|
||||
const owner = getOwner(this);
|
||||
if (owner.isDestroyed || owner.isDestroying) {
|
||||
return;
|
||||
}
|
||||
const tooltip = this.container.lookup("service:tooltip");
|
||||
|
||||
if (event?.target?.classList?.contains("download-calendar")) {
|
||||
const dataset = event.target.dataset;
|
||||
@@ -363,50 +360,25 @@ export default {
|
||||
},
|
||||
]);
|
||||
|
||||
// TODO: remove this when rewriting preview as a component
|
||||
const parentPopover = event.target.closest("[data-tippy-root]");
|
||||
if (parentPopover?._tippy) {
|
||||
parentPopover._tippy.hide();
|
||||
}
|
||||
|
||||
return;
|
||||
return tooltip.close();
|
||||
}
|
||||
|
||||
if (!event?.target?.classList?.contains("discourse-local-date")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const siteSettings = owner.lookup("service:site-settings");
|
||||
|
||||
showPopover(event, {
|
||||
trigger: "click",
|
||||
content: buildHtmlPreview(event.target, siteSettings),
|
||||
allowHTML: true,
|
||||
interactive: true,
|
||||
appendTo: "parent",
|
||||
onHidden: (instance) => {
|
||||
instance.destroy();
|
||||
},
|
||||
const siteSettings = this.container.lookup("service:site-settings");
|
||||
return tooltip.show(event.target, {
|
||||
content: htmlSafe(buildHtmlPreview(event.target, siteSettings)),
|
||||
});
|
||||
},
|
||||
|
||||
hideDatePopover(event) {
|
||||
hidePopover(event);
|
||||
},
|
||||
|
||||
initialize(container) {
|
||||
window.addEventListener("click", this.showDatePopover);
|
||||
this.container = container;
|
||||
window.addEventListener("click", this.showDatePopover, { passive: true });
|
||||
|
||||
const siteSettings = container.lookup("service:site-settings");
|
||||
if (siteSettings.discourse_local_dates_enabled) {
|
||||
$.fn.applyLocalDates = function () {
|
||||
deprecated(
|
||||
"`$.applyLocalDates()` is deprecated, import and use `applyLocalDates()` instead."
|
||||
);
|
||||
|
||||
return applyLocalDates(this.toArray(), siteSettings);
|
||||
};
|
||||
|
||||
withPluginApi("0.8.8", initializeDiscourseLocalDates);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -23,29 +23,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
div[data-tippy-root] {
|
||||
.locale-dates-previews {
|
||||
max-width: 360px;
|
||||
.preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 5px;
|
||||
.locale-dates-previews {
|
||||
max-width: 250px;
|
||||
|
||||
.timezone {
|
||||
font-weight: 700;
|
||||
}
|
||||
.preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 5px;
|
||||
margin: 0;
|
||||
|
||||
&.current {
|
||||
background: var(--tertiary-low);
|
||||
}
|
||||
.timezone {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&.current {
|
||||
background: var(--tertiary-low);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.download-calendar {
|
||||
text-align: right;
|
||||
cursor: pointer;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
.download-calendar {
|
||||
text-align: right;
|
||||
cursor: pointer;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.discourse-local-dates-create-modal-footer {
|
||||
|
||||
@@ -31,44 +31,46 @@ describe "Local dates", type: :system do
|
||||
|
||||
expect(topic_page).to have_content(topic.title)
|
||||
|
||||
post_dates = topic_page.find_all("span[data-date]")
|
||||
|
||||
# Single date in a paragraph.
|
||||
#
|
||||
post_dates[0].click
|
||||
tippy_date = topic_page.find(".tippy-content .current .date-time")
|
||||
|
||||
expect(tippy_date).to have_text("#{formatted_date_for_year(12, 15)}\n2:19 PM", exact: true)
|
||||
find("span[data-date]:nth-of-type(1)").click
|
||||
expect(page.find("[data-content] .current .date-time")).to have_text(
|
||||
"#{formatted_date_for_year(12, 15)}\n2:19 PM",
|
||||
exact: true,
|
||||
)
|
||||
page.send_keys(:escape)
|
||||
|
||||
# Two single dates in the same paragraph.
|
||||
#
|
||||
post_dates[1].click
|
||||
tippy_date = topic_page.find(".tippy-content .current .date-time")
|
||||
find("span[data-date]:nth-of-type(2)").click
|
||||
expect(page.find("[data-content] .current .date-time")).to have_text(
|
||||
"#{formatted_date_for_year(12, 15)}\n1:20 AM",
|
||||
exact: true,
|
||||
)
|
||||
page.send_keys(:escape)
|
||||
|
||||
expect(tippy_date).to have_text("#{formatted_date_for_year(12, 15)}\n1:20 AM", exact: true)
|
||||
|
||||
post_dates[2].click
|
||||
tippy_date = topic_page.find(".tippy-content .current .date-time")
|
||||
|
||||
expect(tippy_date).to have_text("#{formatted_date_for_year(12, 15)}\n2:40 AM", exact: true)
|
||||
find("span[data-date]:nth-of-type(3)").click
|
||||
expect(page.find("[data-content] .current .date-time")).to have_text(
|
||||
"#{formatted_date_for_year(12, 15)}\n2:40 AM",
|
||||
exact: true,
|
||||
)
|
||||
page.send_keys(:escape)
|
||||
|
||||
# Two date ranges in the same paragraph.
|
||||
#
|
||||
post_dates[3].click
|
||||
tippy_date = topic_page.find(".tippy-content .current .date-time")
|
||||
|
||||
expect(tippy_date).to have_text(
|
||||
find("span[data-date]:nth-of-type(4)").click
|
||||
expect(page.find("[data-content] .current .date-time")).to have_text(
|
||||
"#{formatted_date_for_year(12, 15)}\n11:25 AM → 12:26 AM",
|
||||
exact: true,
|
||||
)
|
||||
page.send_keys(:escape)
|
||||
|
||||
post_dates[5].click
|
||||
tippy_date = topic_page.find(".tippy-content .current .date-time")
|
||||
|
||||
expect(tippy_date).to have_text(
|
||||
find("span[data-date]:nth-of-type(6)").click
|
||||
expect(page.find("[data-content] .current .date-time")).to have_text(
|
||||
"#{formatted_date_for_year(12, 22)} 11:57 AM → #{formatted_date_for_year(12, 23)} 11:58 AM",
|
||||
exact: true,
|
||||
)
|
||||
page.send_keys(:escape)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import Component from "@glimmer/component";
|
||||
|
||||
export default class DummyComponent extends Component {
|
||||
<template>
|
||||
My custom component with foo: {{@model.foo}}
|
||||
</template>
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<StyleguideExample @title="<Dmenu />">
|
||||
<Styleguide::Component @tag="dmenu component">
|
||||
<:sample>
|
||||
<DMenu
|
||||
@label={{this.label}}
|
||||
@offset={{this.offset}}
|
||||
@arrow={{this.arrow}}
|
||||
@maxWidth={{this.maxWidth}}
|
||||
@identifier={{this.identifier}}
|
||||
@interactive={{this.interactive}}
|
||||
@triggers={{this.triggers}}
|
||||
@untriggers={{this.untriggers}}
|
||||
@content={{this.content}}
|
||||
>
|
||||
{{this.content}}
|
||||
</DMenu>
|
||||
</:sample>
|
||||
</Styleguide::Component>
|
||||
|
||||
<Styleguide::Component @tag="dmenu component">
|
||||
<:sample>
|
||||
<DMenu
|
||||
@offset={{this.offset}}
|
||||
@arrow={{this.arrow}}
|
||||
@maxWidth={{this.maxWidth}}
|
||||
@identifier={{this.identifier}}
|
||||
@interactive={{this.interactive}}
|
||||
@triggers={{this.triggers}}
|
||||
@untriggers={{this.untriggers}}
|
||||
@content={{this.content}}
|
||||
>
|
||||
<:trigger>
|
||||
{{this.label}}
|
||||
</:trigger>
|
||||
<:content>
|
||||
{{this.content}}
|
||||
</:content>
|
||||
</DMenu>
|
||||
</:sample>
|
||||
</Styleguide::Component>
|
||||
|
||||
<Styleguide::Component @tag="menu service">
|
||||
<:sample>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
id="menu-instance"
|
||||
>{{this.label}}</button>
|
||||
</:sample>
|
||||
<:actions>
|
||||
<DButton @action={{this.registerMenu}}>Register</DButton>
|
||||
</:actions>
|
||||
</Styleguide::Component>
|
||||
|
||||
<Styleguide::Component @tag="menu service">
|
||||
<:sample>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
id="menu-instance-with-component"
|
||||
>{{this.label}}</button>
|
||||
</:sample>
|
||||
<:actions>
|
||||
<DButton @action={{this.registerMenuWithComponent}}>Register</DButton>
|
||||
</:actions>
|
||||
</Styleguide::Component>
|
||||
|
||||
<Styleguide::Controls>
|
||||
<Styleguide::Controls::Row @name="Example label">
|
||||
<Input @value={{this.label}} />
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row @name="[@content]">
|
||||
<Input @value={{this.content}} />
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row @name="[@identifier]">
|
||||
<Input @value={{this.identifier}} />
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row @name="[@offset]">
|
||||
<Input @value={{this.offset}} @type="number" />
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row @name="[@triggers]">
|
||||
<Input @value={{this.triggers}} />
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row @name="[@untriggers]">
|
||||
<Input @value={{this.untriggers}} />
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row @name="[@maxWidth]">
|
||||
<Input @value={{this.maxWidth}} @type="number" />
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row @name="[@interactive]">
|
||||
<DToggleSwitch
|
||||
@state={{this.interactive}}
|
||||
{{on "click" this.toggleInteractive}}
|
||||
/>
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row @name="[@arrow]">
|
||||
<DToggleSwitch @state={{this.arrow}} {{on "click" this.toggleArrow}} />
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row @name="[@inline]">
|
||||
<DToggleSwitch @state={{this.inline}} {{on "click" this.toggleInline}} />
|
||||
</Styleguide::Controls::Row>
|
||||
</Styleguide::Controls>
|
||||
</StyleguideExample>
|
||||
@@ -0,0 +1,112 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import DummyComponent from "discourse/plugins/styleguide/discourse/components/dummy-component";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { MENU } from "float-kit/lib/constants";
|
||||
|
||||
export default class Menus extends Component {
|
||||
@service menu;
|
||||
|
||||
@tracked label = "What is this?";
|
||||
@tracked triggers = MENU.options.triggers;
|
||||
@tracked untriggers = MENU.options.untriggers;
|
||||
@tracked arrow = MENU.options.arrow;
|
||||
@tracked inline = MENU.options.inline;
|
||||
@tracked interactive = MENU.options.interactive;
|
||||
@tracked maxWidth = MENU.options.maxWidth;
|
||||
@tracked identifier;
|
||||
@tracked offset = MENU.options.offset;
|
||||
@tracked _content = htmlSafe("<ul><li>Hello</li><li>World!</li></ul>");
|
||||
|
||||
get content() {
|
||||
return this._content;
|
||||
}
|
||||
|
||||
set content(value) {
|
||||
this._content = htmlSafe(value);
|
||||
}
|
||||
|
||||
get templateCode() {
|
||||
return `<DMenu
|
||||
@label={{html-safe "${this.label}"}}
|
||||
@content={{html-safe "${this.content}"}}
|
||||
/>`;
|
||||
}
|
||||
|
||||
get templateCodeContent() {
|
||||
return `<DMenu @maxWidth={{100}}>
|
||||
<:trigger>
|
||||
${this.label}
|
||||
</:trigger>
|
||||
<:content>
|
||||
${this.content}
|
||||
</:content>
|
||||
</DMenu>`;
|
||||
}
|
||||
|
||||
get serviceCode() {
|
||||
return `this.menu.register(
|
||||
document.queryselector(".my-element"),
|
||||
{ content: htmlSafe(${this.content}) }
|
||||
);`;
|
||||
}
|
||||
|
||||
get serviceCodeComponent() {
|
||||
return `this.menu.register(
|
||||
document.queryselector(".my-element"),
|
||||
{ component: MyComponent, data: { foo: 1 } }
|
||||
);`;
|
||||
}
|
||||
|
||||
@action
|
||||
toggleArrow() {
|
||||
this.arrow = !this.arrow;
|
||||
}
|
||||
|
||||
@action
|
||||
toggleInteractive() {
|
||||
this.interactive = !this.interactive;
|
||||
}
|
||||
|
||||
@action
|
||||
toggleInline() {
|
||||
this.inline = !this.inline;
|
||||
}
|
||||
|
||||
@action
|
||||
registerMenu() {
|
||||
this.menuInstance?.destroy();
|
||||
this.menuInstance = this.menu.register(
|
||||
document.querySelector("#menu-instance"),
|
||||
this.options
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
registerMenuWithComponent() {
|
||||
this.menuInstanceWithComponent?.destroy();
|
||||
this.menuInstanceWithComponent = this.menu.register(
|
||||
document.querySelector("#menu-instance-with-component"),
|
||||
{
|
||||
...this.options,
|
||||
component: DummyComponent,
|
||||
data: { foo: 1 },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
get options() {
|
||||
return {
|
||||
offset: this.offset,
|
||||
arrow: this.arrow,
|
||||
maxWidth: this.maxWidth,
|
||||
identifier: this.identifier,
|
||||
interactive: this.interactive,
|
||||
triggers: this.triggers ?? ["click"],
|
||||
untriggers: this.untriggers ?? ["click"],
|
||||
content: this.content,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
<StyleguideExample @title="<DTooltip>">
|
||||
<DButton>
|
||||
{{i18n "styleguide.sections.rich_tooltip.hover_to_see"}}
|
||||
|
||||
<DTooltip>
|
||||
<h3>{{i18n "styleguide.sections.rich_tooltip.header"}}</h3>
|
||||
{{i18n "styleguide.sections.rich_tooltip.description"}}
|
||||
</DTooltip>
|
||||
</DButton>
|
||||
</StyleguideExample>
|
||||
@@ -0,0 +1,93 @@
|
||||
{{! template-lint-disable no-potential-path-strings }}
|
||||
<StyleguideExample @title="Toasts service">
|
||||
<Styleguide::Component @tag="default">
|
||||
<:actions>
|
||||
<DButton
|
||||
@translatedLabel="Show default toast"
|
||||
@action={{fn this.showToast "default"}}
|
||||
/>
|
||||
</:actions>
|
||||
</Styleguide::Component>
|
||||
|
||||
<Styleguide::Component @tag="success">
|
||||
<:actions>
|
||||
<DButton
|
||||
@translatedLabel="Show success toast"
|
||||
@action={{fn this.showToast "success"}}
|
||||
/>
|
||||
</:actions>
|
||||
</Styleguide::Component>
|
||||
|
||||
<Styleguide::Component @tag="warning">
|
||||
<:actions>
|
||||
<DButton
|
||||
@translatedLabel="Show warning toast"
|
||||
@action={{fn this.showToast "warning"}}
|
||||
/>
|
||||
</:actions>
|
||||
</Styleguide::Component>
|
||||
|
||||
<Styleguide::Component @tag="info">
|
||||
<:actions>
|
||||
<DButton
|
||||
@translatedLabel="Show info toast"
|
||||
@action={{fn this.showToast "info"}}
|
||||
/>
|
||||
</:actions>
|
||||
</Styleguide::Component>
|
||||
|
||||
<Styleguide::Component @tag="error">
|
||||
<:actions>
|
||||
<DButton
|
||||
@translatedLabel="Show error toast"
|
||||
@action={{fn this.showToast "error"}}
|
||||
/>
|
||||
</:actions>
|
||||
</Styleguide::Component>
|
||||
|
||||
<Styleguide::Component @tag="custom component">
|
||||
<:actions>
|
||||
<DButton
|
||||
@translatedLabel="Show toast"
|
||||
@action={{this.showCustomComponentToast}}
|
||||
/>
|
||||
</:actions>
|
||||
</Styleguide::Component>
|
||||
|
||||
<Styleguide::Controls>
|
||||
<Styleguide::Controls::Row @name="[@options.autoClose]">
|
||||
<DToggleSwitch
|
||||
@state={{this.autoClose}}
|
||||
{{on "click" this.toggleAutoClose}}
|
||||
/>
|
||||
</Styleguide::Controls::Row>
|
||||
{{#if this.autoClose}}
|
||||
<Styleguide::Controls::Row @name="[@options.duration] ms">
|
||||
<Input @value={{this.duration}} @type="number" />
|
||||
</Styleguide::Controls::Row>
|
||||
{{/if}}
|
||||
<Styleguide::Controls::Row @name="[@options.class]">
|
||||
<Input @value={{this.class}} />
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row>
|
||||
<b>Model props for default:</b>
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row @name="[@options.data.title]">
|
||||
<Input @value={{this.title}} />
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row @name="[@options.data.message]">
|
||||
<Input @value={{this.message}} />
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row @name="[@options.data.icon]">
|
||||
<IconPicker
|
||||
@name="icon"
|
||||
@value={{this.icon}}
|
||||
@options={{hash maximum=1}}
|
||||
@onChange={{action (mut this.icon)}}
|
||||
/>
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row @name="With an action">
|
||||
<DToggleSwitch @state={{this.action}} {{on "click" this.toggleAction}} />
|
||||
</Styleguide::Controls::Row>
|
||||
</Styleguide::Controls>
|
||||
</StyleguideExample>
|
||||
@@ -0,0 +1,70 @@
|
||||
import { action } from "@ember/object";
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { TOAST } from "float-kit/lib/constants";
|
||||
import DummyComponent from "discourse/plugins/styleguide/discourse/components/dummy-component";
|
||||
|
||||
export default class Toasts extends Component {
|
||||
@service toasts;
|
||||
|
||||
@tracked title = "Title";
|
||||
@tracked message = "Message";
|
||||
@tracked duration = TOAST.options.duration;
|
||||
@tracked autoClose = TOAST.options.autoClose;
|
||||
@tracked class;
|
||||
@tracked action = true;
|
||||
@tracked icon;
|
||||
|
||||
@action
|
||||
showCustomComponentToast() {
|
||||
this.toasts.show({
|
||||
duration: this.duration,
|
||||
autoClose: this.autoClose,
|
||||
class: this.class,
|
||||
component: DummyComponent,
|
||||
data: {
|
||||
foo: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
showToast(theme) {
|
||||
const actions = [];
|
||||
|
||||
if (this.action) {
|
||||
actions.push({
|
||||
label: "Ok",
|
||||
class: "btn-primary",
|
||||
action: (args) => {
|
||||
// eslint-disable-next-line no-alert
|
||||
alert("Closing toast:" + args.data.title);
|
||||
args.close();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.toasts[theme]({
|
||||
duration: this.duration,
|
||||
autoClose: this.autoClose,
|
||||
class: this.class,
|
||||
data: {
|
||||
title: this.title,
|
||||
message: this.message,
|
||||
icon: this.icon,
|
||||
actions,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
toggleAction() {
|
||||
this.action = !this.action;
|
||||
}
|
||||
|
||||
@action
|
||||
toggleAutoClose() {
|
||||
this.autoClose = !this.autoClose;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<StyleguideExample @title="<DTooltip />">
|
||||
<Styleguide::Component @tag="tooltip component">
|
||||
<:sample>
|
||||
<DTooltip
|
||||
@label={{this.label}}
|
||||
@offset={{this.offset}}
|
||||
@arrow={{this.arrow}}
|
||||
@maxWidth={{this.maxWidth}}
|
||||
@identifier={{this.identifier}}
|
||||
@interactive={{this.interactive}}
|
||||
@triggers={{this.triggers}}
|
||||
@untriggers={{this.untriggers}}
|
||||
@content={{this.content}}
|
||||
@inline={{this.inline}}
|
||||
/>
|
||||
</:sample>
|
||||
</Styleguide::Component>
|
||||
|
||||
<Styleguide::Component @tag="tooltip component">
|
||||
<:sample>
|
||||
<DTooltip
|
||||
@offset={{this.offset}}
|
||||
@arrow={{this.arrow}}
|
||||
@maxWidth={{this.maxWidth}}
|
||||
@identifier={{this.identifier}}
|
||||
@interactive={{this.interactive}}
|
||||
@triggers={{this.triggers}}
|
||||
@untriggers={{this.untriggers}}
|
||||
@content={{this.content}}
|
||||
@inline={{this.inline}}
|
||||
>
|
||||
<:trigger>
|
||||
{{this.label}}
|
||||
</:trigger>
|
||||
<:content>
|
||||
{{this.content}}
|
||||
</:content>
|
||||
</DTooltip>
|
||||
</:sample>
|
||||
</Styleguide::Component>
|
||||
|
||||
<Styleguide::Component @tag="tooltip service">
|
||||
<:sample>
|
||||
<span id="tooltip-instance">{{this.label}}</span>
|
||||
</:sample>
|
||||
<:actions>
|
||||
<DButton @action={{this.registerTooltip}}>Register</DButton>
|
||||
</:actions>
|
||||
</Styleguide::Component>
|
||||
|
||||
<Styleguide::Component @tag="tooltip service">
|
||||
<:sample>
|
||||
<span id="tooltip-instance-with-component">{{this.label}}</span>
|
||||
</:sample>
|
||||
<:actions>
|
||||
<DButton @action={{this.registerTooltipWithComponent}}>Register</DButton>
|
||||
</:actions>
|
||||
</Styleguide::Component>
|
||||
|
||||
<Styleguide::Controls>
|
||||
<Styleguide::Controls::Row @name="Example label">
|
||||
<Input @value={{this.label}} />
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row @name="[@content]">
|
||||
<Input @value={{this.content}} />
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row @name="[@identifier]">
|
||||
<Input @value={{this.identifier}} />
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row @name="[@offset]">
|
||||
<Input @value={{this.offset}} @type="number" />
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row @name="[@triggers]">
|
||||
<Input @value={{this.triggers}} />
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row @name="[@untriggers]">
|
||||
<Input @value={{this.untriggers}} />
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row @name="[@maxWidth]">
|
||||
<Input @value={{this.maxWidth}} @type="number" />
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row @name="[@interactive]">
|
||||
<DToggleSwitch
|
||||
@state={{this.interactive}}
|
||||
{{on "click" this.toggleInteractive}}
|
||||
/>
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row @name="[@arrow]">
|
||||
<DToggleSwitch @state={{this.arrow}} {{on "click" this.toggleArrow}} />
|
||||
</Styleguide::Controls::Row>
|
||||
<Styleguide::Controls::Row @name="[@inline]">
|
||||
<DToggleSwitch @state={{this.inline}} {{on "click" this.toggleInline}} />
|
||||
</Styleguide::Controls::Row>
|
||||
</Styleguide::Controls>
|
||||
</StyleguideExample>
|
||||
@@ -0,0 +1,112 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import DummyComponent from "discourse/plugins/styleguide/discourse/components/dummy-component";
|
||||
import { TOOLTIP } from "float-kit/lib/constants";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
|
||||
export default class Tooltips extends Component {
|
||||
@service tooltip;
|
||||
|
||||
@tracked label = "What is this?";
|
||||
@tracked triggers = TOOLTIP.options.triggers;
|
||||
@tracked untriggers = TOOLTIP.options.untriggers;
|
||||
@tracked arrow = TOOLTIP.options.arrow;
|
||||
@tracked inline = TOOLTIP.options.inline;
|
||||
@tracked interactive = TOOLTIP.options.interactive;
|
||||
@tracked maxWidth = TOOLTIP.options.maxWidth;
|
||||
@tracked identifier;
|
||||
@tracked offset = TOOLTIP.options.offset;
|
||||
@tracked _content = "Hello World!";
|
||||
|
||||
get content() {
|
||||
return this._content;
|
||||
}
|
||||
|
||||
set content(value) {
|
||||
this._content = htmlSafe(value);
|
||||
}
|
||||
|
||||
get templateCode() {
|
||||
return `<DTooltip
|
||||
@label="${this.label}"
|
||||
@content="${this.content}"
|
||||
/>`;
|
||||
}
|
||||
|
||||
get templateCodeContent() {
|
||||
return `<DTooltip @maxWidth={{100}}>
|
||||
<:trigger>
|
||||
${this.label}
|
||||
</:trigger>
|
||||
<:content>
|
||||
${this.content}
|
||||
</:content>
|
||||
</DTooltip>`;
|
||||
}
|
||||
|
||||
get serviceCode() {
|
||||
return `this.tooltip.register(
|
||||
document.queryselector(".my-element"),
|
||||
{ content: "${this.content}" }
|
||||
);`;
|
||||
}
|
||||
|
||||
get serviceCodeComponent() {
|
||||
return `this.tooltip.register(
|
||||
document.queryselector(".my-element"),
|
||||
{ component: MyComponent, data: { foo: 1 } }
|
||||
);`;
|
||||
}
|
||||
|
||||
@action
|
||||
toggleArrow() {
|
||||
this.arrow = !this.arrow;
|
||||
}
|
||||
|
||||
@action
|
||||
toggleInteractive() {
|
||||
this.interactive = !this.interactive;
|
||||
}
|
||||
|
||||
@action
|
||||
toggleInline() {
|
||||
this.inline = !this.inline;
|
||||
}
|
||||
|
||||
@action
|
||||
registerTooltip() {
|
||||
this.tooltipInstance?.destroy();
|
||||
this.tooltipInstance = this.tooltip.register(
|
||||
document.querySelector("#tooltip-instance"),
|
||||
this.options
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
registerTooltipWithComponent() {
|
||||
this.tooltipInstanceWithComponent?.destroy();
|
||||
this.tooltipInstanceWithComponent = this.tooltip.register(
|
||||
document.querySelector("#tooltip-instance-with-component"),
|
||||
{
|
||||
...this.options,
|
||||
component: DummyComponent,
|
||||
data: { foo: 1 },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
get options() {
|
||||
return {
|
||||
offset: this.offset,
|
||||
arrow: this.arrow,
|
||||
maxWidth: this.maxWidth,
|
||||
identifier: this.identifier,
|
||||
interactive: this.interactive,
|
||||
triggers: this.triggers,
|
||||
untriggers: this.untriggers,
|
||||
content: this.content,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,35 @@
|
||||
<div class="component">
|
||||
{{yield}}
|
||||
<div class="styleguide__component">
|
||||
{{#if @tag}}
|
||||
<span class="styleguide__component-tag">{{@tag}}</span>
|
||||
{{/if}}
|
||||
|
||||
{{#if (has-block "title")}}
|
||||
<div class="styleguide__component-title">
|
||||
{{yield to="title"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if (or (has-block) (has-block "sample"))}}
|
||||
<div class="styleguide__component-sample">
|
||||
{{#if (has-block)}}
|
||||
{{yield}}
|
||||
{{/if}}
|
||||
|
||||
{{#if (has-block "sample")}}
|
||||
{{yield to="sample"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if (has-block "actions")}}
|
||||
<div class="styleguide__component-actions">
|
||||
{{yield to="actions"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if (has-block "code")}}
|
||||
<div class="styleguide__component-code">
|
||||
{{yield to="code"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
@@ -18,7 +18,9 @@ import headerIcons from "../components/sections/molecules/header-icons";
|
||||
import navigationBar from "../components/sections/molecules/navigation-bar";
|
||||
import navigationStacked from "../components/sections/molecules/navigation-stacked";
|
||||
import postMenu from "../components/sections/molecules/post-menu";
|
||||
import richTooltip from "../components/sections/molecules/rich-tooltip";
|
||||
import tooltips from "../components/sections/molecules/tooltips";
|
||||
import menus from "../components/sections/molecules/menus";
|
||||
import toasts from "../components/sections/molecules/toasts";
|
||||
import signupCta from "../components/sections/molecules/signup-cta";
|
||||
import topicListItem from "../components/sections/molecules/topic-list-item";
|
||||
import topicNotifications from "../components/sections/molecules/topic-notifications";
|
||||
@@ -70,7 +72,9 @@ const SECTIONS = [
|
||||
id: "navigation-stacked",
|
||||
},
|
||||
{ component: postMenu, category: "molecules", id: "post-menu" },
|
||||
{ component: richTooltip, category: "molecules", id: "rich-tooltip" },
|
||||
{ component: tooltips, category: "molecules", id: "tooltips" },
|
||||
{ component: menus, category: "molecules", id: "menus" },
|
||||
{ component: toasts, category: "molecules", id: "toasts" },
|
||||
{ component: signupCta, category: "molecules", id: "signup-cta" },
|
||||
{ component: topicListItem, category: "molecules", id: "topic-list-item" },
|
||||
{
|
||||
|
||||
@@ -75,12 +75,6 @@
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
.component {
|
||||
padding: 2rem;
|
||||
border: 2px dotted var(--primary-low);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.component-properties {
|
||||
width: 100%;
|
||||
|
||||
@@ -235,3 +229,42 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.styleguide__component {
|
||||
border: 2px dotted var(--primary-low);
|
||||
margin-bottom: 2rem;
|
||||
position: relative;
|
||||
|
||||
&-tag {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 3px 6px;
|
||||
background: var(--primary-low);
|
||||
max-width: 25%;
|
||||
@include ellipsis;
|
||||
}
|
||||
|
||||
&-sample {
|
||||
display: flex;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
&-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem 2rem;
|
||||
}
|
||||
|
||||
&-code {
|
||||
display: flex;
|
||||
|
||||
.ember-view {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@ en:
|
||||
paragraph: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
|
||||
date_time_inputs:
|
||||
title: "Date/Time inputs"
|
||||
menus:
|
||||
title: "Menus"
|
||||
toasts:
|
||||
title: "Toasts"
|
||||
font_scale:
|
||||
title: "Font System"
|
||||
colors:
|
||||
@@ -83,12 +87,12 @@ en:
|
||||
title: "Spinners"
|
||||
empty_state:
|
||||
title: "Empty State"
|
||||
rich_tooltip:
|
||||
title: "Rich Tooltip"
|
||||
tooltips:
|
||||
title: "Tooltips"
|
||||
description: "Description"
|
||||
header: "Header"
|
||||
hover_to_see: "Hover to see a tooltip"
|
||||
char_counter:
|
||||
title: "Character Counter"
|
||||
placeholder: "Enter your text here..."
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user