mirror of
https://github.com/discourse/discourse.git
synced 2025-01-19 03:22:46 +08:00
DEV: Convert d-modal and d-modal-body to glimmer components
This commit is contained in:
parent
11e7e949b7
commit
771c4de7f1
|
@ -0,0 +1,10 @@
|
|||
<div
|
||||
id={{@id}}
|
||||
class={{concat-class "modal-body" @class}}
|
||||
tabindex="-1"
|
||||
{{did-insert this.didInsert}}
|
||||
{{will-destroy this.willDestroy}}
|
||||
...attributes
|
||||
>
|
||||
{{yield}}
|
||||
</div>
|
|
@ -1,54 +1,63 @@
|
|||
import { attributeBindings, classNames } from "@ember-decorators/component";
|
||||
import Component from "@ember/component";
|
||||
import Component from "@glimmer/component";
|
||||
import { scheduleOnce } from "@ember/runloop";
|
||||
import { disableImplicitInjections } from "discourse/lib/implicit-injections";
|
||||
import { action } from "@ember/object";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
@classNames("modal-body")
|
||||
@attributeBindings("tabindex")
|
||||
function pick(object, keys) {
|
||||
const result = {};
|
||||
for (const key of keys) {
|
||||
if (key in object) {
|
||||
result[key] = object[key];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@disableImplicitInjections
|
||||
export default class DModalBody extends Component {
|
||||
fixed = false;
|
||||
submitOnEnter = true;
|
||||
dismissable = true;
|
||||
tabindex = -1;
|
||||
@service appEvents;
|
||||
|
||||
didInsertElement() {
|
||||
super.didInsertElement(...arguments);
|
||||
@tracked fixed = false;
|
||||
|
||||
@action
|
||||
didInsert(element) {
|
||||
this._modalAlertElement = document.getElementById("modal-alert");
|
||||
if (this._modalAlertElement) {
|
||||
this._clearFlash();
|
||||
}
|
||||
|
||||
let fixedParent = this.element.closest(".d-modal.fixed-modal");
|
||||
const fixedParent = element.closest(".d-modal.fixed-modal");
|
||||
if (fixedParent) {
|
||||
this.set("fixed", true);
|
||||
this.fixed = true;
|
||||
$(fixedParent).modal("show");
|
||||
}
|
||||
|
||||
scheduleOnce("afterRender", this, this._afterFirstRender);
|
||||
this.appEvents.on("modal-body:flash", this, "_flash");
|
||||
this.appEvents.on("modal-body:clearFlash", this, "_clearFlash");
|
||||
scheduleOnce("afterRender", () => this._afterFirstRender(element));
|
||||
}
|
||||
|
||||
willDestroyElement() {
|
||||
super.willDestroyElement(...arguments);
|
||||
@action
|
||||
willDestroy() {
|
||||
this.appEvents.off("modal-body:flash", this, "_flash");
|
||||
this.appEvents.off("modal-body:clearFlash", this, "_clearFlash");
|
||||
this.appEvents.trigger("modal:body-dismissed");
|
||||
}
|
||||
|
||||
_afterFirstRender() {
|
||||
const maxHeight = this.maxHeight;
|
||||
_afterFirstRender(element) {
|
||||
const maxHeight = this.args.maxHeight;
|
||||
if (maxHeight) {
|
||||
const maxHeightFloat = parseFloat(maxHeight) / 100.0;
|
||||
if (maxHeightFloat > 0) {
|
||||
const viewPortHeight = $(window).height();
|
||||
this.element.style.maxHeight =
|
||||
element.style.maxHeight =
|
||||
Math.floor(maxHeightFloat * viewPortHeight) + "px";
|
||||
}
|
||||
}
|
||||
|
||||
this.appEvents.trigger(
|
||||
"modal:body-shown",
|
||||
this.getProperties(
|
||||
pick(this.args, [
|
||||
"title",
|
||||
"rawTitle",
|
||||
"fixed",
|
||||
|
@ -56,8 +65,8 @@ export default class DModalBody extends Component {
|
|||
"rawSubtitle",
|
||||
"submitOnEnter",
|
||||
"dismissable",
|
||||
"headerClass"
|
||||
)
|
||||
"headerClass",
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,56 +1,78 @@
|
|||
<div class="modal-outer-container">
|
||||
<div class="modal-middle-container">
|
||||
<div class="modal-inner-container">
|
||||
<PluginOutlet @name="above-modal-header" @connectorTagName="div" />
|
||||
<div class="modal-header {{this.headerClass}}">
|
||||
{{#if this.dismissable}}
|
||||
<DButton
|
||||
@icon="times"
|
||||
@action={{route-action "closeModal" "initiatedByCloseButton"}}
|
||||
@class="btn-flat modal-close close"
|
||||
@title="modal.close"
|
||||
/>
|
||||
{{/if}}
|
||||
{{! template-lint-disable no-down-event-binding }}
|
||||
{{! template-lint-disable no-invalid-interactive }}
|
||||
|
||||
{{#if this.title}}
|
||||
<div class="title">
|
||||
<h3 id="discourse-modal-title">{{this.title}}</h3>
|
||||
<div
|
||||
class={{concat-class
|
||||
"modal"
|
||||
"d-modal"
|
||||
this.modalClass
|
||||
this.modalStyle
|
||||
(if this.hasPanels "has-panels")
|
||||
}}
|
||||
id={{if (not-eq this.modalStyle "inline-modal") "discourse-modal"}}
|
||||
data-keyboard="false"
|
||||
aria-modal="true"
|
||||
role="dialog"
|
||||
aria-labelledby={{this.ariaLabelledby}}
|
||||
...attributes
|
||||
{{did-insert this.setupListeners}}
|
||||
{{will-destroy this.cleanupListeners}}
|
||||
{{on "mousedown" this.handleMouseDown}}
|
||||
>
|
||||
<div class="modal-outer-container">
|
||||
<div class="modal-middle-container">
|
||||
<div class="modal-inner-container">
|
||||
<PluginOutlet @name="above-modal-header" @connectorTagName="div" />
|
||||
<div class="modal-header {{this.headerClass}}">
|
||||
{{#if this.dismissable}}
|
||||
<DButton
|
||||
@icon="times"
|
||||
@action={{route-action "closeModal" "initiatedByCloseButton"}}
|
||||
@class="btn-flat modal-close close"
|
||||
@title="modal.close"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.subtitle}}
|
||||
<p class="subtitle">{{this.subtitle}}</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if this.title}}
|
||||
<div class="title">
|
||||
<h3 id="discourse-modal-title">{{this.title}}</h3>
|
||||
|
||||
{{#if this.panels}}
|
||||
<ul class="modal-tabs">
|
||||
{{#each this.panels as |panel|}}
|
||||
<ModalTab
|
||||
@panel={{panel}}
|
||||
@panelsLength={{this.panels.length}}
|
||||
@selectedPanel={{this.selectedPanel}}
|
||||
@onSelectPanel={{this.onSelectPanel}}
|
||||
/>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if this.subtitle}}
|
||||
<p class="subtitle">{{this.subtitle}}</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div id="modal-alert" role="alert"></div>
|
||||
|
||||
{{yield}}
|
||||
|
||||
{{#each this.errors as |error|}}
|
||||
<div class="alert alert-error">
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="alert"
|
||||
aria-label={{i18n "modal.dismiss_error"}}
|
||||
>×</button>
|
||||
{{error}}
|
||||
{{#if this.panels}}
|
||||
<ul class="modal-tabs">
|
||||
{{#each this.panels as |panel|}}
|
||||
<ModalTab
|
||||
@panel={{panel}}
|
||||
@panelsLength={{this.panels.length}}
|
||||
@selectedPanel={{@selectedPanel}}
|
||||
@onSelectPanel={{@onSelectPanel}}
|
||||
/>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
|
||||
<div id="modal-alert" role="alert"></div>
|
||||
|
||||
{{yield}}
|
||||
|
||||
{{#each this.errors as |error|}}
|
||||
<div class="alert alert-error">
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="alert"
|
||||
aria-label={{i18n "modal.dismiss_error"}}
|
||||
>×</button>
|
||||
{{error}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,80 +1,102 @@
|
|||
import {
|
||||
attributeBindings,
|
||||
classNameBindings,
|
||||
} from "@ember-decorators/component";
|
||||
import Component from "@ember/component";
|
||||
import Component from "@glimmer/component";
|
||||
import I18n from "I18n";
|
||||
import { next, schedule } from "@ember/runloop";
|
||||
import discourseComputed, { bind } from "discourse-common/utils/decorators";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import { disableImplicitInjections } from "discourse/lib/implicit-injections";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { action } from "@ember/object";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
|
||||
@classNameBindings(
|
||||
":modal",
|
||||
":d-modal",
|
||||
"modalClass",
|
||||
"modalStyle",
|
||||
"hasPanels"
|
||||
)
|
||||
@attributeBindings(
|
||||
"dataKeyboard:data-keyboard",
|
||||
"ariaModal:aria-modal",
|
||||
"role",
|
||||
"ariaLabelledby:aria-labelledby"
|
||||
)
|
||||
@disableImplicitInjections
|
||||
export default class DModal extends Component {
|
||||
submitOnEnter = true;
|
||||
dismissable = true;
|
||||
title = null;
|
||||
titleAriaElementId = null;
|
||||
subtitle = null;
|
||||
role = "dialog";
|
||||
headerClass = null;
|
||||
@service appEvents;
|
||||
|
||||
// // We handle ESC ourselves
|
||||
dataKeyboard = "false";
|
||||
// // Inform screen readers of the modal
|
||||
ariaModal = "true";
|
||||
@tracked wrapperElement;
|
||||
@tracked modalBodyData = {};
|
||||
|
||||
init() {
|
||||
super.init(...arguments);
|
||||
|
||||
// If we need to render a second modal for any reason, we can't
|
||||
// use `elementId`
|
||||
if (this.modalStyle !== "inline-modal") {
|
||||
this.set("elementId", "discourse-modal");
|
||||
this.set("modalStyle", "fixed-modal");
|
||||
get modalStyle() {
|
||||
if (this.args.modalStyle === "inline-modal") {
|
||||
return "inline-modal";
|
||||
} else {
|
||||
return "fixed-modal";
|
||||
}
|
||||
}
|
||||
|
||||
didInsertElement() {
|
||||
super.didInsertElement(...arguments);
|
||||
get submitOnEnter() {
|
||||
if ("submitOnEnter" in this.modalBodyData) {
|
||||
return this.modalBodyData.submitOnEnter;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
this.appEvents.on("modal:body-shown", this, "_modalBodyShown");
|
||||
get dismissable() {
|
||||
if ("dismissable" in this.modalBodyData) {
|
||||
return this.modalBodyData.dismissable;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
get title() {
|
||||
if (this.modalBodyData.title) {
|
||||
return I18n.t(this.modalBodyData.title);
|
||||
} else if (this.modalBodyData.rawTitle) {
|
||||
return this.modalBodyData.rawTitle;
|
||||
} else {
|
||||
return this.args.title;
|
||||
}
|
||||
}
|
||||
|
||||
get subtitle() {
|
||||
if (this.modalBodyData.subtitle) {
|
||||
return I18n.t(this.modalBodyData.subtitle);
|
||||
}
|
||||
|
||||
return this.modalBodyData.rawSubtitle || this.args.subtitle;
|
||||
}
|
||||
|
||||
get headerClass() {
|
||||
return this.modalBodyData.headerClass;
|
||||
}
|
||||
|
||||
get panels() {
|
||||
return this.args.panels;
|
||||
}
|
||||
|
||||
get errors() {
|
||||
return this.args.errors;
|
||||
}
|
||||
|
||||
@action
|
||||
setupListeners(element) {
|
||||
this.appEvents.on("modal:body-shown", this._modalBodyShown);
|
||||
document.documentElement.addEventListener(
|
||||
"keydown",
|
||||
this._handleModalEvents
|
||||
);
|
||||
this.wrapperElement = element;
|
||||
}
|
||||
|
||||
willDestroyElement() {
|
||||
super.willDestroyElement(...arguments);
|
||||
|
||||
this.appEvents.off("modal:body-shown", this, "_modalBodyShown");
|
||||
@action
|
||||
cleanupListeners() {
|
||||
this.appEvents.off("modal:body-shown", this._modalBodyShown);
|
||||
document.documentElement.removeEventListener(
|
||||
"keydown",
|
||||
this._handleModalEvents
|
||||
);
|
||||
}
|
||||
|
||||
@discourseComputed("title", "titleAriaElementId")
|
||||
ariaLabelledby(title, titleAriaElementId) {
|
||||
if (titleAriaElementId) {
|
||||
return titleAriaElementId;
|
||||
}
|
||||
if (title) {
|
||||
get ariaLabelledby() {
|
||||
if (this.args.titleAriaElementId) {
|
||||
return this.args.titleAriaElementId;
|
||||
} else if (this.args.title) {
|
||||
return "discourse-modal-title";
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
get modalClass() {
|
||||
return this.modalBodyData.modalClass || this.args.modalClass;
|
||||
}
|
||||
|
||||
triggerClickOnEnter(e) {
|
||||
|
@ -93,7 +115,8 @@ export default class DModal extends Component {
|
|||
return true;
|
||||
}
|
||||
|
||||
mouseDown(e) {
|
||||
@action
|
||||
handleMouseDown(e) {
|
||||
if (!this.dismissable) {
|
||||
return;
|
||||
}
|
||||
|
@ -105,46 +128,21 @@ export default class DModal extends Component {
|
|||
// Send modal close (which bubbles to ApplicationRoute) if clicked outside.
|
||||
// We do this because some CSS of ours seems to cover the backdrop and makes
|
||||
// it unclickable.
|
||||
return this.attrs.closeModal?.("initiatedByClickOut");
|
||||
return this.args.closeModal?.("initiatedByClickOut");
|
||||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
_modalBodyShown(data) {
|
||||
if (this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.fixed) {
|
||||
this.element.classList.remove("hidden");
|
||||
this.wrapperElement.classList.remove("hidden");
|
||||
}
|
||||
|
||||
if (data.title) {
|
||||
this.set("title", I18n.t(data.title));
|
||||
} else if (data.rawTitle) {
|
||||
this.set("title", data.rawTitle);
|
||||
}
|
||||
|
||||
if (data.subtitle) {
|
||||
this.set("subtitle", I18n.t(data.subtitle));
|
||||
} else if (data.rawSubtitle) {
|
||||
this.set("subtitle", data.rawSubtitle);
|
||||
} else {
|
||||
// if no subtitle provided, makes sure the previous subtitle
|
||||
// of another modal is not used
|
||||
this.set("subtitle", null);
|
||||
}
|
||||
|
||||
if ("submitOnEnter" in data) {
|
||||
this.set("submitOnEnter", data.submitOnEnter);
|
||||
}
|
||||
|
||||
if ("dismissable" in data) {
|
||||
this.set("dismissable", data.dismissable);
|
||||
} else {
|
||||
this.set("dismissable", true);
|
||||
}
|
||||
|
||||
this.set("headerClass", data.headerClass || null);
|
||||
this.modalBodyData = data;
|
||||
|
||||
schedule("afterRender", () => {
|
||||
this._trapTab();
|
||||
|
@ -153,16 +151,16 @@ export default class DModal extends Component {
|
|||
|
||||
@bind
|
||||
_handleModalEvents(event) {
|
||||
if (this.element.classList.contains("hidden")) {
|
||||
if (this.wrapperElement.classList.contains("hidden")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "Escape" && this.dismissable) {
|
||||
next(() => this.attrs.closeModal("initiatedByESC"));
|
||||
next(() => this.args.closeModal("initiatedByESC"));
|
||||
}
|
||||
|
||||
if (event.key === "Enter" && this.triggerClickOnEnter(event)) {
|
||||
this.element.querySelector(".modal-footer .btn-primary")?.click();
|
||||
this.wrapperElement.querySelector(".modal-footer .btn-primary")?.click();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
|
@ -172,11 +170,13 @@ export default class DModal extends Component {
|
|||
}
|
||||
|
||||
_trapTab(event) {
|
||||
if (this.element.classList.contains("hidden")) {
|
||||
if (this.wrapperElement.classList.contains("hidden")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const innerContainer = this.element.querySelector(".modal-inner-container");
|
||||
const innerContainer = this.wrapperElement.querySelector(
|
||||
".modal-inner-container"
|
||||
);
|
||||
if (!innerContainer) {
|
||||
return;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user