├── .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 | [![SWUbanner](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner-direct-single.svg)](https://vshymanskyy.github.io/StandWithUkraine) 2 | 3 | # Attachment Gallery Control 4 | ![GitHub all releases](https://img.shields.io/github/downloads/OOlashyn/PCF-AttachmentGalleryControl/total) 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 | ![Demo](https://github.com/OOlashyn/PCF-AttachmentGalleryControl/blob/master/Screenshots/gallery-v1.gif?raw=true) 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 | } --------------------------------------------------------------------------------