├── solution ├── .gitignore ├── other │ ├── Relationships.xml │ ├── Customizations.xml │ └── Solution.xml └── solution.cdsproj ├── pcfconfig.json ├── docs └── PCF-Gallery-Control.gif ├── tsconfig.json ├── obj ├── PCFGalleryControl.pcfproj.nuget.cache ├── PCFGalleryControl.pcfproj.nuget.g.targets ├── PCFGalleryControl.pcfproj.nuget.g.props └── project.assets.json ├── .gitignore ├── README.md ├── package.json ├── PCFGalleryControl.pcfproj ├── GalleryFieldControl ├── css │ └── GalleryFieldControl.css ├── ControlManifest.Input.xml └── index.ts └── LICENSE /solution/.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | /obj -------------------------------------------------------------------------------- /pcfconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "outDir": "./out/controls" 3 | } -------------------------------------------------------------------------------- /docs/PCF-Gallery-Control.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DynamicsNinja/PCF-Gallery-Control/HEAD/docs/PCF-Gallery-Control.gif -------------------------------------------------------------------------------- /solution/other/Relationships.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/pcf-scripts/tsconfig_base.json", 3 | "compilerOptions": { 4 | "typeRoots": ["node_modules/@types"], 5 | } 6 | } -------------------------------------------------------------------------------- /obj/PCFGalleryControl.pcfproj.nuget.cache: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dgSpecHash": "cPP26Jr/hyfKSvGYqhp6d+KXCKsdORTIVsOvz0JdCqcWAHajt4Yc/Xq4kgqfNGD5N7a+JL3TjCQLUR44zlVO4Q==", 4 | "success": true 5 | } -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /solution/other/Customizations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 1033 16 | 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PCF Gallery Control 2 | 3 | ## Overview 4 | 5 | PCF control that shows thumbnails of images from entity notes. Control is bound to Single Line of Text field. 6 | 7 | ## Download 8 | 9 | [![download](https://user-images.githubusercontent.com/14048382/27844360-c7ea9670-6174-11e7-8658-80d356c1ba8f.png)](https://github.com/DynamicsNinja/PCF-Gallery-Control/releases/latest) 10 | 11 | ## Preview 12 | 13 | ![PCF-Gallery-Control](docs/PCF-Gallery-Control.gif) 14 | 15 | ## Features 16 | 17 | - Thumbnails of images from notes & email attachments 18 | - Image preview on click on the thumbnail 19 | - Hide preview image on click 20 | - Download image on click on the filename -------------------------------------------------------------------------------- /obj/PCFGalleryControl.pcfproj.nuget.g.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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 | "rebuild": "pcf-scripts rebuild", 9 | "start": "pcf-scripts start" 10 | }, 11 | "dependencies": { 12 | "@types/file-saver": "^2.0.1", 13 | "@types/jquery": "^3.3.29", 14 | "@types/node": "^10.12.18", 15 | "@types/powerapps-component-framework": "1.1.0", 16 | "file-saver": "^2.0.2", 17 | "jquery": "^3.4.1" 18 | }, 19 | "devDependencies": { 20 | "pcf-scripts": "~0", 21 | "pcf-start": "~0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /PCFGalleryControl.pcfproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PCFGalleryControl 7 | 2b4792c9-00d9-487b-b13f-8a2b987ba212 8 | $(MSBuildThisFileDirectory)out\controls 9 | 10 | 11 | 12 | PackageReference 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Always 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /GalleryFieldControl/css/GalleryFieldControl.css: -------------------------------------------------------------------------------- 1 | .Fic\.GalleryFieldControl .thumbnail img { 2 | border: 1px solid #ddd; 3 | border-radius: 4px; 4 | padding: 5px; 5 | } 6 | 7 | .Fic\.GalleryFieldControl .thumbnail img:hover { 8 | box-shadow: 0 0 2px 1px rgba(0, 140, 186, 0.5); 9 | cursor: pointer; 10 | } 11 | 12 | .Fic\.GalleryFieldControl .thumbnail-container { 13 | display: inline-block; 14 | margin-left: 5px; 15 | } 16 | 17 | .Fic\.GalleryFieldControl .file-name { 18 | white-space: nowrap; 19 | overflow: hidden; 20 | text-overflow: ellipsis; 21 | text-align: center; 22 | } 23 | 24 | .Fic\.GalleryFieldControl .file-name:hover { 25 | cursor: pointer; 26 | } 27 | 28 | .Fic\.GalleryFieldControl .preview-img { 29 | max-width: 100%; 30 | margin-top: 10px; 31 | } 32 | 33 | .Fic\.GalleryFieldControl .section-header { 34 | margin: 5px 0px 5px 0px; 35 | } 36 | 37 | .Fic\.GalleryFieldControl .files-group { 38 | border-bottom: 1px solid rgb(216, 216, 216); 39 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ivan Ficko 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 | -------------------------------------------------------------------------------- /GalleryFieldControl/ControlManifest.Input.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /obj/PCFGalleryControl.pcfproj.nuget.g.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | True 5 | NuGet 6 | C:\Users\ivanf\Source\Repos\PCFGalleryControl\obj\project.assets.json 7 | $(UserProfile)\.nuget\packages\ 8 | C:\Users\ivanf\.nuget\packages\ 9 | PackageReference 10 | 4.9.3 11 | 12 | 13 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /solution/solution.cdsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | solution 7 | c5a41bbf-7b82-419a-984e-abf5560aff77 8 | v4.6.2 9 | PackageReference 10 | 11 | 12 | 13 | 14 | Managed 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | Always 30 | 31 | 32 | 33 | 34 | 35 | 2b4792c9-00d9-487b-b13f-8a2b987ba212 36 | PCFGalleryControl 37 | Build 38 | false 39 | Content 40 | Always 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /obj/project.assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "targets": { 4 | ".NETFramework,Version=v4.0": { 5 | "Microsoft.PowerApps.MSBuild.Pcf/0.2.59": { 6 | "type": "package", 7 | "build": { 8 | "build/Microsoft.PowerApps.MSBuild.Pcf.props": {}, 9 | "build/Microsoft.PowerApps.MSBuild.Pcf.targets": {} 10 | } 11 | } 12 | }, 13 | ".NETFramework,Version=v4.0/win": { 14 | "Microsoft.PowerApps.MSBuild.Pcf/0.2.59": { 15 | "type": "package", 16 | "build": { 17 | "build/Microsoft.PowerApps.MSBuild.Pcf.props": {}, 18 | "build/Microsoft.PowerApps.MSBuild.Pcf.targets": {} 19 | } 20 | } 21 | }, 22 | ".NETFramework,Version=v4.0/win-x64": { 23 | "Microsoft.PowerApps.MSBuild.Pcf/0.2.59": { 24 | "type": "package", 25 | "build": { 26 | "build/Microsoft.PowerApps.MSBuild.Pcf.props": {}, 27 | "build/Microsoft.PowerApps.MSBuild.Pcf.targets": {} 28 | } 29 | } 30 | }, 31 | ".NETFramework,Version=v4.0/win-x86": { 32 | "Microsoft.PowerApps.MSBuild.Pcf/0.2.59": { 33 | "type": "package", 34 | "build": { 35 | "build/Microsoft.PowerApps.MSBuild.Pcf.props": {}, 36 | "build/Microsoft.PowerApps.MSBuild.Pcf.targets": {} 37 | } 38 | } 39 | } 40 | }, 41 | "libraries": { 42 | "Microsoft.PowerApps.MSBuild.Pcf/0.2.59": { 43 | "sha512": "lR9n5cKLOHIW40uHCQSyQmXIplLHNis5IqJ6OE+r1EL/ZJNl3eUtleNvJmbVVDcejIp19vGDvpuxfBpj+oPXjg==", 44 | "type": "package", 45 | "path": "microsoft.powerapps.msbuild.pcf/0.2.59", 46 | "files": [ 47 | ".nupkg.metadata", 48 | ".signature.p7s", 49 | "3rdPartyNotice.txt", 50 | "LICENSE.txt", 51 | "build/Microsoft.PowerApps.MSBuild.Pcf.props", 52 | "build/Microsoft.PowerApps.MSBuild.Pcf.targets", 53 | "microsoft.powerapps.msbuild.pcf.0.2.59.nupkg.sha512", 54 | "microsoft.powerapps.msbuild.pcf.nuspec" 55 | ] 56 | } 57 | }, 58 | "projectFileDependencyGroups": { 59 | ".NETFramework,Version=v4.0": [ 60 | "Microsoft.PowerApps.MSBuild.Pcf >= 0.*" 61 | ] 62 | }, 63 | "packageFolders": { 64 | "C:\\Users\\ivanf\\.nuget\\packages\\": {} 65 | }, 66 | "project": { 67 | "version": "1.0.0", 68 | "restore": { 69 | "projectUniqueName": "C:\\Users\\ivanf\\Source\\Repos\\PCFGalleryControl\\PCFGalleryControl.pcfproj", 70 | "projectName": "PCFGalleryControl", 71 | "projectPath": "C:\\Users\\ivanf\\Source\\Repos\\PCFGalleryControl\\PCFGalleryControl.pcfproj", 72 | "packagesPath": "C:\\Users\\ivanf\\.nuget\\packages\\", 73 | "outputPath": "C:\\Users\\ivanf\\Source\\Repos\\PCFGalleryControl\\obj\\", 74 | "projectStyle": "PackageReference", 75 | "skipContentFileWrite": true, 76 | "configFilePaths": [ 77 | "C:\\Users\\ivanf\\AppData\\Roaming\\NuGet\\NuGet.Config", 78 | "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config" 79 | ], 80 | "originalTargetFrameworks": [ 81 | "net40" 82 | ], 83 | "sources": { 84 | "https://www.nuget.org/api/v2/": {} 85 | }, 86 | "frameworks": { 87 | "net40": { 88 | "projectReferences": {} 89 | } 90 | } 91 | }, 92 | "frameworks": { 93 | "net40": { 94 | "dependencies": { 95 | "Microsoft.PowerApps.MSBuild.Pcf": { 96 | "target": "Package", 97 | "version": "[0.*, )" 98 | } 99 | } 100 | } 101 | }, 102 | "runtimes": { 103 | "win": { 104 | "#import": [] 105 | }, 106 | "win-x64": { 107 | "#import": [] 108 | }, 109 | "win-x86": { 110 | "#import": [] 111 | } 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /solution/other/Solution.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | solution 6 | 7 | 8 | 9 | 10 | 11 | 1.0 12 | 13 | 2 14 | 15 | 16 | IvanFicko 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | fic 29 | 30 | 58972 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 |
-------------------------------------------------------------------------------- /GalleryFieldControl/index.ts: -------------------------------------------------------------------------------- 1 | import { IInputs, IOutputs } from "./generated/ManifestTypes"; 2 | 3 | import * as FileSaver from 'file-saver'; 4 | 5 | class EntityReference { 6 | id: string; 7 | typeName: string; 8 | constructor(typeName: string, id: string) { 9 | this.id = id; 10 | this.typeName = typeName; 11 | } 12 | } 13 | 14 | class AttachedFile implements ComponentFramework.FileObject { 15 | fileContent: string; 16 | fileSize: number; 17 | fileName: string; 18 | mimeType: string; 19 | constructor(fileName: string, mimeType: string, fileContent: string, fileSize: number) { 20 | this.fileName = fileName; 21 | this.mimeType = mimeType; 22 | this.fileContent = fileContent; 23 | this.fileSize = fileSize; 24 | } 25 | } 26 | 27 | export class GalleryFieldControl implements ComponentFramework.StandardControl { 28 | 29 | private _context: ComponentFramework.Context; 30 | private _container: HTMLDivElement; 31 | 32 | private _thumbnailHeight: number | null; 33 | private _thumbnailWidth: number | null; 34 | 35 | private _minImageHeight: number | null; 36 | private _minImageWidth: number | null; 37 | 38 | private _notesContainer: HTMLDivElement; 39 | private _timelineEmailsContainer: HTMLDivElement; 40 | 41 | private _previewImage: HTMLImageElement; 42 | 43 | private _thumbnailClicked: EventListenerOrEventListenerObject; 44 | private _clearPreviewImage: EventListenerOrEventListenerObject; 45 | 46 | private _supportedMimeTypes: string[] = ["image/jpeg", "image/png", "image/svg+xml"]; 47 | private _supportedExtensions : string[] = [".jpg", ".jpeg", ".png", ".svg", ".gif"]; 48 | 49 | constructor() { 50 | 51 | } 52 | 53 | /** 54 | * Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here. 55 | * Data-set values are not initialized here, use updateView. 56 | * @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. 57 | * @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously. 58 | * @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. 59 | * @param container If a control is marked control-type='starndard', it will receive an empty div element within which it can render its content. 60 | */ 61 | public init(context: ComponentFramework.Context, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container: HTMLDivElement) { 62 | // Add control initialization code 63 | this._context = context; 64 | this._container = container; 65 | 66 | this._thumbnailHeight = context.parameters.thumbnailHeight == undefined ? null : context.parameters.thumbnailHeight.raw; 67 | this._thumbnailWidth = context.parameters.thumbnailWidth == undefined ? null : context.parameters.thumbnailWidth.raw; 68 | 69 | this._minImageHeight = context.parameters.minImageHeight == undefined ? null : context.parameters.minImageHeight.raw; 70 | this._minImageWidth = context.parameters.minImageWidth == undefined ? null : context.parameters.minImageWidth.raw; 71 | 72 | let reference: EntityReference = new EntityReference( 73 | (context).page.entityTypeName, 74 | (context).page.entityId 75 | ) 76 | 77 | this._thumbnailClicked = this.ThumbnailClicked.bind(this); 78 | this._clearPreviewImage = this.ClearPreviewImage.bind(this); 79 | 80 | let notesContainer = document.createElement("div"); 81 | notesContainer.classList.add("files-group"); 82 | this._notesContainer = notesContainer; 83 | 84 | let notesHeader = document.createElement("div"); 85 | notesHeader.textContent = reference.typeName == "email" ? "Attachments" : "Notes"; 86 | notesHeader.classList.add("section-header"); 87 | notesContainer.appendChild(notesHeader); 88 | 89 | let notes = document.createElement("div"); 90 | notesContainer.appendChild(notes); 91 | 92 | 93 | let timelineEmailsContainer = document.createElement("div"); 94 | timelineEmailsContainer.classList.add("files-group"); 95 | this._timelineEmailsContainer = timelineEmailsContainer; 96 | 97 | let timelineEmailsHeader = document.createElement("div"); 98 | timelineEmailsHeader.textContent = "Timeline Emails"; 99 | timelineEmailsHeader.classList.add("section-header"); 100 | timelineEmailsContainer.appendChild(timelineEmailsHeader); 101 | 102 | let timelineEmails = document.createElement("div"); 103 | timelineEmailsContainer.appendChild(timelineEmails); 104 | 105 | 106 | this._container.appendChild(notesContainer); 107 | this._container.appendChild(timelineEmailsContainer); 108 | 109 | let previewImg = document.createElement("img"); 110 | previewImg.classList.add("preview-img"); 111 | previewImg.addEventListener("click", this._clearPreviewImage); 112 | 113 | this._previewImage = previewImg; 114 | 115 | this._container.appendChild(previewImg); 116 | 117 | this.GetFiles(reference).then(result => this.RenderThumbnails(result, notes)); 118 | this.GetEmailAttachemntsFromTimeline(reference).then(result => this.RenderThumbnails(result, timelineEmails)); 119 | } 120 | 121 | 122 | /** 123 | * 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. 124 | * @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 125 | */ 126 | public updateView(context: ComponentFramework.Context): void { 127 | // Add code to update control view 128 | } 129 | 130 | /** 131 | * It is called by the framework prior to a control receiving new data. 132 | * @returns an object based on nomenclature defined in manifest, expecting object[s] for property marked as “bound” or “output” 133 | */ 134 | public getOutputs(): IOutputs { 135 | return {}; 136 | } 137 | 138 | /** 139 | * Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup. 140 | * i.e. cancelling any pending remote calls, removing listeners, etc. 141 | */ 142 | public destroy(): void { 143 | // Add code to cleanup control if necessary 144 | } 145 | 146 | 147 | private async GetFiles(ref: EntityReference): Promise { 148 | let attachmentType = ref.typeName == "email" ? "activitymimeattachment" : "annotation"; 149 | let fetchXml = 150 | "" + 151 | " " + 152 | " " + 153 | " " + 154 | " " + 155 | " " + 156 | ""; 157 | 158 | let query = '?fetchXml=' + encodeURIComponent(fetchXml); 159 | 160 | try { 161 | const result = await this._context.webAPI.retrieveMultipleRecords(attachmentType, query); 162 | if (result.entities.length == 0) { this._notesContainer.hidden = true; } 163 | let items = []; 164 | for (let i = 0; i < result.entities.length; i++) { 165 | let record = result.entities[i]; 166 | let fileName = record["filename"]; 167 | let mimeType = record["mimetype"]; 168 | let content = record["body"] || record["documentbody"]; 169 | let fileSize = record["filesize"]; 170 | 171 | const ext = fileName.substr(fileName.lastIndexOf('.')).toLowerCase(); 172 | 173 | if (!this._supportedMimeTypes.includes(mimeType) && !this._supportedExtensions.includes(ext)) { continue; } 174 | 175 | let file = new AttachedFile(fileName, mimeType, content, fileSize); 176 | items.push(file); 177 | } 178 | return items; 179 | } 180 | catch (error) { 181 | return []; 182 | } 183 | } 184 | 185 | private async GetEmailAttachemntsFromTimeline(ref: EntityReference) { 186 | 187 | let fetchXml = 188 | "" + 189 | " " + 190 | " " + 191 | " " + 192 | " " + 193 | " " + 194 | " " + 195 | " " + 196 | ""; 197 | 198 | 199 | let query = '?fetchXml=' + encodeURIComponent(fetchXml); 200 | 201 | try { 202 | const result = await this._context.webAPI.retrieveMultipleRecords("activitymimeattachment", query); 203 | if (result.entities.length == 0) { this._timelineEmailsContainer.hidden = true; } 204 | let items = []; 205 | for (let i = 0; i < result.entities.length; i++) { 206 | let record = result.entities[i]; 207 | 208 | let mimeType = record["mimetype"]; 209 | let fileName = record["filename"]; 210 | let content = record["body"] || record["documentbody"]; 211 | let fileSize = record["filesize"]; 212 | 213 | const ext = fileName.substr(fileName.lastIndexOf('.')).toLowerCase(); 214 | 215 | if (!this._supportedMimeTypes.includes(mimeType) && !this._supportedExtensions.includes(ext)) { continue; } 216 | 217 | let file = new AttachedFile(fileName, mimeType, content, fileSize); 218 | items.push(file); 219 | } 220 | return items; 221 | } 222 | catch (error) { 223 | return []; 224 | } 225 | } 226 | 227 | private async RenderThumbnails(files: AttachedFile[], container: HTMLDivElement) { 228 | for (let index = 0; index < files.length; index++) { 229 | const file = files[index]; 230 | 231 | let itemContainer = document.createElement("div"); 232 | itemContainer.classList.add("thumbnail-container"); 233 | 234 | let thumbnailDiv = document.createElement("div"); 235 | thumbnailDiv.classList.add("thumbnail"); 236 | 237 | // let imageDiv = document.createElement("img"); 238 | // imageDiv.src = 'data:' + file.mimeType + ';base64, ' + file.fileContent; 239 | let base64String = 'data:' + file.mimeType + ';base64, ' + file.fileContent; 240 | let imageDiv = await this.GetLoadedImageElement(base64String); 241 | 242 | if ((this._minImageHeight != null && imageDiv.height < this._minImageHeight) && (this._minImageWidth != null && imageDiv.width < this._minImageWidth)) { 243 | imageDiv.remove(); 244 | continue; 245 | } 246 | 247 | imageDiv.height = this._thumbnailHeight == null ? 150 : this._thumbnailHeight; 248 | imageDiv.width = this._thumbnailWidth == null ? 150 : this._thumbnailWidth; 249 | imageDiv.addEventListener("click", this._thumbnailClicked); 250 | thumbnailDiv.appendChild(imageDiv); 251 | 252 | let fileNameDiv = document.createElement("div"); 253 | fileNameDiv.classList.add("file-name"); 254 | fileNameDiv.style.width = (this._thumbnailWidth == null ? 162 : this._thumbnailWidth + 12).toString() + "px"; 255 | fileNameDiv.textContent = file.fileName; 256 | fileNameDiv.onclick = (e => { this.DownloadFile(file); }); 257 | 258 | itemContainer.appendChild(thumbnailDiv); 259 | itemContainer.appendChild(fileNameDiv); 260 | 261 | container.appendChild(itemContainer); 262 | } 263 | } 264 | 265 | private Base64ToFile(base64Data: string, tempfilename: string, contentType: string) { 266 | contentType = contentType || ''; 267 | const sliceSize = 1024; 268 | const byteCharacters = atob(base64Data); 269 | const bytesLength = byteCharacters.length; 270 | const slicesCount = Math.ceil(bytesLength / sliceSize); 271 | const byteArrays = new Array(slicesCount); 272 | 273 | for (let sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) { 274 | const begin = sliceIndex * sliceSize; 275 | const end = Math.min(begin + sliceSize, bytesLength); 276 | 277 | const bytes = new Array(end - begin); 278 | for (let offset = begin, i = 0; offset < end; ++i, ++offset) { 279 | bytes[i] = byteCharacters[offset].charCodeAt(0); 280 | } 281 | byteArrays[sliceIndex] = new Uint8Array(bytes); 282 | } 283 | return new File(byteArrays, tempfilename, { type: contentType }); 284 | } 285 | 286 | private ThumbnailClicked(evt: Event): void { 287 | let base64 = (evt.srcElement).src; 288 | this._previewImage.src = base64; 289 | } 290 | 291 | private ClearPreviewImage(evt: Event): void { 292 | this._previewImage.src = ""; 293 | } 294 | 295 | private DownloadFile(file: AttachedFile): void { 296 | const myFile = this.Base64ToFile(file.fileContent, file.fileName, file.mimeType); 297 | FileSaver.saveAs(myFile, file.fileName); 298 | } 299 | 300 | private async GetLoadedImageElement(base64: string):Promise{ 301 | return new Promise((resolve, reject) => { 302 | let imageDiv = document.createElement("img"); 303 | imageDiv.onload = () => resolve(imageDiv); 304 | imageDiv.src = base64; 305 | }); 306 | } 307 | } --------------------------------------------------------------------------------