├── .eslintrc.json
├── .gitignore
├── AttachmentGallery
├── ControlManifest.Input.xml
├── css
│ └── main.css
├── img
│ └── pdf_icon.png
└── index.ts
├── LICENSE.md
├── PCF-AttachmentGalleryControl.pcfproj
├── README.md
├── Screenshots
└── gallery-v1.gif
├── Solution
└── AttachmentGallerySol
│ ├── .gitignore
│ ├── AttachmentGallerySol.cdsproj
│ └── src
│ └── Other
│ ├── Customizations.xml
│ ├── Relationships.xml
│ └── Solution.xml
├── package-lock.json
├── package.json
├── pcfconfig.json
└── tsconfig.json
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true
5 | },
6 | "extends": [
7 | "eslint:recommended"
8 | ],
9 | "globals": {
10 | "ComponentFramework": true
11 | },
12 | "parser": "@typescript-eslint/parser",
13 | "parserOptions": {
14 | "ecmaVersion": 12,
15 | "sourceType": "module"
16 | },
17 | "plugins": [
18 | "@microsoft/power-apps",
19 | "@typescript-eslint"
20 | ],
21 | "rules": {
22 | "no-unused-vars": "off",
23 | "no-undef": "off"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # generated directory
7 | **/generated
8 |
9 | # output directory
10 | /out
11 |
12 | # msbuild output directories
13 | /bin
14 | /obj
15 |
16 | # MSBuild Binary and Structured Log
17 | *.binlog
18 |
--------------------------------------------------------------------------------
/AttachmentGallery/ControlManifest.Input.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
14 |
15 |
19 |
20 |
21 |
22 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/AttachmentGallery/css/main.css:
--------------------------------------------------------------------------------
1 | @import "https://static2.sharepointonline.com/files/fabric/office-ui-fabric-core/10.0.0/css/fabric.min.css";
2 |
3 | .thumbnail {
4 | border: 1px solid #ddd;
5 | border-radius: 4px;
6 | padding: 5px;
7 | margin: 5px;
8 | height: 100px;
9 | }
10 |
11 | /* Add a hover effect (blue shadow) */
12 | .thumbnail:hover {
13 | box-shadow: 0 0 2px 1px rgba(0, 140, 186, 0.5);
14 | }
15 |
16 | .thumbnailsList {
17 | display: block;
18 | position: relative;
19 | height: auto;
20 | max-width: 100%;
21 | overflow-y: hidden;
22 | overflow-x: auto;
23 | word-wrap: normal;
24 | white-space: nowrap;
25 | }
26 |
27 | .preview-section {
28 | display: flex;
29 | justify-content: center;
30 | align-items: center;
31 | max-width: 100%;
32 | position: relative;
33 | }
34 |
35 | .preview-img {
36 | max-width: 100%;
37 | height:400px;
38 | cursor: pointer;
39 | }
40 |
41 | .header-icon{
42 | font-size: 24px;
43 | cursor: pointer;
44 | }
45 |
46 | .header-icon-container{
47 | display: flex;
48 | justify-content: space-around;
49 | width: 200px;
50 | }
51 |
52 | .arrow-button {
53 | cursor: pointer;
54 | position: absolute;
55 | top: 50%;
56 | width: auto;
57 | margin-top: -22px;
58 | padding: 16px;
59 | color: white;
60 | font-weight: bold;
61 | font-size: 18px;
62 | transition: 0.6s ease;
63 | border-radius: 0 3px 3px 0;
64 | user-select: none;
65 | background-color: rgba(0, 0, 0, 0.4);
66 | }
67 |
68 | /* Position the "next button" to the right */
69 | .preview-next {
70 | right: 0;
71 | border-radius: 3px 0 0 3px;
72 | }
73 |
74 | /* Position the "prev button" to the left */
75 | .preview-prev {
76 | left: 0;
77 | border-radius: 3px 0 0 3px;
78 | }
79 |
80 | /* On hover, add a black background color with a little bit see-through */
81 | .preview-prev:hover,
82 | .preview-next:hover {
83 | background-color: rgba(0, 0, 0, 0.8);
84 | }
85 |
86 | .main-container {
87 | display: flex;
88 | justify-content: center;
89 | align-items: center;
90 | flex-direction: column;
91 | }
92 |
93 | .dwc-center{
94 | display: flex;
95 | justify-content: center;
96 | align-items: center;
97 | }
98 |
99 | .dwc-modal-header {
100 | display: flex;
101 | justify-content: space-between;
102 | align-items: center;
103 | padding: 2px 10px;
104 | padding-top: 10px;
105 | padding-bottom: 10px;
106 | background-color: black;
107 | color: white;
108 | }
109 |
110 | .dwc-modal-body {
111 | padding: 2px 16px;
112 | height: 100%;
113 | }
114 |
115 | .dwc-modal {
116 | display: none;
117 | position: fixed;
118 | z-index: 1;
119 | left: 0;
120 | top: 0;
121 | width: 100%;
122 | height: 100%;
123 | background-color: rgb(0, 0, 0);
124 | background-color: rgba(0, 0, 0, 0.4);
125 | }
126 |
127 | .dwc-modal-img-container{
128 | height: 700px;
129 | max-width:100%;
130 | margin: auto;
131 | }
132 |
133 | .dwc-modal-img {
134 | display: block;
135 | max-height: 90%;
136 | }
137 |
138 | /* Modal Content */
139 | .dwc-modal-content {
140 | background-color: rgba(0, 0, 0, 0.8);
141 | position: relative;
142 | margin: auto;
143 | padding: 0;
144 | border: 1px solid #888;
145 | width: 100%;
146 | height: 100%;
147 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
148 | animation-name: animatetop;
149 | animation-duration: 0.4s
150 | }
151 |
152 | .dwc-modal-notetext-container{
153 | background: white;
154 | padding-left: 5px;
155 | width:20%;
156 | display: flex;
157 | flex-wrap: wrap
158 | }
159 |
160 | .dwc-modal-notetext{
161 | width: 100%;
162 | word-break: break-word;
163 | }
164 |
165 | .dwc-hide{
166 | display: none !important;
167 | }
168 |
169 | .dwc-pdf-canvas-container{
170 | width: 100%;
171 | overflow: auto;
172 | text-align: center;
173 | }
174 |
175 | .dwc-flex-container{
176 | display: flex;
177 | }
178 |
179 | .dwc-overflow{
180 | overflow: auto;
181 | }
182 |
183 | .dwc-pageinput-container{
184 | margin-left: 4px;
185 | margin-right: 4px;
186 | align-items: center;
187 | }
188 |
189 | .dwc-side-margin-4{
190 | margin-left: 4px;
191 | margin-right: 4px;
192 | }
193 |
194 | .dwc-zoom-container{
195 | display: flex;
196 | align-items: center;
197 | }
198 |
199 | .dwc-page-input {
200 | width: 50px;
201 | background-color: #7b7b7b;
202 | border: none;
203 | color: white;
204 | text-align: right;
205 | padding-right: 4px;
206 | height: 24px;
207 | font-size: 18px;
208 | }
209 |
210 | .dwc-page-span{
211 | font-size:20px;
212 | }
213 |
214 | .dwc-top-right {
215 | position: absolute;
216 | top: 8px;
217 | right: 16px;
218 | }
219 |
220 | /* Add Animation */
221 | @keyframes animatetop {
222 | from {
223 | top: -300px;
224 | opacity: 0
225 | }
226 |
227 | to {
228 | top: 0;
229 | opacity: 1
230 | }
231 | }
--------------------------------------------------------------------------------
/AttachmentGallery/img/pdf_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OOlashyn/PCF-AttachmentGalleryControl/fdfec2d04dc0f3e9aa575ccdfbf9ff31c7021cb8/AttachmentGallery/img/pdf_icon.png
--------------------------------------------------------------------------------
/AttachmentGallery/index.ts:
--------------------------------------------------------------------------------
1 | import {IInputs, IOutputs} from "./generated/ManifestTypes";
2 | import { saveAs } from 'file-saver';
3 |
4 | interface Attachment {
5 | documentBody: string;
6 | mimeType: string;
7 | title: string;
8 | noteText: string;
9 | id: string;
10 | filename: string;
11 | }
12 |
13 | interface IModalState {
14 | isOpen: boolean,
15 | isPdfViewerOpen: boolean
16 | }
17 |
18 | interface IPdfState {
19 | pdf: any,
20 | currentPage: number,
21 | zoom: number
22 | }
23 |
24 | export class AttachmentGallery implements ComponentFramework.StandardControl {
25 | private _previewImg: HTMLImageElement;
26 | private _thumbnailsGallery: HTMLDivElement;
27 | private _container: HTMLDivElement;
28 | private _context: ComponentFramework.Context;
29 | private _currentIndex: number;
30 | private _notes: Attachment[];
31 | private _modalContainer: HTMLDivElement;
32 | private _modalContentContainer: HTMLDivElement;
33 | private _modalHeaderText: HTMLHeadingElement;
34 | private _modalImage: HTMLImageElement;
35 | private _noteText: HTMLParagraphElement;
36 | private _noteTextContainer: HTMLDivElement;
37 | private _pdfCanvas: HTMLCanvasElement;
38 | private pdfState: IPdfState;
39 | private modalState: IModalState;
40 | private _pdfViewerContainer: HTMLDivElement;
41 | private _modalImageContainer: HTMLDivElement;
42 | private _pdfPageInput: HTMLInputElement;
43 | private _pdfTotalPages: HTMLSpanElement;
44 | private _pdfPageControlsContainer: HTMLDivElement;
45 | private _imageViewerContainer: HTMLDivElement;
46 | private pdfImageSrc: string;
47 | private _mainDivContainer: HTMLDivElement;
48 | private _notFoundContainer: HTMLDivElement;
49 |
50 | /**
51 | * Empty constructor.
52 | */
53 | constructor()
54 | {
55 |
56 | }
57 |
58 | /**
59 | * Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here.
60 | * Data-set values are not initialized here, use updateView.
61 | * @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to property names defined in the manifest, as well as utility functions.
62 | * @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously.
63 | * @param state A piece of data that persists in one session for a single user. Can be set at any point in a controls life cycle by calling 'setControlState' in the Mode interface.
64 | * @param container If a control is marked control-type='standard', it will receive an empty div element within which it can render its content.
65 | */
66 | public init(context: ComponentFramework.Context, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container:HTMLDivElement): void
67 | {
68 | this._notes = [];
69 | this._context = context;
70 | this.setPreview = this.setPreview.bind(this);
71 | this.openModal = this.openModal.bind(this);
72 | this.closeModal = this.closeModal.bind(this);
73 | this.changeImage = this.changeImage.bind(this);
74 | this.setPreviewFromThumbnail = this.setPreviewFromThumbnail.bind(this);
75 | this.toggleNoteColumn = this.toggleNoteColumn.bind(this);
76 | this.generateImageSrcUrl = this.generateImageSrcUrl.bind(this);
77 | //this.GetAttachmentsDemo = this.GetAttachmentsDemo.bind(this);
78 | this.b64toBlob = this.b64toBlob.bind(this);
79 | this.downloadFile = this.downloadFile.bind(this);
80 | this.setPdfViewer = this.setPdfViewer.bind(this);
81 | this.pdfRender = this.pdfRender.bind(this);
82 | this.togglePdfViwer = this.togglePdfViwer.bind(this);
83 | this.setModalImage = this.setModalImage.bind(this);
84 | this.changePdfPage = this.changePdfPage.bind(this);
85 | this.zoomPdfPage = this.zoomPdfPage.bind(this);
86 | this.setPdfPage = this.setPdfPage.bind(this);
87 |
88 | this._context.resources.getResource('img/pdf_icon.png',
89 | this.setPdfImage.bind(this), this.showError.bind(this,"ERROR with PDF Image!"));
90 |
91 | this.modalState = {
92 | isOpen: false,
93 | isPdfViewerOpen: false
94 | }
95 |
96 | this.pdfState = {
97 | pdf: null,
98 | currentPage: 1,
99 | zoom: 1
100 | }
101 |
102 | this._container = document.createElement('div');
103 |
104 | //--------- Attachments not found placeholder
105 | this._notFoundContainer = document.createElement('div');
106 |
107 | let refreshIcon = document.createElement('i');
108 | refreshIcon.className = 'dwc-top-right ms-Icon ms-Icon--Refresh';
109 |
110 | this._notFoundContainer.appendChild(refreshIcon);
111 |
112 | let notFoundText = document.createElement('p');
113 | notFoundText.classList.add('dwc-center');
114 | notFoundText.innerText = 'Attachments not found. Press Refresh to try to load them again.';
115 |
116 | this._notFoundContainer.appendChild(notFoundText);
117 |
118 | this._container.appendChild(this._notFoundContainer);
119 |
120 | let mainContainer = document.createElement('div');
121 | mainContainer.classList.add('main-container','dwc-hide');
122 |
123 | this._mainDivContainer = mainContainer;
124 |
125 | //-------- creating thumbnails
126 | this._thumbnailsGallery = document.createElement("div");
127 | this._thumbnailsGallery.classList.add('thumbnailsList');
128 |
129 | //-------- creating preview section
130 | let bigPreview = document.createElement("div");
131 | bigPreview.classList.add('preview-section');
132 |
133 | this._previewImg = document.createElement('img');
134 | this._previewImg.classList.add('preview-img');
135 | this._previewImg.onclick = () => this.openModal();
136 | bigPreview.appendChild(this._previewImg);
137 |
138 | //-------- prev and next buttons
139 | let next = document.createElement('a');
140 | next.classList.add('arrow-button', 'preview-next');
141 | next.innerHTML = "❯";
142 | next.onclick = () => this.changeImage(1);
143 |
144 | let prev = document.createElement('a');
145 | prev.classList.add('arrow-button', 'preview-prev');
146 | prev.innerHTML = "❮";
147 | prev.onclick = () => this.changeImage(-1);
148 |
149 | bigPreview.appendChild(prev);
150 | bigPreview.appendChild(next);
151 |
152 | mainContainer.appendChild(bigPreview);
153 | mainContainer.appendChild(this._thumbnailsGallery);
154 |
155 | this._container.appendChild(mainContainer);
156 |
157 | container.appendChild(this._container);
158 |
159 | //--------- create modal
160 |
161 | this._modalContainer = document.createElement('div');
162 | this._modalContainer.classList.add('dwc-modal');
163 |
164 | let modalContent = document.createElement('div');
165 | modalContent.classList.add('dwc-modal-content');
166 |
167 | this._modalContentContainer = modalContent;
168 |
169 | //--------- create modal header
170 | let modalHeader = document.createElement('div');
171 | modalHeader.classList.add('dwc-modal-header');
172 |
173 | //--------- create pdf controls
174 | let pdfControlsContainer = document.createElement('div');
175 |
176 | pdfControlsContainer.classList.add('dwc-flex-container', 'dwc-pageinput-container', 'dwc-hide');
177 |
178 | let prevPdfPageIcon = document.createElement('i');
179 | prevPdfPageIcon.className = "header-icon ms-Icon ms-Icon--ChevronLeft";
180 | prevPdfPageIcon.addEventListener('click', () => this.changePdfPage(-1));
181 |
182 | pdfControlsContainer.appendChild(prevPdfPageIcon);
183 |
184 | let pdfPageNumberContainer = document.createElement('div');
185 |
186 | let pdfPageInput = document.createElement('input');
187 | pdfPageInput.value = '1';
188 | pdfPageInput.className = 'dwc-page-input';
189 | pdfPageInput.addEventListener('keypress', (e) => this.setPdfPage(e));
190 |
191 | pdfPageNumberContainer.appendChild(pdfPageInput);
192 |
193 | this._pdfPageInput = pdfPageInput;
194 |
195 | let totalPagesSpan = document.createElement('span');
196 | totalPagesSpan.innerHTML = " / 0";
197 | totalPagesSpan.className = 'dwc-page-span';
198 |
199 | pdfPageNumberContainer.appendChild(totalPagesSpan);
200 |
201 | this._pdfTotalPages = totalPagesSpan;
202 |
203 | pdfControlsContainer.appendChild(pdfPageNumberContainer);
204 |
205 | let nextPdfPageIcon = document.createElement('i');
206 | nextPdfPageIcon.className = "header-icon ms-Icon ms-Icon--ChevronRight";
207 | nextPdfPageIcon.addEventListener('click', () => this.changePdfPage(1));
208 |
209 | pdfControlsContainer.appendChild(nextPdfPageIcon);
210 |
211 | let zoomControlsContainer = document.createElement('div');
212 | zoomControlsContainer.className = 'dwc-zoom-container';
213 |
214 | let zoomText = document.createElement('span');
215 | zoomText.style.color = 'white';
216 | zoomText.innerHTML = 'Zoom: ';
217 |
218 | let plusIcon = document.createElement('i');
219 | plusIcon.className = "header-icon dwc-side-margin-4 ms-Icon ms-Icon--Add";
220 | plusIcon.addEventListener('click', () => this.zoomPdfPage(0.2));
221 |
222 | let minusIcon = document.createElement('i');
223 | minusIcon.className = "header-icon dwc-side-margin-4 ms-Icon ms-Icon--Remove";
224 | minusIcon.addEventListener('click', () => this.zoomPdfPage(-0.2));
225 |
226 | zoomControlsContainer.appendChild(zoomText);
227 | zoomControlsContainer.appendChild(plusIcon);
228 | zoomControlsContainer.appendChild(minusIcon);
229 |
230 | pdfControlsContainer.appendChild(zoomControlsContainer);
231 |
232 | this._pdfPageControlsContainer = pdfControlsContainer;
233 |
234 | //---------- create modal buttons
235 | let rightHeaderContainer = document.createElement('div');
236 |
237 | rightHeaderContainer.classList.add("header-icon-container");
238 |
239 | let downloadIcon = document.createElement('i');
240 | downloadIcon.className = "header-icon ms-Icon ms-Icon--Download";
241 | downloadIcon.addEventListener('click', this.downloadFile);
242 |
243 | rightHeaderContainer.appendChild(downloadIcon);
244 |
245 | let infoIcon = document.createElement('i');
246 | infoIcon.className = "header-icon ms-Icon ms-Icon--Info";
247 | infoIcon.addEventListener('click', this.toggleNoteColumn);
248 |
249 | rightHeaderContainer.appendChild(infoIcon);
250 |
251 | let closeIcon = document.createElement('i');
252 | closeIcon.className = "header-icon ms-Icon ms-Icon--ChromeClose";
253 | closeIcon.addEventListener('click', this.closeModal);
254 |
255 | rightHeaderContainer.appendChild(closeIcon);
256 |
257 | //--------- create modal header text
258 | let leftHeaderContainer = document.createElement('div');
259 |
260 | let headerText = document.createElement('h3');
261 | headerText.innerHTML = "0/10";
262 | this._modalHeaderText = headerText;
263 |
264 | leftHeaderContainer.appendChild(headerText);
265 |
266 | modalHeader.appendChild(leftHeaderContainer);
267 | modalHeader.appendChild(pdfControlsContainer);
268 | modalHeader.appendChild(rightHeaderContainer);
269 |
270 | modalContent.appendChild(modalHeader);
271 |
272 | //--------- create modal body
273 |
274 | let modalBody = document.createElement('div');
275 | modalBody.classList.add('dwc-modal-body');
276 |
277 | //--------- add prev/next buttons
278 |
279 | let nextImgModal = document.createElement('a');
280 | nextImgModal.classList.add('arrow-button', 'preview-next');
281 | nextImgModal.innerHTML = "❯";
282 | nextImgModal.onclick = () => this.changeImage(1);
283 |
284 | let prevImgModal = document.createElement('a');
285 | prevImgModal.classList.add('arrow-button', 'preview-prev');
286 | prevImgModal.innerHTML = "❮";
287 | prevImgModal.onclick = () => this.changeImage(-1);
288 |
289 | modalBody.appendChild(nextImgModal);
290 | modalBody.appendChild(prevImgModal);
291 |
292 | //--------- image container
293 |
294 | let imageViewerContainer = document.createElement('div');
295 | imageViewerContainer.className = 'dwc-flex-container';
296 |
297 | this._imageViewerContainer = imageViewerContainer;
298 |
299 | modalBody.appendChild(imageViewerContainer);
300 |
301 | let modalImageContainer = document.createElement('div');
302 | modalImageContainer.classList.add('dwc-modal-img-container');
303 |
304 | this._modalImage = document.createElement('img');
305 | this._modalImage.classList.add('dwc-modal-img');
306 |
307 | modalImageContainer.appendChild(this._modalImage);
308 | imageViewerContainer.appendChild(modalImageContainer);
309 |
310 | this._modalImageContainer = modalImageContainer;
311 |
312 | //-------- create preview note container
313 |
314 | let modalNoteTextContainer = document.createElement('div');
315 | modalNoteTextContainer.classList.add('dwc-modal-notetext-container', 'dwc-hide');
316 |
317 | let noteText = document.createElement('p');
318 | noteText.className = 'dwc-modal-notetext';
319 | modalNoteTextContainer.appendChild(noteText);
320 |
321 | this._noteText = noteText;
322 | this._noteTextContainer = modalNoteTextContainer;
323 |
324 | imageViewerContainer.appendChild(modalNoteTextContainer);
325 |
326 | //--------- create pdf viewer container
327 |
328 | let pdfViewerContainer = document.createElement('div');
329 | pdfViewerContainer.classList.add('dwc-hide');
330 |
331 | let canvasContainer = document.createElement('div');
332 | canvasContainer.className = 'dwc-pdf-canvas-container';
333 | canvasContainer.id = "canvas_container";
334 |
335 | this._pdfCanvas = document.createElement('canvas');
336 | this._pdfCanvas.id = "pdf_renderer";
337 |
338 | canvasContainer.appendChild(this._pdfCanvas);
339 |
340 | pdfViewerContainer.appendChild(canvasContainer);
341 |
342 | modalBody.appendChild(pdfViewerContainer);
343 |
344 | this._pdfViewerContainer = pdfViewerContainer;
345 |
346 | modalContent.appendChild(modalBody);
347 | this._modalContainer.appendChild(modalContent);
348 |
349 | document.body.appendChild(this._modalContainer);
350 |
351 | console.log("context", context);
352 |
353 | let curentRecord: ComponentFramework.EntityReference = {
354 | id: (context).page.entityId,
355 | name: (context).page.entityTypeName
356 | }
357 |
358 | console.log("curentRecord", curentRecord);
359 |
360 | const pdfScript = document.createElement('script');
361 | pdfScript.src = 'https://unpkg.com/pdfjs-dist@3.11.174/build/pdf.min.js';
362 |
363 | document.body.appendChild(pdfScript);
364 |
365 | this.GetAttachments(curentRecord).then(result => this.CreateGallery(result));
366 | }
367 |
368 | /**
369 | * Generate Image Element src url
370 | * @param fileType file extension
371 | * @param fileContent file content, base 64 format
372 | */
373 | private generateImageSrcUrl(fileType: string, fileContent: string): string {
374 | return "data:" + fileType + ";base64, " + fileContent;
375 | }
376 |
377 | private toggleNoteColumn(): void {
378 | if (this.modalState.isPdfViewerOpen) return;
379 |
380 | if (this._noteTextContainer.classList.contains('dwc-hide')) {
381 | this._noteTextContainer.classList.remove('dwc-hide');
382 | } else {
383 | this._noteTextContainer.classList.add('dwc-hide');
384 | }
385 | }
386 |
387 | private openModal(): void {
388 | this._modalContainer.style.display = "block";
389 | this.modalState.isOpen = true;
390 | let currentNote = this._notes[this._currentIndex];
391 |
392 | this.setModalImage(currentNote);
393 | }
394 |
395 | private closeModal(): void {
396 | this._modalContainer.style.display = "none";
397 | this.modalState.isOpen = false;
398 | }
399 |
400 | private async GetAttachments(curentRecord: ComponentFramework.EntityReference): Promise {
401 | console.log("GetAttachments started");
402 |
403 | const searchQuery = "?$select=annotationid,documentbody,mimetype,notetext,subject,filename" +
404 | "&$filter=_objectid_value eq " +
405 | curentRecord.id +
406 | " and isdocument eq true and (startswith(mimetype, 'application/pdf') or startswith(mimetype, 'image/'))";
407 | try {
408 |
409 | const result = await this._context.webAPI.retrieveMultipleRecords("annotation", searchQuery);
410 |
411 | console.log("Retrieved attachments", result);
412 | if (result && result.entities) {
413 | for (let index = 0; index < result.entities.length; index++) {
414 |
415 | let item: Attachment = {
416 | id: result.entities[index].id,
417 | mimeType: result.entities[index].mimetype,
418 | noteText: result.entities[index].notetext,
419 | title: result.entities[index].subject,
420 | filename: result.entities[index].filename,
421 | documentBody: result.entities[index].documentbody
422 | };
423 | this._notes.push(item);
424 | }
425 | }
426 | } catch (error) {
427 | console.error("ERROR RETRIEVING ATTACHMENT");
428 | console.error(error);
429 | }
430 |
431 | return this._notes;
432 | }
433 |
434 | private setPdfImage(body:string){
435 | this.pdfImageSrc = this.generateImageSrcUrl('image/png',body);
436 | }
437 |
438 | private showError(text:string){
439 | console.error("ERROR:", text);
440 | }
441 |
442 | private CreateGallery(result: Attachment[]): any {
443 | console.log("Create Gallery Started");
444 | console.log("Notes: ", result);
445 | if (result.length > 0) {
446 | let count = 0;
447 | for (let i = 0; i < result.length; i++) {
448 | let newImg = document.createElement('img');
449 | newImg.className = "thumbnail";
450 | newImg.src = result[i].mimeType.indexOf('pdf') == -1
451 | ? this.generateImageSrcUrl(result[i].mimeType, result[i].documentBody)
452 | : this.pdfImageSrc;
453 | newImg.alt = count.toString();
454 | newImg.addEventListener('click', this.setPreviewFromThumbnail);
455 | this._thumbnailsGallery.appendChild(newImg);
456 | count++;
457 | }
458 |
459 | this._notFoundContainer.classList.add('dwc-hide');
460 | this._mainDivContainer.classList.remove('dwc-hide');
461 |
462 | this._currentIndex = 0;
463 | this.setPreview(0);
464 | }
465 | }
466 |
467 | private setPreviewFromThumbnail(e: Event) {
468 | let currentImgIndex = parseInt((e.srcElement as HTMLImageElement).alt);
469 | this.setPreview(currentImgIndex);
470 | }
471 |
472 | private setModalImage(note: Attachment) {
473 | let isAttachmentPdf = note.mimeType.indexOf('pdf') != -1;
474 |
475 | if (this.modalState.isOpen && !this.modalState.isPdfViewerOpen && isAttachmentPdf) {
476 | this.togglePdfViwer(true);
477 | this.setPdfViewer(note);
478 | } else {
479 | if (this.modalState.isOpen && this.modalState.isPdfViewerOpen && isAttachmentPdf) {
480 | this.setPdfViewer(note);
481 | } else {
482 | if (this.modalState.isOpen && this.modalState.isPdfViewerOpen && !isAttachmentPdf) {
483 | this.togglePdfViwer(false);
484 | }
485 | this._modalImage.src = this._previewImg.src;
486 | }
487 | }
488 | }
489 |
490 | private setPreview(currentNoteNumber: number) {
491 | if (currentNoteNumber === this._notes.length) { currentNoteNumber = 0 }
492 | if (currentNoteNumber < 0) { currentNoteNumber = this._notes.length - 1 }
493 | let currentNote = this._notes[currentNoteNumber];
494 |
495 | let isAttachmentPdf = currentNote.mimeType.indexOf('pdf') != -1;
496 |
497 | this._previewImg.src = !isAttachmentPdf
498 | ? this.generateImageSrcUrl(currentNote.mimeType, currentNote.documentBody)
499 | : this.pdfImageSrc;
500 |
501 | this._modalHeaderText.innerHTML = (currentNoteNumber + 1).toString() + " / " + this._notes.length.toString()
502 | + " " + currentNote.title;
503 |
504 | if (this.modalState.isOpen) {
505 | this.setModalImage(currentNote);
506 | }
507 |
508 | this._noteText.innerHTML = currentNote.noteText;
509 | this._currentIndex = currentNoteNumber;
510 | }
511 |
512 | private changeImage(moveIndex: number) {
513 | this.setPreview(this._currentIndex += moveIndex);
514 | }
515 |
516 | //=========== FILE DOWNLOAD LOGIC ===============
517 |
518 | private b64toBlob(b64Data: string, contentType: string, sliceSize: number): Blob {
519 | contentType = contentType || '';
520 | sliceSize = sliceSize || 512;
521 |
522 | var byteCharacters = atob(b64Data);
523 | var byteArrays = [];
524 |
525 | for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
526 | var slice = byteCharacters.slice(offset, offset + sliceSize);
527 |
528 | var byteNumbers = new Array(slice.length);
529 | for (var i = 0; i < slice.length; i++) {
530 | byteNumbers[i] = slice.charCodeAt(i);
531 | }
532 |
533 | var byteArray = new Uint8Array(byteNumbers);
534 |
535 | byteArrays.push(byteArray);
536 | }
537 |
538 | var blob = new Blob(byteArrays, { type: contentType });
539 | return blob;
540 | }
541 |
542 | private downloadFile(): void {
543 | let note = this._notes[this._currentIndex];
544 | let blob = this.b64toBlob(note.documentBody, note.mimeType, 512);
545 | saveAs(blob, note.filename);
546 | }
547 |
548 |
549 | //=========== PDF LOGIC
550 |
551 | private setPdfViewer(pdfItem: Attachment) {
552 | let pdfData = atob(pdfItem.documentBody);
553 | // @ts-ignore
554 | pdfjsLib.getDocument({ data: pdfData }).promise.then((pdf) => {
555 | this.pdfState.pdf = pdf;
556 | this.pdfState.currentPage = 1;
557 | this.pdfState.zoom = 1;
558 | this._pdfPageInput.value = '1';
559 |
560 | this._pdfTotalPages.innerHTML = " / " + pdf._pdfInfo.numPages.toString();
561 | this.pdfRender();
562 | });
563 | }
564 |
565 | private pdfRender() {
566 | // @ts-ignore
567 | this.pdfState.pdf.getPage(this.pdfState.currentPage).then((page) => {
568 |
569 | let canvas = this._pdfCanvas;
570 | var ctx = canvas.getContext('2d');
571 | var viewport = page.getViewport({
572 | scale: this.pdfState.zoom
573 | });
574 |
575 | canvas.width = viewport.width;
576 | canvas.height = viewport.height
577 |
578 | page.render({
579 | canvasContext: ctx,
580 | viewport: viewport
581 | });
582 | });
583 | }
584 |
585 | private togglePdfViwer(visible: boolean): void {
586 | if (visible) {
587 | this._pdfViewerContainer.classList.remove("dwc-hide");
588 | this._pdfPageControlsContainer.classList.remove("dwc-hide");
589 | this._imageViewerContainer.classList.add("dwc-hide");
590 | this._modalContentContainer.classList.add('dwc-overflow');
591 | this.modalState.isPdfViewerOpen = true;
592 | } else {
593 | this._pdfViewerContainer.classList.add("dwc-hide");
594 | this._pdfPageControlsContainer.classList.add("dwc-hide");
595 | this._imageViewerContainer.classList.remove("dwc-hide");
596 | this._modalContentContainer.classList.remove('dwc-overflow');
597 | this.modalState.isPdfViewerOpen = false;
598 | }
599 | }
600 |
601 | private setPdfPage(event: KeyboardEvent): void {
602 | if (this.pdfState.pdf == null) return;
603 |
604 | // Get key code
605 | let code = (event.keyCode ? event.keyCode : event.which);
606 |
607 | // If key code matches that of the Enter key
608 | if (code == 13) {
609 | let desiredPage = parseInt(this._pdfPageInput.value);
610 |
611 | if (desiredPage >= 1 &&
612 | desiredPage <= this.pdfState.pdf._pdfInfo.numPages) {
613 |
614 | this.pdfState.currentPage = desiredPage;
615 | this.pdfRender();
616 | }
617 | }
618 | }
619 |
620 | private changePdfPage(pageShift: number): void {
621 | if (this.pdfState.pdf == null) return;
622 |
623 | let nextPage = this.pdfState.currentPage;
624 | nextPage += pageShift;
625 |
626 | if (nextPage <= 0 || nextPage > this.pdfState.pdf._pdfInfo.numPages) return;
627 |
628 | this.pdfState.currentPage = nextPage;
629 | this._pdfPageInput.value = this.pdfState.currentPage.toString();
630 | this.pdfRender();
631 | }
632 |
633 | private zoomPdfPage(zoomChange: number): void {
634 | if (this.pdfState.pdf == null) return;
635 | this.pdfState.zoom += zoomChange;
636 | this.pdfRender();
637 | }
638 |
639 | //---------- END PDF LOGIC
640 |
641 | /**
642 | * Called when any value in the property bag has changed. This includes field values, data-sets, global values such as container height and width, offline status, control metadata values such as label, visible, etc.
643 | * @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to names defined in the manifest, as well as utility functions
644 | */
645 | public updateView(context: ComponentFramework.Context): void
646 | {
647 | // Add code to update control view
648 | }
649 |
650 | /**
651 | * It is called by the framework prior to a control receiving new data.
652 | * @returns an object based on nomenclature defined in manifest, expecting object[s] for property marked as “bound” or “output”
653 | */
654 | public getOutputs(): IOutputs
655 | {
656 | return {};
657 | }
658 |
659 | /**
660 | * Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup.
661 | * i.e. cancelling any pending remote calls, removing listeners, etc.
662 | */
663 | public destroy(): void
664 | {
665 | // Add code to cleanup control if necessary
666 | }
667 | }
668 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Oleksandr Olashyn (dancingwithcrm)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/PCF-AttachmentGalleryControl.pcfproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\PowerApps
5 |
6 |
7 |
8 |
9 |
10 |
11 | PCF-AttachmentGalleryControl
12 | 30bf79fe-db2c-4573-b122-47af8bd57c3b
13 | $(MSBuildThisFileDirectory)out\controls
14 |
15 |
16 |
17 | v4.6.2
18 |
19 | net462
20 | PackageReference
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://vshymanskyy.github.io/StandWithUkraine)
2 |
3 | # Attachment Gallery Control
4 | 
5 |
6 | This is PCF control that shows images and pdf from attached notes and also can show note title and note text.
7 |
8 | 
9 |
10 | **IMPORTANT: pdf.js update to v4 broke existing functionality (see issue [#36](https://github.com/OOlashyn/PCF-AttachmentGalleryControl/issues/36)). Unfortunately, this control was created before PCF was GA and fixing the issue while preserving correct naming is not possible. To solve this issue please delete the previous solution and install the latest [1.0.0.1 release](https://github.com/OOlashyn/PCF-AttachmentGalleryControl/releases/)**
11 |
12 | Update to version 1.0.0
13 |
14 | * UI update - Using Office Fabric UI for icons.
15 | * PDF Support - Added build-in pdf reader using pdf.js library.
16 | * Note text - added ability for big text to reflow to next line.
17 | * File download - added ability to download the file.
18 | * Full Screen - open images and pdf files in full screen.
19 |
20 | ## Download Attachment Gallery Control
21 |
22 | [Download](https://github.com/OOlashyn/PCF-AttachmentGalleryControl/releases/)
23 |
--------------------------------------------------------------------------------
/Screenshots/gallery-v1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OOlashyn/PCF-AttachmentGalleryControl/fdfec2d04dc0f3e9aa575ccdfbf9ff31c7021cb8/Screenshots/gallery-v1.gif
--------------------------------------------------------------------------------
/Solution/AttachmentGallerySol/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # msbuild output directories
4 | /bin
5 | /obj
6 |
7 | # MSBuild Binary and Structured Log
8 | *.binlog
9 |
--------------------------------------------------------------------------------
/Solution/AttachmentGallerySol/AttachmentGallerySol.cdsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\PowerApps
5 |
6 |
7 |
8 |
9 |
10 |
11 | ed3e21c7-45ae-4cbe-9304-4352cbffcff0
12 | v4.6.2
13 |
14 | net462
15 | PackageReference
16 | src
17 |
18 |
19 |
23 |
24 |
25 | Both
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | PreserveNewest
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/Solution/AttachmentGallerySol/src/Other/Customizations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | 1033
17 |
18 |
--------------------------------------------------------------------------------
/Solution/AttachmentGallerySol/src/Other/Relationships.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/Solution/AttachmentGallerySol/src/Other/Solution.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | AttachmentGallerySol
6 |
7 |
8 |
9 |
10 |
11 | 1.0.0.1
12 |
13 | 2
14 |
15 |
16 | dancingwithcrm
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | dwcrm
29 |
30 | 32659
31 |
32 |
33 |
34 | 1
35 | 1
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | 1
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | 2
63 | 1
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | 1
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pcf-project",
3 | "version": "1.0.0",
4 | "description": "Project containing your PowerApps Component Framework (PCF) control.",
5 | "scripts": {
6 | "build": "pcf-scripts build",
7 | "clean": "pcf-scripts clean",
8 | "lint": "pcf-scripts lint",
9 | "lint:fix": "pcf-scripts lint fix",
10 | "rebuild": "pcf-scripts rebuild",
11 | "start": "pcf-scripts start",
12 | "start:watch": "pcf-scripts start watch",
13 | "refreshTypes": "pcf-scripts refreshTypes"
14 | },
15 | "dependencies": {
16 | "file-saver": "^2.0.5"
17 | },
18 | "devDependencies": {
19 | "@microsoft/eslint-plugin-power-apps": "^0.2.6",
20 | "@types/file-saver": "^2.0.7",
21 | "@types/node": "^18.8.2",
22 | "@types/powerapps-component-framework": "^1.3.4",
23 | "@typescript-eslint/eslint-plugin": "^5.39.0",
24 | "@typescript-eslint/parser": "^5.39.0",
25 | "eslint": "^8.24.0",
26 | "eslint-plugin-import": "^2.26.0",
27 | "eslint-plugin-node": "^11.1.0",
28 | "eslint-plugin-promise": "^6.0.1",
29 | "pcf-scripts": "^1",
30 | "pcf-start": "^1",
31 | "typescript": "^4.8.4"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/pcfconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "outDir": "./out/controls"
3 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./node_modules/pcf-scripts/tsconfig_base.json",
3 | "compilerOptions": {
4 | "typeRoots": ["node_modules/@types"],
5 | }
6 | }
--------------------------------------------------------------------------------