Merge branch 'develop' into feature/SSR-113

This commit is contained in:
Kulbhushan Kaushik 2022-11-09 12:34:55 -05:00
commit a901892227
18 changed files with 747 additions and 42 deletions

View file

@ -152,7 +152,7 @@ describe("dropdownQuestion.vue", () => {
const wrapper = shallowMount(dropdownQuestion, {
propsData: {
options: {},
modelValue: 0,
modelValue: "0",
},
mixins: [mockMixin]
});

View file

@ -37,6 +37,8 @@ export default {
disableAutoFill: Boolean,
validationRules: String,
cmsWidgetName: String,
hasError: Boolean,
errors: Array
},
setup(props) {
const propsClone = Object.assign({}, props);

View file

@ -1,25 +1,25 @@
import siteHeader from "@/common-components/site-header/site-header";
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
describe("site-header", () => {
test("renders the logo image", () => {
const wrapper = shallowMount(siteHeader, {
setProps: {
cmsWidgetName: "header",
},
mixins: [mockMixin]
});
const wrapper = setupMocks({mountOptionsMockData: {}});
expect(wrapper.find("img")).toBeTruthy();
wrapper.unmount();
});
});
const mockMixin = {
methods: {
getCmsContent: jest.fn()
}
function setupMocks({
mountOptionsMockData = {},
}) {
const mountOptions = getMountOptions(mountOptionsMockData);
const wrapper = shallowMount(siteHeader, mountOptions);
return wrapper;
}

View file

@ -1,14 +1,39 @@
<template>
<div class="site-header d-flex justify-content-center align-items-center flex-column">
<img id="siteHeaderImage" class="img-fluid" :src="imageSrc" :alt="altText" />
<menuModal/>
<div>
<div class="site-header d-flex justify-content-center align-items-center flex-column">
<img id="siteHeaderImage" class="img-fluid" :src="imageSrc" :alt="altText" />
<menuModal/>
</div>
<alert
v-if="displayGlobalAlert"
class="position-absolute rounded-0 w-100 border-0 shadow-sm start-0"
cmsWidgetName="GlobalAlert"
:manualHeadline="globalAlertMessage.messageHeadline"
:manualCopy="globalAlertMessage.messageCopy"
:alertClass="globalAlertMessage.type"
v-bind:isDismissible="globalAlertMessage.isDismissible" />
</div>
</template>
<script>
import menuModal from "@/common-components/site-header/menu-modal/menu-modal";
import alert from "@/ux-components/alert/alert";
import eventBus from "@/helpers/event-bus/event-bus";
import { globalEvents } from "@/constants/events";
export default({
name: "siteHeader",
data() {
return {
displayGlobalAlert: false,
globalAlertMessage: {
isDismissible: false,
messageCopy: "",
messageHeadline: "",
type: "",
},
};
},
props: {
cmsWidgetName: String,
},
@ -22,8 +47,21 @@ import menuModal from "@/common-components/site-header/menu-modal/menu-modal";
return this.getCmsContent(this.cmsWidgetName, "AltText")
}
},
mounted() {
// Check if alert event is on the bus
const alertEvent = eventBus.readAndPopEventFromBus(
globalEvents.Categories.GLOBAL_ALERT,
globalEvents.SubCategories.PAGE_NOT_FOUND
);
// If alert event is on the bus, then display the alert
if (alertEvent !== undefined) {
this.displayGlobalAlert = true;
this.globalAlertMessage = alertEvent;
}
},
components: {
menuModal
menuModal,
alert
},
})
</script>

View file

@ -55,6 +55,7 @@ export default {
validationRules: String,
cmsWidgetName: String,
maxLength: String,
disableAutoFill: Boolean,
},
setup(props) {
const propsClone = Object.assign({}, props);

View file

@ -9,7 +9,6 @@
</template>
<script>
import { useMainStore } from "@/store";
export default {
name: "vehicle-banner",
props: {
@ -25,7 +24,7 @@ export default {
return this.genericVehicleImage;
}
const imageUrl = useMainStore().order.vehicle.imageUrl;
const imageUrl = this.mainStore.order.vehicle.imageUrl;
if (!imageUrl || imageUrl === "NULL"){
return this.getUnmatchedVehicleIcon();
}
@ -53,7 +52,7 @@ export default {
},
methods: {
getUnmatchedVehicleIcon(){
switch(useMainStore().order.vehicle.category){
switch(this.mainStore.order.vehicle.category){
case this.vehicleCategories.CAR:
return this.carUnmatchedVehicleIcon;
case this.vehicleCategories.SUV:

17
src/constants/events.js Normal file
View file

@ -0,0 +1,17 @@
const globalEvents = {
Categories: {
GLOBAL_ALERT: "GLOBAL_ALERT",
},
SubCategories: {
PAGE_NOT_FOUND: "PAGE_NOT_FOUND",
},
};
const globalEventTypes = {
Success: "alert-success",
Warning: "alert-warning",
Info: "alert-info",
Danger: "alert-danger",
};
export { globalEvents, globalEventTypes };

View file

@ -118,4 +118,36 @@ function mapStringToState(str) {
}
return stringBuilder.trimStart();
}
}
export function doesCopyContainRouterLink(copy) {
return copy.includes(this.dynamicStrings.ROUTER_LINK);
}
export function splitCopyOnCMSPlaceHolder(copy) {
// splits copy on { ... } such as {routerlink: ...}
return copy.split(/{(.*?)}/g);
}
export function getRouterLinkRouteFromCopy(copy) {
// sample input: {routerLink:estimate,provide your VIN}
// first split would return 'estimate,provide your VIN'
// second split would return 'estimate'
return copy.split(":")[1].split(",")[0];
}
export function getRouterLinkDisplayTextFromCopy(copy) {
// sample input: {routerLink:estimate,provide your VIN}
// first split would return 'estimate,provide your VIN'
// second split would return 'provide your VIN'
return copy.split(":")[1].split(",")[1];
}
// Copy returned from the CMS that has newlines will return blocks wrapped in
// <p ... >...</p>
// This function returns an array of each paragraph, works with or without html
// attributes present
export function splitCMSCopyOnParagraphTag(copy) {
// filter removes empty strings that are a result of string.split with regex
return copy.split(/(?:<p(?:.*?)>)|(?:<\/p>)/g).filter((paragraph) => paragraph !== "");
}

View file

@ -0,0 +1,35 @@
import { useMainStore } from '@/store';
export default {
// Adds event to the bus given its category, subcategory, and eventValue;
addEventToBus(category, subCategory, eventValue) {
useMainStore().addEventToBus( {
category: category,
subCategory: subCategory,
eventValue: eventValue,
});
},
// Finds event on the bus, removes the item, and returns its value to the caller.
readAndPopEventFromBus(category, subCategory) {
const event = useMainStore().eventBusItem(category, subCategory);
if(event) {
useMainStore().removeEventFromBus( {
category: category,
subCategory: subCategory,
});
}
return event;
},
// Finds the event on the bus and returns its value to the caller, does not remove it.
readEventFromBus(category, subCategory) {
const event = useMainStore().eventBusItem(category, subCategory);
return event;
},
};

View file

@ -0,0 +1,78 @@
import { globalEvents, globalEventTypes } from "@/constants/events";
import eventBus from "@/helpers/event-bus/event-bus";
import { useMainStore } from '@/store';
import { createTestingPinia } from '@pinia/testing';
const pinia = createTestingPinia();
useMainStore(pinia);
useMainStore().addEventToBus = jest.fn();
useMainStore().removeEventFromBus = jest.fn();
useMainStore().eventBusItem = jest.fn();
describe("event-bus.js", () => {
let event = {
isDismissible: true,
messageCopy: "You can get a quote by starting on this page.",
messageHeadline: "We're sorry, something went wrong.",
type: globalEventTypes.Danger,
};
afterEach(() => {
jest.resetAllMocks();
})
it("removes items when readandpop is called", () => {
useMainStore().eventBusItem.mockReturnValueOnce(event);
const eventValue = eventBus.readAndPopEventFromBus(
globalEvents.Categories.GLOBAL_ALERT,
globalEvents.SubCategories.PAGE_NOT_FOUND
);
expect(useMainStore().eventBusItem).toBeCalledTimes(1);
expect(useMainStore().removeEventFromBus).toBeCalledTimes(1);
});
it("doesn't try to remove items when readandpop is called and item doesn't exist", () => {
useMainStore().eventBusItem.mockReturnValueOnce(undefined);
const eventValue = eventBus.readAndPopEventFromBus(
globalEvents.Categories.GLOBAL_ALERT,
globalEvents.SubCategories.PAGE_NOT_FOUND
);
expect(useMainStore().eventBusItem).toBeCalledTimes(1);
expect(useMainStore().removeEventFromBus).toBeCalledTimes(0);
});
it("returns event from bus", () => {
useMainStore().eventBusItem.mockReturnValueOnce(event);
const eventValue = eventBus.readEventFromBus(
globalEvents.Categories.GLOBAL_ALERT,
globalEvents.SubCategories.PAGE_NOT_FOUND
);
expect(eventValue).toBe(event);
});
it("Reads event from bus, should have event value.", () => {
// Arrange / Act
eventBus.addEventToBus(
globalEvents.Categories.GLOBAL_ALERT,
globalEvents.SubCategories.PAGE_NOT_FOUND,
event
);
expect(useMainStore().addEventToBus).toHaveBeenCalled();
});
});

View file

@ -1,11 +0,0 @@
<template>
<div>
404: Page Not Found
</div>
</template>
<script>
export default {
name: 'error-404'
}
</script>

View file

@ -117,13 +117,13 @@ function setupMocks({
//Mock api responses
const apiResponses = {
cmsContent: {
FunnelSubHeaderWidget: pageHeaderWidgetHeaderText,
SiteSubHeaderWidget: pageHeaderWidgetHeaderText,
VehicleModelQuestion: buttonQuestionContent,
VehicleBannerWidget: {
GenericVehicleImage:
"https://digitalisscms.dev.safelite.io/images/default-source/default-album/blurred-image.jpg",
},
FunnelHeaderWidget: {
SiteHeaderWidget: {
LogoImage:
"https://digitalisscms.dev.safelite.io/images/default-source/default-album/logos/insuranceLogo.jpg",
},

View file

@ -3,6 +3,8 @@ import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader";
import {issPageValues} from '@/router/router-constants/issPage-values';
import { routingTable } from "@/router/router-constants/routing-table";
import { useMainStore } from '@/store';
import eventBus from "@/helpers/event-bus/event-bus";
import { globalEvents, globalEventTypes } from "@/constants/events";
const routes = [
{
@ -141,13 +143,25 @@ function getNavigationMap (scenario, currentRoute) {
};
function GoToStartOn404(next) {
const errorPageName = issPageValues.ERROR_404;
const errorPageName = issPageValues.VEHICLE_YEAR;
router.addRoute({
path: "/",
name: errorPageName,
component: lazyLoadComponent(errorPageName),
});
// Put item on the bus
eventBus.addEventToBus(
globalEvents.Categories.GLOBAL_ALERT,
globalEvents.SubCategories.PAGE_NOT_FOUND,
{
isDismissible: true,
messageCopy: "You can get a quote by starting on this page.",
messageHeadline: "We're sorry, something went wrong.",
type: globalEventTypes.Danger,
}
);
next({
name: errorPageName,
query: {issPage: errorPageName },

View file

@ -1,5 +1,4 @@
export const issPageValues = {
ERROR_404: "error-404",
WELCOME_PAGE: "welcome-page",
VEHICLE_MAKE: "vehicle-make",
VEHICLE_YEAR: "vehicle-year",

View file

@ -37,7 +37,8 @@ const getDefaultState = () => {
crmCustomerId: null,
lastPageVisited: null,
experiments: [],
triggeredSiteEntry: false
triggeredSiteEntry: false,
eventBus: []
},
};
};
@ -49,6 +50,16 @@ export const useMainStore = defineStore({
state: () => state,
getters: {
vehicle: this.order.vehicle
},
eventBusItem: (state) => ( eventCategory, eventSubCategory) => {
const matchedEvent = state.applicationUser.eventBus.find(
( { category, subCategory } ) =>
category === eventCategory && subCategory === eventSubCategory
);
return matchedEvent?.eventValue;
},
eventBus: (state) => state.applicationUser.eventBus,
},
actions:
{
@ -137,6 +148,21 @@ export const useMainStore = defineStore({
this.order.vehicle.style = style;
}
},
addEventToBus (event) {
this.applicationUser.eventBus.push(event);
},
removeEventFromBus (eventData) {
const matchedEvent = this.applicationUser.eventBus.find(
( {category, subCategory } ) =>
category === eventData.category && subCategory === eventData.subCategory
);
const itemIndex = this.applicationUser.eventBus.indexOf(matchedEvent);
// If the item exists, remove it.
if (itemIndex > -1) {
this.applicationUser.eventBus.splice(itemIndex, 1);
}
},
// populate initial state
populateInitialState()

View file

@ -3,19 +3,83 @@ import { createApp } from 'vue';
import { createPinia } from "pinia";
import App from '@/App.vue';
describe("Store", () => {
let store = null;
beforeAll(() => {
const vueApp = createApp(App);
const pinia = createPinia();
vueApp.use(pinia);
describe("Store", () => {
let store;
const vueApp = createApp(App);
const pinia = createPinia();
vueApp.use(pinia);
beforeEach(() => {
store = useMainStore();
store.applicationUser.eventBus = [];
jest.resetAllMocks();
})
it("Should Store Vehicle Year", () => {
let testYear = "2001";
store.updateVehicleYear(testYear);
expect(store.order.vehicle.year).toEqual(testYear);
});
it("Should add events to the bus", () => {
let event = {
category: "TestCategory",
subCategory: "TestSubCategory",
eventValue: {
isDismissible: true,
messageCopy: "You can get a quote by starting on this page.",
messageHeadline: "We're sorry, something went wrong.",
type: "testType",
},
};
store.addEventToBus(event);
expect(store.applicationUser.eventBus[0]).toEqual(event);
});
it("Should remove events from the bus", () => {
let event = {
category: "TestCategory",
subCategory: "TestSubCategory",
eventValue: {
isDismissible: true,
messageCopy: "You can get a quote by starting on this page.",
messageHeadline: "We're sorry, something went wrong.",
type: "testType",
},
};
store.addEventToBus(event);
expect(store.applicationUser.eventBus.length).toBe(1);
store.removeEventFromBus({ category: event.category, subCategory: event.subCategory })
expect(store.applicationUser.eventBus.length).toBe(0);
});
it("Should return correct event using the getter function eventBusItem", () => {
let event = {
category: "TestCategory",
subCategory: "TestSubCategory",
eventValue: {
isDismissible: true,
messageCopy: "You can get a quote by starting on this page.",
messageHeadline: "We're sorry, something went wrong.",
type: "testType",
},
};
store.addEventToBus(event);
const actual = store.eventBusItem(event.category, event.subCategory)
expect(actual).toEqual(event.eventValue);
});
});

View file

@ -0,0 +1,222 @@
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import alert from "./alert";
describe("alert.vue", () => {
it("Should add class 'alert-dismissible' if isDismissible is true", async () => {
// Arrange
const wrapper = shallowMount(
alert,
setupMocks({
propsData: {
isDismissible: true,
manualHeadline: "testHeader",
manualCopy: "testCopy",
},
})
);
const wrapperDiv = wrapper.find("div");
// Assert
expect(wrapperDiv.classes()).toContain("alert-dismissible");
});
it("Should add specified alert class", async () => {
// Arrange
const wrapper = shallowMount(
alert,
setupMocks({
propsData: {
alertClass: "warning",
manualHeadline: "testHeader",
manualCopy: "testCopy",
},
})
);
const wrapperDiv = wrapper.find("div");
// Assert
expect(wrapperDiv.classes()).toContain("warning");
});
it("Should update alert Headline to manualHeadline datam entered and alert copy to manualCopy datam entered when no cmsWidgetName entered", async () => {
// Arrange
const wrapper = shallowMount(alert, setupMocks({}));
// Assert
expect(wrapper.vm.alertHeadline).toBe("testHeader");
expect(wrapper.vm.alertCopy).toBe("testCopy");
});
it("Should container a <router-link> tag if the manualCopy contains a {routerLink: testName, testLink} placeholder", () => {
// Arrange & Act
const wrapper = shallowMount(
alert,
setupMocks({
propsData: {
manualHeadline: "testHeader",
manualCopy: "testCopy with a {routerLink: testName, testLink} inside of it",
},
stubs: ["router-link"],
})
);
// Assert
expect(wrapper.find("router-link").exists()).toBe(true);
});
it("Should contain 'n+1' <p> tags if the body copy has 'n' <p> tags", () => {
// Arrange & Act
const wrapper = shallowMount(
alert,
setupMocks({
propsData: {
manualHeadline: "testHeader",
manualCopy:
"<p>testCopy with a {routerLink: testName, testLink} inside of it</p><p>and two paragraphs</p>",
},
stubs: ["router-link"],
})
);
// Assert
expect(wrapper.findAll("p").length === 3).toBe(true);
});
it("Should call scrollIntoView() when the clientBoundingRect is not entirely in the viewport (out of view top)", () => {
// Arrange
var viewPortHeight = 200;
setUpViewPort(viewPortHeight);
Element.prototype.getBoundingClientRect = jest.fn(() => {
return { top: -100, bottom: 200 };
});
var mockScrollIntoView = jest.fn();
Element.prototype.scrollIntoView = mockScrollIntoView;
// Act
const wrapper = shallowMount(alert, setupMocks({}));
// Assert
// This is an implementation detail - we just need to test that the final step of snapping
// the window to the alert is working. If using a different function to accomplish that
// just swap this out with the new function
expect(mockScrollIntoView).toHaveBeenCalled();
});
it("Should call scrollIntoView() when the clientBoundingRect is not entirely in the viewport (bottom is hidden behind footer)", () => {
// Arrange
var viewPortHeight = 240;
setUpViewPort(viewPortHeight);
Element.prototype.getBoundingClientRect = jest.fn(() => {
return { top: 100, bottom: 200 };
});
var mockScrollIntoView = jest.fn();
Element.prototype.scrollIntoView = mockScrollIntoView;
// Act
const wrapper = shallowMount(alert, setupMocks({}));
// Assert
// This is an implementation detail - we just need to test that the final step of snapping
// the window to the alert is working. If using a different function to accomplish that
// just swap this out with the new function
expect(mockScrollIntoView).toHaveBeenCalled();
});
it("Should not call scrollIntoView() when the clientBoundingRect is not entirely in the viewport but 'shouldScrollToOnMount' is false", () => {
// Arrange
var viewPortHeight = 200;
setUpViewPort(viewPortHeight);
Element.prototype.getBoundingClientRect = jest.fn(() => {
return { top: -100, bottom: 200 };
});
var mockScrollIntoView = jest.fn();
Element.prototype.scrollIntoView = mockScrollIntoView;
// Act
const wrapper = shallowMount(
alert,
setupMocks({
propsData: {
shouldScrollToOnMount: false,
manualHeadline: "testHeader",
manualCopy: "testCopy",
},
})
);
// Assert
// This is an implementation detail - we just need to test that the final step of snapping
// the window to the alert is working. If using a different function to accomplish that
// just swap this out with the new function
expect(mockScrollIntoView).not.toHaveBeenCalled();
});
it("Should not call scrollIntoView() when the clientBoundingRect is entirely in the viewport", () => {
// Arrange
var viewPortHeight = 500;
setUpViewPort(viewPortHeight);
Element.prototype.getBoundingClientRect = jest.fn(() => {
return { top: 100, bottom: 200 };
});
var mockScrollIntoView = jest.fn();
Element.prototype.scrollIntoView = mockScrollIntoView;
// Act
const wrapper = shallowMount(alert, setupMocks({}));
// Assert
// This is an implementation detail - we just need to test that the final step of snapping
// the window to the alert is working. If using a different function to accomplish that
// just swap this out with the new function
expect(mockScrollIntoView).not.toHaveBeenCalled();
});
});
const mockMixin = {
methods: {
getCmsContent: jest.fn(),
getFooterInfoBoxHeight: jest.fn(() => 50),
},
computed: {
dynamicStrings: jest.fn(() => {
return { ROUTER_LINK: "routerLink:" };
}),
},
};
function setUpViewPort(height) {
Object.defineProperty(global.window, "innerHeight", {
writable: true,
configurable: true,
value: height,
});
Object.defineProperty(window.document.documentElement, "clientHeight", {
writable: true,
configurable: true,
value: height,
});
}
function setupMocks(mountOptionsMockData = {}) {
const defaultMountOptions = {
propsData: {
manualHeadline: "testHeader",
manualCopy: "testCopy",
},
mixins: [mockMixin],
};
const baseMountOptions = getMountOptions(
Object.assign(defaultMountOptions, mountOptionsMockData)
);
const allMountOptions = Object.assign(defaultMountOptions, baseMountOptions);
return allMountOptions;
}

View file

@ -0,0 +1,189 @@
<template>
<div
class="alert fade show text-center mb-0 py-2 px-4"
role="alert"
:class="[
isDismissible ? 'alert-dismissible' : '',
this.alertClass,
this.cssClassNameForCmsWidget,
]">
<p class="m-0 fw-bold alert-heading">{{ alertHeadline }}</p>
<template v-for="paragraph in splitAlertCopyForParagraphTag" :key="paragraph">
<p
class="m-0 text-body small"
v-if="!doesCopyContainRouterLink(paragraph)"
v-html="paragraph"></p>
<p class="m-0 text-body small" v-else>
<template v-for="copy in splitCopyOnCMSPlaceHolder(paragraph)" :key="copy">
<span v-if="!doesCopyContainRouterLink(copy)" v-html="copy"></span>
<span v-else>
<router-link
:to="{
query: { fmgPage: `${getRouterLinkRouteFromCopy(copy)}` },
name: 'root',
}"
>{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link
>
</span>
</template>
</p>
</template>
<button type="button" class="btn-close p-2" data-bs-dismiss="alert" aria-label="Close">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23.7 23.7" xml:space="preserve">
<path
d="m23.24 2.7-9.15 9.15L23.24 21a1.581 1.581 0 0 1-1.12 2.7c-.42 0-.82-.16-1.12-.46l-9.15-9.15-9.15 9.15c-.3.3-.7.46-1.12.46A1.581 1.581 0 0 1 .46 21l8.47-8.47.68-.68L.46 2.7c-.62-.62-.62-1.62 0-2.24.62-.62 1.62-.62 2.24 0l8.47 8.47.68.68L21 .46a1.57 1.57 0 0 1 2.23 0c.63.62.63 1.62.01 2.24z" />
</svg>
</button>
</div>
</template>
<script>
import {
doesCopyContainRouterLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
splitCMSCopyOnParagraphTag,
} from "@/helpers/cms-content-helper";
export default {
name: "alert",
props: {
isDismissible: Boolean,
/*
alertClass class names:
alert-success (green)
alert-danger (red)
alert-warning (yellow)
alert-info (blue)
*/
alertClass: String,
cmsWidgetName: {
type: String,
default(rawProps) {
if (!rawProps.cmsWidgetName) {
console.log("Error: Missing a CMS Widget Name (required field)");
}
return "widgetUndefined";
},
},
manualHeadline: String,
manualCopy: String,
shouldScrollToOnMount: {
type: Boolean,
default: true,
},
},
computed: {
alertHeadline() {
return this.manualHeadline
? this.manualHeadline
: this.getCmsContent(this.cmsWidgetName, "HeadlineText");
},
alertCopy() {
return this.manualCopy
? this.manualCopy
: this.getCmsContent(this.cmsWidgetName, "BodyText");
},
splitAlertCopyForParagraphTag() {
return splitCMSCopyOnParagraphTag(this.alertCopy);
},
},
methods: {
doesCopyContainRouterLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
ensureAlertIsInViewPort() {
if (this.shouldScrollToOnMount && this.$el.style.display != "none") {
var footerHeight = this.getFooterInfoBoxHeight();
if (!this.isAlertInViewport(footerHeight)) {
this.$el.scrollIntoView(true); // 'true' attempts to scroll element to top of viewport
}
}
},
isAlertInViewport(footerHeight) {
const rect = this.$el.getBoundingClientRect();
return (
rect.top >= 0 &&
// remove footerHeight from window height to avoid items being hidden behind footer
rect.bottom <=
(window.innerHeight - footerHeight ||
document.documentElement.clientHeight - footerHeight)
);
},
},
mounted() {
this.ensureAlertIsInViewPort();
},
};
</script>
<style lang="scss">
.alert {
button {
display: none;
}
.btn-close {
background: none;
opacity: 1;
width: 0.75rem;
height: 0.75rem;
}
&.alert-dismissible {
button {
display: flex;
top: 2px;
right: 2px;
}
}
&.alert-info {
background-color: $blue-100;
.alert-heading {
color: $blue-700;
}
svg {
fill: $blue-700;
width: 1rem;
height: 1rem;
}
}
&.alert-danger {
background-color: $red-100;
.alert-heading {
color: $red-600;
}
svg {
fill: $red-600;
width: 1rem;
height: 1rem;
}
}
&.alert-warning {
background-color: $yellow-100;
.alert-heading {
color: $yellow-600;
}
svg {
fill: $yellow-600;
width: 1rem;
height: 1rem;
}
}
&.alert-success {
background-color: $green-100;
.alert-heading {
color: $green-700;
}
svg {
fill: $green-700;
width: 1rem;
height: 1rem;
}
}
& p {
font-size: 0.875rem;
margin-bottom: 0.25rem !important;
}
}
</style>