UNPKG

@gitlab/ui

Version:
281 lines (273 loc) • 9.95 kB
import { extend } from '../../vue'; import { NAME_TOOLTIP } from '../../constants/components'; import { EVENT_NAME_OPEN, EVENT_NAME_CLOSE, EVENT_NAME_DISABLE, EVENT_NAME_ENABLE, EVENT_NAME_SHOW, EVENT_NAME_SHOWN, EVENT_NAME_HIDE, EVENT_NAME_HIDDEN, EVENT_NAME_DISABLED, EVENT_NAME_ENABLED, MODEL_EVENT_NAME_PREFIX } from '../../constants/events'; import { PROP_TYPE_OBJECT, PROP_TYPE_STRING, PROP_TYPE_NUMBER_STRING, PROP_TYPE_NUMBER_OBJECT_STRING, PROP_TYPE_BOOLEAN, PROP_TYPE_ARRAY_STRING, PROP_TYPE_FUNCTION } from '../../constants/props'; import { HTMLElement, SVGElement } from '../../constants/safe-types'; import { useParentMixin } from '../../mixins/use-parent'; import { getScopeId } from '../../utils/get-scope-id'; import { isUndefinedOrNull } from '../../utils/inspect'; import { pick } from '../../utils/object'; import { makePropsConfigurable, makeProp } from '../../utils/props'; import { createNewChildComponent } from '../../utils/create-new-child-component'; import { normalizeSlotMixin } from '../../mixins/normalize-slot'; import { BVTooltip } from './helpers/bv-tooltip'; // --- Constants --- const MODEL_PROP_NAME_ENABLED = 'disabled'; const MODEL_EVENT_NAME_ENABLED = MODEL_EVENT_NAME_PREFIX + MODEL_PROP_NAME_ENABLED; const MODEL_PROP_NAME_SHOW = 'show'; const MODEL_EVENT_NAME_SHOW = MODEL_EVENT_NAME_PREFIX + MODEL_PROP_NAME_SHOW; // --- Props --- const props = makePropsConfigurable({ // String: scrollParent, window, or viewport // Element: element reference // Object: Vue component boundary: makeProp([HTMLElement, PROP_TYPE_OBJECT, PROP_TYPE_STRING], 'scrollParent'), boundaryPadding: makeProp(PROP_TYPE_NUMBER_STRING, 50), // String: HTML ID of container, if null body is used (default) // HTMLElement: element reference reference // Object: Vue Component container: makeProp([HTMLElement, PROP_TYPE_OBJECT, PROP_TYPE_STRING]), customClass: makeProp(PROP_TYPE_STRING), delay: makeProp(PROP_TYPE_NUMBER_OBJECT_STRING, 50), [MODEL_PROP_NAME_ENABLED]: makeProp(PROP_TYPE_BOOLEAN, false), fallbackPlacement: makeProp(PROP_TYPE_ARRAY_STRING, 'flip'), // ID to use for tooltip element // If not provided on will automatically be generated id: makeProp(PROP_TYPE_STRING), noFade: makeProp(PROP_TYPE_BOOLEAN, false), noninteractive: makeProp(PROP_TYPE_BOOLEAN, false), offset: makeProp(PROP_TYPE_NUMBER_STRING, 0), placement: makeProp(PROP_TYPE_STRING, 'top'), [MODEL_PROP_NAME_SHOW]: makeProp(PROP_TYPE_BOOLEAN, false), // String ID of element, or element/component reference // Or function that returns one of the above // Required target: makeProp([HTMLElement, SVGElement, PROP_TYPE_FUNCTION, PROP_TYPE_OBJECT, PROP_TYPE_STRING], undefined, true), title: makeProp(PROP_TYPE_STRING), triggers: makeProp(PROP_TYPE_ARRAY_STRING, 'hover focus'), variant: makeProp(PROP_TYPE_STRING) }, NAME_TOOLTIP); // --- Main component --- // @vue/component const BTooltip = /*#__PURE__*/extend({ name: NAME_TOOLTIP, mixins: [normalizeSlotMixin, useParentMixin], inheritAttrs: false, props, data() { return { localShow: this[MODEL_PROP_NAME_SHOW], localTitle: '', localContent: '' }; }, computed: { // Data that will be passed to the template and popper templateData() { return { title: this.localTitle, content: this.localContent, interactive: !this.noninteractive, // Pass these props as is ...pick(this.$props, ['boundary', 'boundaryPadding', 'container', 'customClass', 'delay', 'fallbackPlacement', 'id', 'noFade', 'offset', 'placement', 'target', 'target', 'triggers', 'variant', MODEL_PROP_NAME_ENABLED]) }; }, // Used to watch for changes to the title and content props templateTitleContent() { const { title, content } = this; return { title, content }; } }, watch: { [MODEL_PROP_NAME_SHOW](newValue, oldValue) { if (newValue !== oldValue && newValue !== this.localShow && this.$_toolpop) { if (newValue) { this.$_toolpop.show(); } else { // We use `forceHide()` to override any active triggers this.$_toolpop.forceHide(); } } }, [MODEL_PROP_NAME_ENABLED](newValue) { if (newValue) { this.doDisable(); } else { this.doEnable(); } }, localShow(newValue) { // TODO: May need to be done in a `$nextTick()` this.$emit(MODEL_EVENT_NAME_SHOW, newValue); }, templateData() { this.$nextTick(() => { if (this.$_toolpop) { this.$_toolpop.updateData(this.templateData); } }); }, // Watchers for title/content props (prop changes do not trigger the `updated()` hook) templateTitleContent() { this.$nextTick(this.updateContent); } }, created() { // Create private non-reactive props this.$_toolpop = null; }, updated() { // Update the `propData` object // Done in a `$nextTick()` to ensure slot(s) have updated this.$nextTick(this.updateContent); }, beforeDestroy() { // Shutdown our local event listeners this.$off(EVENT_NAME_OPEN, this.doOpen); this.$off(EVENT_NAME_CLOSE, this.doClose); this.$off(EVENT_NAME_DISABLE, this.doDisable); this.$off(EVENT_NAME_ENABLE, this.doEnable); // Destroy the tip instance if (this.$_toolpop) { this.$_toolpop.$destroy(); this.$_toolpop = null; } }, mounted() { // Instantiate a new BVTooltip instance // Done in a `$nextTick()` to ensure DOM has completed rendering // so that target can be found this.$nextTick(() => { // Load the on demand child instance const Component = this.getComponent(); // Ensure we have initial content this.updateContent(); // Pass down the scoped style attribute if available const scopeId = getScopeId(this) || getScopeId(this.bvParent); // Create the instance const $toolpop = this.$_toolpop = createNewChildComponent(this, Component, { // Pass down the scoped style ID _scopeId: scopeId || undefined }); // Set the initial data $toolpop.updateData(this.templateData); // Set listeners $toolpop.$on(EVENT_NAME_SHOW, this.onShow); $toolpop.$on(EVENT_NAME_SHOWN, this.onShown); $toolpop.$on(EVENT_NAME_HIDE, this.onHide); $toolpop.$on(EVENT_NAME_HIDDEN, this.onHidden); $toolpop.$on(EVENT_NAME_DISABLED, this.onDisabled); $toolpop.$on(EVENT_NAME_ENABLED, this.onEnabled); // Initially disabled? if (this[MODEL_PROP_NAME_ENABLED]) { // Initially disabled this.doDisable(); } // Listen to open signals from others this.$on(EVENT_NAME_OPEN, this.doOpen); // Listen to close signals from others this.$on(EVENT_NAME_CLOSE, this.doClose); // Listen to disable signals from others this.$on(EVENT_NAME_DISABLE, this.doDisable); // Listen to enable signals from others this.$on(EVENT_NAME_ENABLE, this.doEnable); // Initially show tooltip? if (this.localShow) { $toolpop.show(); } }); }, methods: { getComponent() { // Overridden by BPopover return BVTooltip; }, updateContent() { // Overridden by BPopover // Tooltip: Default slot is `title` // Popover: Default slot is `content`, `title` slot is title // We pass a scoped slot function reference by default (Vue v2.6x) // And pass the title prop as a fallback this.setTitle(this.normalizeSlot() || this.title); }, // Helper methods for `updateContent()` setTitle(value) { value = isUndefinedOrNull(value) ? '' : value; // We only update the value if it has changed if (this.localTitle !== value) { this.localTitle = value; } }, setContent(value) { value = isUndefinedOrNull(value) ? '' : value; // We only update the value if it has changed if (this.localContent !== value) { this.localContent = value; } }, // --- Template event handlers --- onShow(bvEvent) { // Placeholder this.$emit(EVENT_NAME_SHOW, bvEvent); if (bvEvent) { this.localShow = !bvEvent.defaultPrevented; } }, onShown(bvEvent) { // Tip is now showing this.localShow = true; this.$emit(EVENT_NAME_SHOWN, bvEvent); }, onHide(bvEvent) { this.$emit(EVENT_NAME_HIDE, bvEvent); }, onHidden(bvEvent) { // Tip is no longer showing this.$emit(EVENT_NAME_HIDDEN, bvEvent); this.localShow = false; }, onDisabled(bvEvent) { // Prevent possible endless loop if user mistakenly // fires `disabled` instead of `disable` if (bvEvent && bvEvent.type === EVENT_NAME_DISABLED) { this.$emit(MODEL_EVENT_NAME_ENABLED, true); this.$emit(EVENT_NAME_DISABLED, bvEvent); } }, onEnabled(bvEvent) { // Prevent possible endless loop if user mistakenly // fires `enabled` instead of `enable` if (bvEvent && bvEvent.type === EVENT_NAME_ENABLED) { this.$emit(MODEL_EVENT_NAME_ENABLED, false); this.$emit(EVENT_NAME_ENABLED, bvEvent); } }, // --- Local event listeners --- doOpen() { !this.localShow && this.$_toolpop && this.$_toolpop.show(); }, doClose() { this.localShow && this.$_toolpop && this.$_toolpop.hide(); }, doDisable() { this.$_toolpop && this.$_toolpop.disable(); }, doEnable() { this.$_toolpop && this.$_toolpop.enable(); } }, render(h) { // Always renders a comment node // TODO: // Future: Possibly render a target slot (single root element) // which we can apply the listeners to (pass `this.$el` to BVTooltip) return h(); } }); export { BTooltip, props };