|
| 1 | +import Component from '@glimmer/component'; |
| 2 | +import { inject as service } from '@ember/service'; |
| 3 | +import { dasherize } from '@ember/string'; |
| 4 | +import { typeOf } from '@ember/utils'; |
| 5 | +import { assert, warn } from '@ember/debug'; |
| 6 | +import { DEBUG } from '@glimmer/env'; |
| 7 | +import { importSync } from '@embroider/macros'; |
| 8 | +import { ensureSafeComponent } from '@embroider/util'; |
| 9 | +import { guidFor } from '@ember/object/internals'; |
| 10 | + |
| 11 | +const VALID_OVERLAY_POSITIONS = ['parent', 'sibling']; |
| 12 | + |
| 13 | +// Args: |
| 14 | +// `hasOverlay` | Toggles presence of overlay div in DOM |
| 15 | +// `translucentOverlay` | Indicates translucence of overlay, toggles presence of `translucent` CSS selector |
| 16 | +// `onClose` | The action handler for the dialog's `onClose` action. This action triggers when the user clicks the modal overlay. |
| 17 | +// `onClickOverlay` | An action to be called when the overlay is clicked. If this action is specified, clicking the overlay will invoke it instead of `onClose`. |
| 18 | +// `clickOutsideToClose` | Indicates whether clicking outside a modal *without* an overlay should close the modal. Useful if your modal isn't the focus of interaction, and you want hover effects to still work outside the modal. |
| 19 | +// `renderInPlace` | A boolean, when true renders the modal without wormholing or tethering, useful for including a modal in a style guide |
| 20 | +// `overlayPosition` | either `'parent'` or `'sibling'`, to control whether the overlay div is rendered as a parent element of the container div or as a sibling to it (default: `'parent'`) |
| 21 | +// `containerClass` | CSS class name(s) to append to container divs. Set this from template. |
| 22 | +// `containerClassNames` | CSS class names to append to container divs. If you subclass this component, you may define this in your subclass.) |
| 23 | +// `overlayClass` | CSS class name(s) to append to overlay divs. Set this from template. |
| 24 | +// `overlayClassNames` | CSS class names to append to overlay divs. If you subclass this component, you may define this in your subclass.) |
| 25 | +// `wrapperClass` | CSS class name(s) to append to wrapper divs. Set this from template. |
| 26 | +// `wrapperClassNames` | CSS class names to append to wrapper divs. If you subclass this component, you may define this in your subclass.) |
| 27 | +// `animatable` | A boolean, when `true` makes modal animatable using `liquid-fire` (requires `liquid-wormhole` to be installed, and for tethering situations `liquid-tether`. Having these optional dependencies installed and not specifying `animatable` will make `animatable=true` be the default.) |
| 28 | +// `tetherTarget` | If you specify a `tetherTarget`, you are opting into "tethering" behavior, and you must have either `ember-tether` or `liquid-tether` installed. |
| 29 | +// `destinationElementId`| optional |
| 30 | +// `targetAttachment` | Delegates to Hubspot Tether* |
| 31 | +// `tetherClassPrefix` | Delegates to Hubspot Tether* |
| 32 | +// `offset` | Delegates to Hubspot Tether* |
| 33 | +// `targetOffset` | Delegates to Hubspot Tether* |
| 34 | +// `constraints` | Delegates to Hubspot Tether* |
| 35 | +// `stack` | Delegates to liquid-wormhole/liquid-tether |
| 36 | +// `value` | pass a `value` to set a "value" to be passed to liquid-wormhole / liquid-tether |
| 37 | + |
| 38 | +export default class ModalDialog extends Component { |
| 39 | + @service('modal-dialog') modalService; |
| 40 | + |
| 41 | + get value() { |
| 42 | + // pass a `value` to set a "value" to be passed to liquid-wormhole / liquid-tether |
| 43 | + return this.args.value || 0; |
| 44 | + } |
| 45 | + get hasLiquidWormhole() { |
| 46 | + return this.modalService.hasLiquidWormhole; |
| 47 | + } |
| 48 | + |
| 49 | + get hasLiquidTether() { |
| 50 | + return this.modalService.hasLiquidTether; |
| 51 | + } |
| 52 | + |
| 53 | + get hasOverlay() { |
| 54 | + return this.args.hasOverlay ?? true; |
| 55 | + } |
| 56 | + |
| 57 | + get stack() { |
| 58 | + // this `stack` string will be set as this element's ID and passed to liquid-wormhole / liquid-tether |
| 59 | + return guidFor(this); |
| 60 | + } |
| 61 | + |
| 62 | + get containerClassNamesVal() { |
| 63 | + return this.args.containerClassNames || this.containerClassNames || null; |
| 64 | + } |
| 65 | + |
| 66 | + get attachmentClass() { |
| 67 | + let { attachment } = this.args; |
| 68 | + if (!attachment) { |
| 69 | + return undefined; |
| 70 | + } |
| 71 | + return attachment |
| 72 | + .split(' ') |
| 73 | + .map((attachmentPart) => { |
| 74 | + return `emd-attachment-${dasherize(attachmentPart)}`; |
| 75 | + }) |
| 76 | + .join(' '); |
| 77 | + } |
| 78 | + |
| 79 | + get targetAttachment() { |
| 80 | + return this.args.targetAttachment || 'middle center'; |
| 81 | + } |
| 82 | + |
| 83 | + get whichModalDialogComponent() { |
| 84 | + let { hasLiquidTether, hasLiquidWormhole } = this; |
| 85 | + let { animatable, tetherTarget, renderInPlace } = this.args; |
| 86 | + let module = importSync('ember-modal-dialog/components/basic-dialog'); |
| 87 | + |
| 88 | + if (renderInPlace) { |
| 89 | + module = importSync('ember-modal-dialog/components/in-place-dialog'); |
| 90 | + } else if ( |
| 91 | + tetherTarget && |
| 92 | + hasLiquidTether && |
| 93 | + hasLiquidWormhole && |
| 94 | + animatable === true |
| 95 | + ) { |
| 96 | + module = importSync('ember-modal-dialog/components/liquid-tether-dialog'); |
| 97 | + } else if (tetherTarget) { |
| 98 | + this.ensureEmberTetherPresent(); |
| 99 | + module = importSync('ember-modal-dialog/components/tether-dialog'); |
| 100 | + } else if (hasLiquidWormhole && animatable === true) { |
| 101 | + module = importSync('ember-modal-dialog/components/liquid-dialog'); |
| 102 | + } |
| 103 | + |
| 104 | + return ensureSafeComponent(module.default, this); |
| 105 | + } |
| 106 | + |
| 107 | + get destinationElementId() { |
| 108 | + return ( |
| 109 | + this.args.destinationElementId || this.modalService.destinationElementId |
| 110 | + ); |
| 111 | + } |
| 112 | + |
| 113 | + validateProps() { |
| 114 | + let overlayPosition = this.overlayPosition; |
| 115 | + if (VALID_OVERLAY_POSITIONS.indexOf(overlayPosition) === -1) { |
| 116 | + warn( |
| 117 | + `overlayPosition value '${overlayPosition}' is not valid (valid values [${VALID_OVERLAY_POSITIONS.join( |
| 118 | + ', ' |
| 119 | + )}])`, |
| 120 | + false, |
| 121 | + { id: 'ember-modal-dialog.validate-overlay-position' } |
| 122 | + ); |
| 123 | + } |
| 124 | + } |
| 125 | + |
| 126 | + get overlayPosition() { |
| 127 | + let result = this.args.overlayPosition || 'parent'; |
| 128 | + if (DEBUG && VALID_OVERLAY_POSITIONS.indexOf(result) === -1) { |
| 129 | + warn( |
| 130 | + `overlayPosition value '${result}' is not valid (valid values [${VALID_OVERLAY_POSITIONS.join( |
| 131 | + ', ' |
| 132 | + )}])`, |
| 133 | + false, |
| 134 | + { id: 'ember-modal-dialog.validate-overlay-position' } |
| 135 | + ); |
| 136 | + } |
| 137 | + return result; |
| 138 | + } |
| 139 | + |
| 140 | + ensureEmberTetherPresent() { |
| 141 | + if (!this.modalService.hasEmberTether) { |
| 142 | + throw new Error( |
| 143 | + 'Please install ember-tether in order to pass a tetherTarget to modal-dialog' |
| 144 | + ); |
| 145 | + } |
| 146 | + } |
| 147 | + |
| 148 | + onCloseAction = () => { |
| 149 | + const { onClose } = this.args; |
| 150 | + // we shouldn't warn if the callback is not provided at all |
| 151 | + if (!onClose) { |
| 152 | + return; |
| 153 | + } |
| 154 | + |
| 155 | + assert( |
| 156 | + 'onClose handler must be a function', |
| 157 | + typeOf(onClose) === 'function' |
| 158 | + ); |
| 159 | + |
| 160 | + onClose(); |
| 161 | + }; |
| 162 | + |
| 163 | + onClickOverlayAction = (ev) => { |
| 164 | + ev.preventDefault(); |
| 165 | + |
| 166 | + const { onClickOverlay } = this.args; |
| 167 | + // we shouldn't warn if the callback is not provided at all |
| 168 | + if (!onClickOverlay) { |
| 169 | + this.onCloseAction(); |
| 170 | + return; |
| 171 | + } |
| 172 | + |
| 173 | + assert( |
| 174 | + 'onClickOverlay handler must be a function', |
| 175 | + typeOf(onClickOverlay) === 'function' |
| 176 | + ); |
| 177 | + |
| 178 | + onClickOverlay(); |
| 179 | + }; |
| 180 | +} |
0 commit comments