@energysage/es-ds-components 3.0.0-alpha.6 → 3.0.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +9 -0
- package/.eslintrc.json +3 -0
- package/.prettierrc.json +10 -0
- package/components/es-accordion-list.vue +112 -0
- package/components/es-accordion.vue +7 -0
- package/components/es-badge.vue +20 -0
- package/components/es-breadcrumbs.vue +43 -32
- package/components/es-button.vue +112 -0
- package/components/es-card.vue +64 -0
- package/components/es-carousel.vue +160 -0
- package/components/{b-col.vue → es-col.vue} +11 -11
- package/components/es-collapse.vue +38 -36
- package/components/es-cta-banner.vue +95 -0
- package/components/es-cta-card.vue +60 -0
- package/components/es-data-table-simple.vue +73 -0
- package/components/es-data-table.vue +126 -0
- package/components/es-error-page.vue +120 -0
- package/components/es-file-input.vue +331 -0
- package/components/es-file-preview-modal.vue +61 -0
- package/components/es-footer.vue +163 -0
- package/components/es-form-checkbox.vue +40 -0
- package/components/es-form-input.vue +130 -0
- package/components/es-form-msg.vue +81 -0
- package/components/es-form-radio-card.vue +57 -0
- package/components/es-form-radio-cards.vue +78 -0
- package/components/es-form-textarea.vue +124 -0
- package/components/es-form.vue +16 -0
- package/components/es-modal.vue +103 -0
- package/components/es-nav-bar-account-menu.vue +67 -0
- package/components/es-nav-bar-featured-article.vue +60 -0
- package/components/es-nav-bar-link.vue +27 -0
- package/components/es-nav-bar-product-menu.vue +215 -0
- package/components/es-nav-bar-top-level-menu.vue +193 -0
- package/components/es-nav-bar-topic-menu.vue +194 -0
- package/components/es-nav-bar.vue +396 -0
- package/components/es-pagination.vue +66 -0
- package/components/es-popover.vue +113 -0
- package/components/es-progress-circle.vue +81 -0
- package/components/es-progress.vue +36 -0
- package/components/es-radio-button-group.vue +53 -0
- package/components/es-radio-button.vue +50 -57
- package/components/es-rating.vue +120 -0
- package/components/es-reviews-io-card-carousel.vue +275 -0
- package/components/es-slider.vue +147 -0
- package/components/es-spinner.vue +22 -0
- package/components/es-support-card.vue +150 -0
- package/components/es-support.vue +66 -0
- package/components/es-tab.vue +7 -0
- package/components/es-tabs.vue +67 -0
- package/components/es-verification-code.vue +94 -0
- package/components/es-view-more.vue +63 -0
- package/components/es-zip-code-form.vue +204 -0
- package/components/icon/arrow-clockwise.vue +51 -0
- package/components/icon/arrow-counter-clockwise.vue +37 -0
- package/components/icon/arrow-cycle.vue +43 -0
- package/components/icon/arrow-left.vue +39 -0
- package/components/icon/arrow-right.vue +39 -0
- package/components/icon/arrow-up.vue +39 -0
- package/components/icon/arrow.vue +68 -0
- package/components/icon/bank.vue +38 -0
- package/components/icon/battery.vue +40 -0
- package/components/icon/bell.vue +42 -0
- package/components/icon/block.vue +46 -0
- package/components/icon/bolt.vue +38 -0
- package/components/icon/business.vue +42 -0
- package/components/icon/calculator.vue +37 -0
- package/components/icon/calendar.vue +46 -0
- package/components/icon/cart.vue +46 -0
- package/components/icon/chat-add.vue +44 -0
- package/components/icon/chat-approve.vue +42 -0
- package/components/icon/chat-bubble.vue +40 -0
- package/components/icon/chat-dots.vue +42 -0
- package/components/icon/check.vue +39 -0
- package/components/{icon-chevron-down.vue → icon/chevron-down.vue} +16 -16
- package/components/icon/chevron-horizontal.vue +39 -0
- package/components/icon/chevron-left.vue +39 -0
- package/components/{icon-chevron-right.vue → icon/chevron-right.vue} +16 -16
- package/components/icon/chevron-up.vue +39 -0
- package/components/icon/circle-alert.vue +46 -0
- package/components/icon/circle-check.vue +46 -0
- package/components/icon/circle-chevron-down.vue +47 -0
- package/components/icon/circle-chevron-left.vue +46 -0
- package/components/icon/circle-chevron-right.vue +46 -0
- package/components/icon/circle-chevron-up.vue +46 -0
- package/components/icon/circle-minus.vue +46 -0
- package/components/icon/circle-plus.vue +46 -0
- package/components/icon/circle-x.vue +46 -0
- package/components/icon/circle.vue +42 -0
- package/components/icon/clock.vue +46 -0
- package/components/icon/community-solar.vue +37 -0
- package/components/icon/companies.vue +43 -0
- package/components/icon/contact-us.vue +37 -0
- package/components/icon/copy.vue +45 -0
- package/components/icon/credit-card.vue +37 -0
- package/components/icon/document.vue +45 -0
- package/components/icon/electric-vehicle.vue +37 -0
- package/components/icon/electricity-circle.vue +39 -0
- package/components/icon/envelope.vue +45 -0
- package/components/icon/es-leaf.vue +39 -0
- package/components/icon/eye.vue +41 -0
- package/components/icon/facebook.vue +41 -0
- package/components/icon/file-doc.vue +72 -0
- package/components/icon/file-docx.vue +72 -0
- package/components/icon/file-pdf.vue +72 -0
- package/components/icon/flag.vue +37 -0
- package/components/icon/folder.vue +41 -0
- package/components/icon/gear.vue +37 -0
- package/components/icon/gift.vue +37 -0
- package/components/icon/hamburger.vue +38 -0
- package/components/icon/heart.vue +39 -0
- package/components/icon/heat-pump.vue +41 -0
- package/components/icon/image.vue +45 -0
- package/components/icon/info-blog-post.vue +47 -0
- package/components/icon/info.vue +45 -0
- package/components/icon/instagram.vue +49 -0
- package/components/icon/light-bulb.vue +36 -0
- package/components/icon/link-out.vue +38 -0
- package/components/icon/linkedin.vue +41 -0
- package/components/icon/location.vue +45 -0
- package/components/icon/lock-off.vue +45 -0
- package/components/icon/lock-on.vue +48 -0
- package/components/icon/marketplace.vue +43 -0
- package/components/icon/minus.vue +38 -0
- package/components/icon/money-bag.vue +37 -0
- package/components/icon/money.vue +35 -0
- package/components/icon/open-envelope.vue +45 -0
- package/components/icon/paper.vue +45 -0
- package/components/icon/payback.vue +37 -0
- package/components/icon/pencil.vue +45 -0
- package/components/icon/person-add.vue +41 -0
- package/components/icon/person-check.vue +41 -0
- package/components/icon/person-cross.vue +41 -0
- package/components/icon/person.vue +41 -0
- package/components/icon/phone.vue +41 -0
- package/components/icon/play.vue +41 -0
- package/components/icon/plus.vue +38 -0
- package/components/icon/roof.vue +37 -0
- package/components/icon/rooftop-solar.vue +37 -0
- package/components/icon/saving.vue +37 -0
- package/components/icon/search.vue +43 -0
- package/components/icon/settings-horizontal.vue +57 -0
- package/components/icon/settings-vertical.vue +57 -0
- package/components/icon/shield-with-check.vue +38 -0
- package/components/icon/shield.vue +41 -0
- package/components/icon/shopping-bag.vue +39 -0
- package/components/icon/solar.vue +37 -0
- package/components/icon/sound-down.vue +45 -0
- package/components/icon/sound-off.vue +41 -0
- package/components/icon/sound-on.vue +41 -0
- package/components/icon/sound-up.vue +41 -0
- package/components/icon/star-empty.vue +38 -0
- package/components/icon/star-full.vue +36 -0
- package/components/icon/star-half.vue +55 -0
- package/components/icon/state-ak.vue +53 -0
- package/components/icon/state-al.vue +38 -0
- package/components/icon/state-ar.vue +38 -0
- package/components/icon/state-az.vue +38 -0
- package/components/icon/state-ca.vue +38 -0
- package/components/icon/state-co.vue +38 -0
- package/components/icon/state-ct.vue +38 -0
- package/components/icon/state-dc.vue +52 -0
- package/components/icon/state-de.vue +38 -0
- package/components/icon/state-fl.vue +38 -0
- package/components/icon/state-ga.vue +38 -0
- package/components/icon/state-hi.vue +59 -0
- package/components/icon/state-ia.vue +38 -0
- package/components/icon/state-id.vue +38 -0
- package/components/icon/state-il.vue +38 -0
- package/components/icon/state-in.vue +38 -0
- package/components/icon/state-ks.vue +38 -0
- package/components/icon/state-ky.vue +38 -0
- package/components/icon/state-la.vue +38 -0
- package/components/icon/state-ma.vue +38 -0
- package/components/icon/state-md.vue +39 -0
- package/components/icon/state-me.vue +38 -0
- package/components/icon/state-mi.vue +38 -0
- package/components/icon/state-mn.vue +38 -0
- package/components/icon/state-mo.vue +38 -0
- package/components/icon/state-ms.vue +38 -0
- package/components/icon/state-mt.vue +38 -0
- package/components/icon/state-nc.vue +38 -0
- package/components/icon/state-nd.vue +38 -0
- package/components/icon/state-ne.vue +38 -0
- package/components/icon/state-nh.vue +38 -0
- package/components/icon/state-nj.vue +37 -0
- package/components/icon/state-nm.vue +38 -0
- package/components/icon/state-nv.vue +38 -0
- package/components/icon/state-ny.vue +41 -0
- package/components/icon/state-oh.vue +38 -0
- package/components/icon/state-ok.vue +38 -0
- package/components/icon/state-or.vue +38 -0
- package/components/icon/state-pa.vue +38 -0
- package/components/icon/state-ri.vue +38 -0
- package/components/icon/state-sc.vue +38 -0
- package/components/icon/state-sd.vue +38 -0
- package/components/icon/state-tn.vue +38 -0
- package/components/icon/state-tx.vue +38 -0
- package/components/icon/state-ut.vue +38 -0
- package/components/icon/state-va.vue +38 -0
- package/components/icon/state-vt.vue +38 -0
- package/components/icon/state-wa.vue +38 -0
- package/components/icon/state-wi.vue +38 -0
- package/components/icon/state-wv.vue +38 -0
- package/components/icon/state-wy.vue +38 -0
- package/components/icon/thumbs-up.vue +39 -0
- package/components/icon/trash-can.vue +41 -0
- package/components/icon/tree.vue +37 -0
- package/components/icon/twitter.vue +43 -0
- package/components/icon/upload.vue +41 -0
- package/components/icon/verified.vue +40 -0
- package/components/icon/video-play.vue +41 -0
- package/components/icon/wifi.vue +38 -0
- package/components/icon/x.vue +38 -0
- package/components/icon/youtube.vue +45 -0
- package/composables/forms.ts +124 -0
- package/lib-utils/mime-type-finder.js +48 -0
- package/nuxt.config.ts +40 -9
- package/package.json +29 -5
- package/tsconfig.json +2 -2
- package/utils/get-es-footer-content.js +90 -0
- package/utils/get-es-nav-bar-account-content.js +55 -0
- package/utils/get-es-nav-bar-global-content.js +474 -0
- /package/components/{b-container.vue → es-container.vue} +0 -0
- /package/components/{b-row.vue → es-row.vue} +0 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import findMimeType, { mimeTypes } from '../lib-utils/mime-type-finder';
|
|
3
|
+
|
|
4
|
+
type UploadInfo = {
|
|
5
|
+
name: string;
|
|
6
|
+
uploadUrl: string;
|
|
7
|
+
additionalFields?: object;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const props = defineProps({
|
|
11
|
+
/**
|
|
12
|
+
* An array of Objects with the following shape:
|
|
13
|
+
* {
|
|
14
|
+
* name: String,
|
|
15
|
+
* uploadUrl: String,
|
|
16
|
+
* additionalFields: Object,
|
|
17
|
+
* }
|
|
18
|
+
*/
|
|
19
|
+
uploadUrls: {
|
|
20
|
+
type: Array<UploadInfo>,
|
|
21
|
+
default: () => [],
|
|
22
|
+
},
|
|
23
|
+
/**
|
|
24
|
+
* The file types that are allowed to be uploaded. This is a list of MIME types as strings. If the array is
|
|
25
|
+
* empty, all file types are allowed. A list of acceptable MIME types can be found here in the template column:
|
|
26
|
+
* https://www.iana.org/assignments/media-types/media-types.xhtml
|
|
27
|
+
*/
|
|
28
|
+
fileTypes: {
|
|
29
|
+
type: Array,
|
|
30
|
+
default: () => [],
|
|
31
|
+
required: true,
|
|
32
|
+
validator(fileTypes) {
|
|
33
|
+
return fileTypes.every((fileType) => mimeTypes.includes(fileType));
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
/**
|
|
37
|
+
* The maximum file size in MB. This is per file, not the total size of all files.
|
|
38
|
+
*/
|
|
39
|
+
maxFileSizeMb: {
|
|
40
|
+
type: Number,
|
|
41
|
+
default: 25,
|
|
42
|
+
required: false,
|
|
43
|
+
},
|
|
44
|
+
/**
|
|
45
|
+
* When true, the component will be shorter vertically. This is achieved by giving the parent div
|
|
46
|
+
* flex-direction: row instead of the default flex-direction: column.
|
|
47
|
+
*/
|
|
48
|
+
collapsed: {
|
|
49
|
+
type: Boolean,
|
|
50
|
+
default: false,
|
|
51
|
+
required: false,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
let currentFiles: Array<File> = [];
|
|
56
|
+
let active = false;
|
|
57
|
+
const fileInput = ref(null);
|
|
58
|
+
const emit = defineEmits([
|
|
59
|
+
'fileSizeError',
|
|
60
|
+
'fileTypeError',
|
|
61
|
+
'fileIsAFolderError',
|
|
62
|
+
'fileDataRead',
|
|
63
|
+
'duplicateFileMessage',
|
|
64
|
+
'readyToUpload',
|
|
65
|
+
'uploadFailure',
|
|
66
|
+
'uploadSuccess',
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
const filterLargeFiles = (files: Array<File>) => {
|
|
70
|
+
// Takes an array of files, and filters out any files that exceed the maxFileSizeMb prop. Emits a
|
|
71
|
+
// fileSizeError event for each file that exceeds the maxFileSizeMb prop. The File API uses bytes, so
|
|
72
|
+
// we have to convert the maxFileSizeMb prop to bytes.
|
|
73
|
+
const maxFileSizeBytes = props.maxFileSizeMb * 1000000;
|
|
74
|
+
return [...files].filter((file) => {
|
|
75
|
+
if (file.size > maxFileSizeBytes) {
|
|
76
|
+
emit('fileSizeError', file.name);
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const removeSpaceFromFileNames = (files: Array<File>) => {
|
|
84
|
+
const newFiles = files.map(
|
|
85
|
+
(file) =>
|
|
86
|
+
new File(
|
|
87
|
+
// Return new File object with the modified name (without any space)
|
|
88
|
+
[file],
|
|
89
|
+
file.name.replace(/\s/g, ''),
|
|
90
|
+
{ type: file.type, lastModified: file.lastModified },
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
return newFiles;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
async function verifyMimeType(file: File) {
|
|
97
|
+
// If an empty folder then trigger fileIsAFolderError
|
|
98
|
+
if (file.type === '') {
|
|
99
|
+
emit('fileIsAFolderError');
|
|
100
|
+
}
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
const fileReader = new FileReader();
|
|
103
|
+
fileReader.onload = (evt) => {
|
|
104
|
+
const uint = new Uint8Array(evt.target.result);
|
|
105
|
+
const bytes: string[] = [];
|
|
106
|
+
uint.forEach((byte) => {
|
|
107
|
+
bytes.push(byte.toString(16));
|
|
108
|
+
});
|
|
109
|
+
const hex = bytes.join('').toUpperCase();
|
|
110
|
+
const mimeType = findMimeType(hex);
|
|
111
|
+
if (!mimeType || (props.fileTypes.length > 0 && !props.fileTypes.includes(mimeType))) {
|
|
112
|
+
emit('fileTypeError', file.name);
|
|
113
|
+
return reject();
|
|
114
|
+
}
|
|
115
|
+
return resolve(file);
|
|
116
|
+
};
|
|
117
|
+
fileReader.onerror = (error) => {
|
|
118
|
+
reject(error);
|
|
119
|
+
};
|
|
120
|
+
const blob = file.slice(0, 4);
|
|
121
|
+
fileReader.readAsArrayBuffer(blob);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function uploadSingleFile(file: File) {
|
|
126
|
+
const uploadInfo = props.uploadUrls.find((uploadUrl) => uploadUrl.name === file.name);
|
|
127
|
+
if (!uploadInfo) {
|
|
128
|
+
emit('uploadFailure', {
|
|
129
|
+
name: file.name,
|
|
130
|
+
message: 'No upload URL found for this file.',
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const form = new FormData();
|
|
135
|
+
if (uploadInfo.additionalFields) {
|
|
136
|
+
Object.entries(uploadInfo.additionalFields).forEach(([key, value]) => {
|
|
137
|
+
form.append(key, value);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
form.append('file', file);
|
|
141
|
+
fetch(uploadInfo.uploadUrl, {
|
|
142
|
+
method: 'POST',
|
|
143
|
+
body: form,
|
|
144
|
+
})
|
|
145
|
+
.then((response) => {
|
|
146
|
+
if (response.status >= 200 && response.status < 300) {
|
|
147
|
+
emit('uploadSuccess', file.name);
|
|
148
|
+
} else {
|
|
149
|
+
emit('uploadFailure', {
|
|
150
|
+
name: file.name,
|
|
151
|
+
message: `Received ${response.status} code from server.`,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
.catch((error) => {
|
|
156
|
+
if (error.response) {
|
|
157
|
+
// The request was made and the server responded with a status code
|
|
158
|
+
// that falls out of the range of 2xx
|
|
159
|
+
emit('uploadFailure', {
|
|
160
|
+
name: file.name,
|
|
161
|
+
message: `Received ${error.response.status} code from server.`,
|
|
162
|
+
});
|
|
163
|
+
} else if (error.request) {
|
|
164
|
+
// The request was made but no response was received
|
|
165
|
+
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
|
166
|
+
// http.ClientRequest in node.js
|
|
167
|
+
emit('uploadFailure', { name: file.name, message: 'The server did not respond.' });
|
|
168
|
+
} else {
|
|
169
|
+
// Something happened in setting up the request that triggered an Error
|
|
170
|
+
emit('uploadFailure', {
|
|
171
|
+
name: file.name,
|
|
172
|
+
message: 'There was an error sending your file to the server.',
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const readFilesIntoUrl = (files: Array<File>) => {
|
|
179
|
+
files.forEach((file) => {
|
|
180
|
+
const fileReader = new FileReader();
|
|
181
|
+
fileReader.onload = () => {
|
|
182
|
+
emit('fileDataRead', {
|
|
183
|
+
name: file.name,
|
|
184
|
+
type: file.type,
|
|
185
|
+
size: file.size,
|
|
186
|
+
// For non-image files (pdf, docx, etc.)
|
|
187
|
+
data: file.type.includes('application')
|
|
188
|
+
? URL.createObjectURL(new Blob([file], { type: file.type }))
|
|
189
|
+
: fileReader.result,
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
fileReader.readAsDataURL(file);
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
async function verifyFiles(files: Array<File>) {
|
|
197
|
+
const correctlySizedFiles = filterLargeFiles(files);
|
|
198
|
+
const correctlyNamedFiles = removeSpaceFromFileNames(correctlySizedFiles);
|
|
199
|
+
|
|
200
|
+
// Make sure the file is the correct mime type
|
|
201
|
+
let newValidFiles = await Promise.allSettled(correctlyNamedFiles.map(async (file) => verifyMimeType(file)));
|
|
202
|
+
// Filter out undefined values and "rejected"
|
|
203
|
+
newValidFiles = newValidFiles.filter((file) => file && file.status === 'fulfilled').map((file) => file.value);
|
|
204
|
+
|
|
205
|
+
// If the new file with name already exists in the current files
|
|
206
|
+
// Don't upload the new file and display an error
|
|
207
|
+
const duplicateFiles = newValidFiles.filter(({ name }) => currentFiles.some((file) => file.name === name));
|
|
208
|
+
duplicateFiles.forEach((file) => emit('duplicateFileMessage', file.name));
|
|
209
|
+
|
|
210
|
+
// For new files with unused names, add them to files
|
|
211
|
+
newValidFiles = newValidFiles.filter(({ name }) => !currentFiles.some((file) => file.name === name));
|
|
212
|
+
currentFiles = [...currentFiles, ...newValidFiles];
|
|
213
|
+
|
|
214
|
+
if (newValidFiles.length > 0) {
|
|
215
|
+
emit('readyToUpload', newValidFiles);
|
|
216
|
+
readFilesIntoUrl(newValidFiles);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
watch(
|
|
221
|
+
() => props.uploadUrls,
|
|
222
|
+
(newUrls, oldUrls) => {
|
|
223
|
+
// For every file that has an associated upload URL, we start the upload
|
|
224
|
+
newUrls.forEach((newUrlPair) => {
|
|
225
|
+
const oldUrl = oldUrls.find(({ name }) => name === newUrlPair.name);
|
|
226
|
+
const fileToUpload = currentFiles.find((file) => file.name === newUrlPair.name);
|
|
227
|
+
if ((!oldUrl || oldUrl.uploadUrl !== newUrlPair.uploadUrl) && fileToUpload) {
|
|
228
|
+
uploadSingleFile(fileToUpload);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
},
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
async function onFileChanged($event: Event) {
|
|
235
|
+
const target = $event.target as HTMLInputElement;
|
|
236
|
+
// The user has selected files from the file picker. We have to filter out any files that exceed the
|
|
237
|
+
// maxFileSizeMb prop.
|
|
238
|
+
if (target && target.files) {
|
|
239
|
+
await verifyFiles([...target.files]);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const onDrop = (event) => {
|
|
244
|
+
// The user has dropped files onto the component. We have to apply the same logic as if they had
|
|
245
|
+
// selected the files from the file picker which limits the file types to the ones specified in the
|
|
246
|
+
// fileTypes prop.
|
|
247
|
+
active = false;
|
|
248
|
+
|
|
249
|
+
// Use DataTransferItemList interface to access the file(s)
|
|
250
|
+
const dataTransfersAsFiles = [...event.dataTransfer.items]
|
|
251
|
+
.filter((item) => {
|
|
252
|
+
if (item.kind !== 'file') {
|
|
253
|
+
emit('fileTypeError', item.name);
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
return true;
|
|
257
|
+
})
|
|
258
|
+
.map((item) => item.getAsFile());
|
|
259
|
+
|
|
260
|
+
verifyFiles(dataTransfersAsFiles);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const openFilePicker = () => {
|
|
264
|
+
if (fileInput.value) {
|
|
265
|
+
fileInput.value.click();
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
</script>
|
|
269
|
+
|
|
270
|
+
<template>
|
|
271
|
+
<div
|
|
272
|
+
role="presentation"
|
|
273
|
+
class="es-file-upload align-items-center rounded-sm d-flex flex-column justify-content-center p-150"
|
|
274
|
+
:class="{ active: active }"
|
|
275
|
+
@dragenter.stop.prevent="active = true"
|
|
276
|
+
@dragover.stop.prevent="active = true"
|
|
277
|
+
@dragleave.stop.prevent="active = false"
|
|
278
|
+
@dragend.stop.prevent="onDrop"
|
|
279
|
+
@drop.stop.prevent="onDrop"
|
|
280
|
+
@click.self="openFilePicker"
|
|
281
|
+
@keydown.enter.prevent="openFilePicker"
|
|
282
|
+
@keydown.space.prevent="openFilePicker">
|
|
283
|
+
<div
|
|
284
|
+
class="d-flex justify-content-center flex-column"
|
|
285
|
+
:class="{ 'flex-md-row': collapsed }">
|
|
286
|
+
<div class="align-self-center">
|
|
287
|
+
<icon-upload
|
|
288
|
+
class="d-none d-md-inline-block"
|
|
289
|
+
:class="{ 'mb-200': collapsed }"
|
|
290
|
+
height="32px"
|
|
291
|
+
width="32px" />
|
|
292
|
+
<icon-upload
|
|
293
|
+
class="d-md-none"
|
|
294
|
+
height="24px"
|
|
295
|
+
width="24px" />
|
|
296
|
+
</div>
|
|
297
|
+
<div
|
|
298
|
+
class="align-self-center text-center"
|
|
299
|
+
:class="{ 'px-100': collapsed }">
|
|
300
|
+
<slot name="cta" />
|
|
301
|
+
</div>
|
|
302
|
+
<div class="align-self-center">
|
|
303
|
+
<es-button
|
|
304
|
+
class="d-none d-md-inline w-100 w-md-auto"
|
|
305
|
+
:class="{ 'mb-200': collapsed }"
|
|
306
|
+
@click="openFilePicker">
|
|
307
|
+
<slot name="buttonText">
|
|
308
|
+
<p class="m-0">Browse files</p>
|
|
309
|
+
</slot>
|
|
310
|
+
</es-button>
|
|
311
|
+
<es-button
|
|
312
|
+
class="d-md-none w-100"
|
|
313
|
+
@click="openFilePicker">
|
|
314
|
+
<slot name="buttonText">
|
|
315
|
+
<p class="m-0">Choose files</p>
|
|
316
|
+
</slot>
|
|
317
|
+
</es-button>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
<div>
|
|
321
|
+
<slot name="helpText" />
|
|
322
|
+
</div>
|
|
323
|
+
<input
|
|
324
|
+
ref="fileInput"
|
|
325
|
+
type="file"
|
|
326
|
+
:accept="fileTypes.join(', ')"
|
|
327
|
+
style="display: none"
|
|
328
|
+
multiple
|
|
329
|
+
@change="onFileChanged($event)" />
|
|
330
|
+
</div>
|
|
331
|
+
</template>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps({
|
|
3
|
+
visible: {
|
|
4
|
+
type: Boolean,
|
|
5
|
+
default: false,
|
|
6
|
+
required: false,
|
|
7
|
+
},
|
|
8
|
+
fileName: {
|
|
9
|
+
type: String,
|
|
10
|
+
default: 'test.png',
|
|
11
|
+
required: false,
|
|
12
|
+
},
|
|
13
|
+
fileSource: {
|
|
14
|
+
type: String,
|
|
15
|
+
default: '',
|
|
16
|
+
required: false,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
defineEmits(['hidden']);
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<es-modal
|
|
25
|
+
:id="`preview-modal-${fileName}`"
|
|
26
|
+
hide-footer
|
|
27
|
+
:visible="visible"
|
|
28
|
+
size="xl"
|
|
29
|
+
body-class="border-top p-0"
|
|
30
|
+
@hidden="$emit('hidden')">
|
|
31
|
+
<template #modal-title>
|
|
32
|
+
{{ fileName }}
|
|
33
|
+
</template>
|
|
34
|
+
<div class="d-md-none d-block file-preview text-center">
|
|
35
|
+
<nuxt-img
|
|
36
|
+
loading="lazy"
|
|
37
|
+
class="img-fluid file-preview"
|
|
38
|
+
:src="fileSource"
|
|
39
|
+
:alt="fileName" />
|
|
40
|
+
</div>
|
|
41
|
+
<div class="d-none d-md-block">
|
|
42
|
+
<div class="px-500 py-200 bg-gray-200">
|
|
43
|
+
<div class="card bg-white file-preview">
|
|
44
|
+
<nuxt-img
|
|
45
|
+
loading="lazy"
|
|
46
|
+
class="img-fluid file-preview rounded-img"
|
|
47
|
+
:src="fileSource"
|
|
48
|
+
:alt="fileName" />
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</es-modal>
|
|
53
|
+
</template>
|
|
54
|
+
|
|
55
|
+
<style lang="scss" scoped>
|
|
56
|
+
@use '@energysage/es-ds-styles/scss/variables' as variables;
|
|
57
|
+
|
|
58
|
+
.rounded-img {
|
|
59
|
+
border-radius: variables.$card-inner-border-radius !important;
|
|
60
|
+
}
|
|
61
|
+
</style>
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<script lang="js">
|
|
2
|
+
import IconFacebook from './icon/facebook.vue';
|
|
3
|
+
import IconInstagram from './icon/instagram.vue';
|
|
4
|
+
import IconLinkedin from './icon/linkedin.vue';
|
|
5
|
+
import IconTwitter from './icon/twitter.vue';
|
|
6
|
+
import IconYoutube from './icon/youtube.vue';
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
name: 'EsFooter',
|
|
10
|
+
components: {
|
|
11
|
+
IconFacebook,
|
|
12
|
+
IconInstagram,
|
|
13
|
+
IconLinkedin,
|
|
14
|
+
IconTwitter,
|
|
15
|
+
IconYoutube,
|
|
16
|
+
},
|
|
17
|
+
props: {
|
|
18
|
+
content: {
|
|
19
|
+
type: Object,
|
|
20
|
+
required: true,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
computed: {
|
|
24
|
+
copyrightText() {
|
|
25
|
+
return this.content.copyrightText.replace('{currentYear}', new Date().getFullYear());
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<div class="es-footer clearfix">
|
|
33
|
+
<!-- Make an impact -->
|
|
34
|
+
<div class="bg-blue-900 text-white py-200 py-lg-300 px-200">
|
|
35
|
+
<div class="container px-0">
|
|
36
|
+
<div class="row">
|
|
37
|
+
<div
|
|
38
|
+
class="col col-12 col-md-5 col-lg-4 font-size-200 font-size-md-400 font-weight-bold mb-150 mb-md-0">
|
|
39
|
+
{{ content.banner.headline }}
|
|
40
|
+
<br />
|
|
41
|
+
{{ content.banner.subHeadline }}
|
|
42
|
+
</div>
|
|
43
|
+
<div class="col col-12 col-md-7 col-lg-8">
|
|
44
|
+
{{ content.banner.body }}
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<!-- Make an impact -->
|
|
50
|
+
<div class="bg-blue-800 py-200 pt-lg-200 pb-lg-150 px-200">
|
|
51
|
+
<div class="container px-0">
|
|
52
|
+
<div class="row">
|
|
53
|
+
<!-- Link columns -->
|
|
54
|
+
<div class="col col-12 col-lg-8 order-lg-1 text-white">
|
|
55
|
+
<div class="row">
|
|
56
|
+
<div
|
|
57
|
+
v-for="column in content.columns"
|
|
58
|
+
:key="column.title"
|
|
59
|
+
class="col col-6 col-md-3 mb-100 mb-lg-0">
|
|
60
|
+
<ul class="list-unstyled mb-lg-0">
|
|
61
|
+
<li class="font-weight-bold font-size-75 font-size-md-100 mb-100 mb-lg-50">
|
|
62
|
+
{{ column.title }}
|
|
63
|
+
</li>
|
|
64
|
+
<li
|
|
65
|
+
v-for="link in column.links"
|
|
66
|
+
:key="link.text"
|
|
67
|
+
class="mt-0 mt-lg-25 mb-100 mb-lg-50 font-size-75">
|
|
68
|
+
<es-nav-bar-link
|
|
69
|
+
:href="link.url"
|
|
70
|
+
:target="link.newTab ? '_blank' : null"
|
|
71
|
+
class="text-reset">
|
|
72
|
+
{{ link.text }}
|
|
73
|
+
</es-nav-bar-link>
|
|
74
|
+
</li>
|
|
75
|
+
</ul>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
<!-- Link columns -->
|
|
80
|
+
<!-- Logo and Social-->
|
|
81
|
+
<div class="col col-12 col-lg-4 order-lg-0">
|
|
82
|
+
<es-nav-bar-link
|
|
83
|
+
class="d-block mb-150 d-flex justify-content-center justify-content-lg-start"
|
|
84
|
+
:href="content.home.link">
|
|
85
|
+
<span class="sr-only">
|
|
86
|
+
{{ content.home.name }}
|
|
87
|
+
</span>
|
|
88
|
+
<div class="es-footer-logo">
|
|
89
|
+
<slot name="logo" />
|
|
90
|
+
</div>
|
|
91
|
+
</es-nav-bar-link>
|
|
92
|
+
<!-- Social icons -->
|
|
93
|
+
<div class="d-flex text-white justify-content-center justify-content-lg-start">
|
|
94
|
+
<es-nav-bar-link
|
|
95
|
+
v-for="(iconLink, index) in content.socialLinks"
|
|
96
|
+
:key="iconLink.text"
|
|
97
|
+
class="text-reset"
|
|
98
|
+
:class="{ 'mr-150': index < content.socialLinks.length - 1 }"
|
|
99
|
+
:href="iconLink.url"
|
|
100
|
+
target="_blank">
|
|
101
|
+
<span class="sr-only">{{ iconLink.text }}</span>
|
|
102
|
+
<component :is="iconLink.icon" />
|
|
103
|
+
</es-nav-bar-link>
|
|
104
|
+
</div>
|
|
105
|
+
<!-- Social icons -->
|
|
106
|
+
</div>
|
|
107
|
+
<!-- Logo and Social -->
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="bg-blue-700 text-blue-50 px-200 py-100">
|
|
112
|
+
<div class="container px-0">
|
|
113
|
+
<!-- Trademark Info -->
|
|
114
|
+
<p class="font-size-75">
|
|
115
|
+
{{ content.trademarkText }}
|
|
116
|
+
</p>
|
|
117
|
+
<p class="mb-200 font-size-75">
|
|
118
|
+
{{ copyrightText }}
|
|
119
|
+
</p>
|
|
120
|
+
<!-- Trademark Info -->
|
|
121
|
+
<!-- Legal -->
|
|
122
|
+
<hr class="border-top border-blue-500 m-0" />
|
|
123
|
+
<div class="row justify-content-center pt-100">
|
|
124
|
+
<div
|
|
125
|
+
v-for="link in content.legalLinks"
|
|
126
|
+
:key="link.text"
|
|
127
|
+
class="col col-6 col-lg-auto font-size-75 font-weight-semibold font-size-md-50 font-weight-md-normal mt-25 mb-50 px-lg-300">
|
|
128
|
+
<es-nav-bar-link
|
|
129
|
+
:href="link.url"
|
|
130
|
+
class="text-reset font-weight-normal">
|
|
131
|
+
{{ link.text }}
|
|
132
|
+
</es-nav-bar-link>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
<!-- Legal -->
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
<div class="bg-blue-50 px-200 py-100">
|
|
139
|
+
<div class="container px-0">
|
|
140
|
+
<!-- DOE -->
|
|
141
|
+
<div class="d-lg-flex align-items-end">
|
|
142
|
+
<!-- eslint-disable-next-line -->
|
|
143
|
+
<img
|
|
144
|
+
:src="content.departmentOfEnergy.logoUrl"
|
|
145
|
+
alt="department of energy logo"
|
|
146
|
+
aria-hidden="true"
|
|
147
|
+
class="mr-100 mb-100 mb-lg-0"
|
|
148
|
+
width="99"
|
|
149
|
+
height="24" />
|
|
150
|
+
<p class="mb-0 font-size-75 font-weight-normal font-weight-lg-semibold">
|
|
151
|
+
<es-nav-bar-link
|
|
152
|
+
:href="content.departmentOfEnergy.learnMore.link"
|
|
153
|
+
target="_blank"
|
|
154
|
+
class="text-blue-900">
|
|
155
|
+
{{ content.departmentOfEnergy.learnMore.text }}
|
|
156
|
+
</es-nav-bar-link>
|
|
157
|
+
</p>
|
|
158
|
+
</div>
|
|
159
|
+
<!-- DOE -->
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</template>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const model = defineModel<boolean>({
|
|
3
|
+
default: false,
|
|
4
|
+
});
|
|
5
|
+
defineOptions({
|
|
6
|
+
inheritAttrs: false,
|
|
7
|
+
});
|
|
8
|
+
defineProps({
|
|
9
|
+
id: {
|
|
10
|
+
type: String,
|
|
11
|
+
required: true,
|
|
12
|
+
},
|
|
13
|
+
disabled: {
|
|
14
|
+
type: Boolean,
|
|
15
|
+
default: false,
|
|
16
|
+
},
|
|
17
|
+
indeterminate: {
|
|
18
|
+
type: Boolean,
|
|
19
|
+
default: false,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<div class="custom-control custom-checkbox">
|
|
26
|
+
<input
|
|
27
|
+
v-bind="$attrs"
|
|
28
|
+
:id="id"
|
|
29
|
+
v-model="model"
|
|
30
|
+
type="checkbox"
|
|
31
|
+
class="custom-control-input"
|
|
32
|
+
:indeterminate="indeterminate"
|
|
33
|
+
:disabled="disabled" />
|
|
34
|
+
<label
|
|
35
|
+
:for="id"
|
|
36
|
+
class="custom-control-label">
|
|
37
|
+
<slot />
|
|
38
|
+
</label>
|
|
39
|
+
</div>
|
|
40
|
+
</template>
|