From 7bf711daf2f7451a99de25c23fa3bf098d217f54 Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Sun, 30 Nov 2025 23:40:47 +0200 Subject: [PATCH 01/58] Refactor: Transition to TypeScript and Webpack build system --- .gitignore | 3 + .../Cropper.Blazor.MAUI.Net9.csproj | 2 +- .../Cropper.Blazor.Server.Net8.csproj | 2 +- .../Cropper.Blazor.Server.Net9.csproj | 2 +- src/Cropper.Blazor/Client/wwwroot/index.html | 1 - .../Cropper.Blazor/.config/dotnet-tools.json | 10 - .../Cropper.Blazor/Cropper.Blazor.csproj | 89 ++-- .../Cropper/cropperJsInterop.ts | 370 +++++++++++++++ .../Cropper.Blazor/excubowebcompiler.json | 53 --- .../Cropper.Blazor/package.json | 25 ++ .../Cropper.Blazor/tsconfig.json | 18 + .../Cropper.Blazor/webpack.config.js | 45 ++ .../Cropper.Blazor/wwwroot/cropper.min.css | 9 - .../Cropper.Blazor/wwwroot/cropper.min.js | 10 - .../wwwroot/cropperJsInterop.js | 425 ------------------ 15 files changed, 517 insertions(+), 547 deletions(-) delete mode 100644 src/Cropper.Blazor/Cropper.Blazor/.config/dotnet-tools.json create mode 100644 src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts delete mode 100644 src/Cropper.Blazor/Cropper.Blazor/excubowebcompiler.json create mode 100644 src/Cropper.Blazor/Cropper.Blazor/package.json create mode 100644 src/Cropper.Blazor/Cropper.Blazor/tsconfig.json create mode 100644 src/Cropper.Blazor/Cropper.Blazor/webpack.config.js delete mode 100644 src/Cropper.Blazor/Cropper.Blazor/wwwroot/cropper.min.css delete mode 100644 src/Cropper.Blazor/Cropper.Blazor/wwwroot/cropper.min.js delete mode 100644 src/Cropper.Blazor/Cropper.Blazor/wwwroot/cropperJsInterop.js diff --git a/.gitignore b/.gitignore index e7c92439..aaf655f4 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ _ReSharper*/ .idea/ #Nuget packages folder packages/ +# Node.js +node_modules/ +package-lock.json diff --git a/examples/Cropper.Blazor.MAUI.Net9/Cropper.Blazor.MAUI.Net9.csproj b/examples/Cropper.Blazor.MAUI.Net9/Cropper.Blazor.MAUI.Net9.csproj index 360d69d2..3d6afa26 100644 --- a/examples/Cropper.Blazor.MAUI.Net9/Cropper.Blazor.MAUI.Net9.csproj +++ b/examples/Cropper.Blazor.MAUI.Net9/Cropper.Blazor.MAUI.Net9.csproj @@ -61,7 +61,7 @@ - + diff --git a/examples/Cropper.Blazor.Server.Net8/Cropper.Blazor.Server.Net8.csproj b/examples/Cropper.Blazor.Server.Net8/Cropper.Blazor.Server.Net8.csproj index e4f253d6..62be134e 100644 --- a/examples/Cropper.Blazor.Server.Net8/Cropper.Blazor.Server.Net8.csproj +++ b/examples/Cropper.Blazor.Server.Net8/Cropper.Blazor.Server.Net8.csproj @@ -7,7 +7,7 @@ - + diff --git a/examples/Cropper.Blazor.Server.Net9/Cropper.Blazor.Server.Net9.csproj b/examples/Cropper.Blazor.Server.Net9/Cropper.Blazor.Server.Net9.csproj index 003affe0..c53b59a6 100644 --- a/examples/Cropper.Blazor.Server.Net9/Cropper.Blazor.Server.Net9.csproj +++ b/examples/Cropper.Blazor.Server.Net9/Cropper.Blazor.Server.Net9.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Cropper.Blazor/Client/wwwroot/index.html b/src/Cropper.Blazor/Client/wwwroot/index.html index 2cf83604..5ed8dfd4 100644 --- a/src/Cropper.Blazor/Client/wwwroot/index.html +++ b/src/Cropper.Blazor/Client/wwwroot/index.html @@ -67,7 +67,6 @@ }); - diff --git a/src/Cropper.Blazor/Cropper.Blazor/.config/dotnet-tools.json b/src/Cropper.Blazor/Cropper.Blazor/.config/dotnet-tools.json deleted file mode 100644 index 2c41bf42..00000000 --- a/src/Cropper.Blazor/Cropper.Blazor/.config/dotnet-tools.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "version": 1, - "isRoot": true, - "tools": { - "excubo.webcompiler": { - "version": "4.2.1", - "commands": ["webcompiler"] - } - } -} diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj index 925abadc..89d5bf6a 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj @@ -1,12 +1,4 @@  - - - - IncludeGeneratedStaticFiles; - $(ResolveStaticWebAssetsInputsDependsOn) - - - net10.0;net9.0;net8.0;net7.0;net6.0 disable @@ -14,7 +6,7 @@ - 1.5.0 + 1.5.1 LICENSE NuGet.png Cropper.Blazor @@ -74,12 +66,44 @@ True \ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -90,37 +114,11 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -141,4 +139,23 @@ + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts new file mode 100644 index 00000000..4d61dad3 --- /dev/null +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts @@ -0,0 +1,370 @@ +import * as Cropper from 'cropperjs/src'; + + +declare global { + interface DotNet { + createJSObjectReference(obj: any): any; + } + + const DotNet: DotNet; + + interface Window { + cropper: CropperDecorator; + cropperUrlImageHelper: typeof CropperUrlImageHelper; + } +} + +type CropperId = string; + +interface CropperExtendedOptions + extends Cropper.default.Options { + correlationId?: string; +} + +export class CropperDecorator { + private cropperInstances: Record = {}; + + clear(id: CropperId) { + return this.cropperInstances[id].clear(); + } + + crop(id: CropperId) { + return this.cropperInstances[id].crop(); + } + + destroy(id: CropperId) { + const instance = this.cropperInstances[id]; + if (instance) { + instance.destroy(); + delete this.cropperInstances[id]; + } + } + + disable(id: CropperId) { + return this.cropperInstances[id].disable(); + } + + enable(id: CropperId) { + return this.cropperInstances[id].enable(); + } + + getCanvasData(id: CropperId): Cropper.default.CanvasData { + return this.cropperInstances[id].getCanvasData(); + } + + getContainerData(id: CropperId): Cropper.default.ContainerData { + return this.cropperInstances[id].getContainerData(); + } + + getCropBoxData(id: CropperId) { + return this.cropperInstances[id].getCropBoxData(); + } + + getCroppedCanvas(id: CropperId, options: any) { + options.maxWidth ??= Infinity; + options.maxHeight ??= Infinity; + return this.cropperInstances[id].getCroppedCanvas(options); + } + + async getCroppedCanvasInBackground( + id: CropperId, + options: any, + dotNetCanvasReceiverRef: any + ) { + setTimeout(async () => { + const canvas = this.getCroppedCanvas(id, options); + const jsRef = DotNet.createJSObjectReference(canvas); + await dotNetCanvasReceiverRef.invokeMethodAsync( + "ReceiveCanvasReference", + jsRef + ); + }, 0); + } + + getCroppedCanvasDataURL( + id: CropperId, + options: any, + type?: string, + encoderOptions?: number + ) { + options.maxWidth ??= Infinity; + options.maxHeight ??= Infinity; + + return this.cropperInstances[id] + .getCroppedCanvas(options) + .toDataURL(type, encoderOptions); + } + + getData(id: CropperId, rounded?: boolean): Cropper.default.Data { + return this.cropperInstances[id].getData(rounded); + } + + getImageData(id: CropperId): Cropper.default.ImageData { + return this.cropperInstances[id].getImageData(); + } + + move(id: CropperId, offsetX: number, offsetY: number) { + return this.cropperInstances[id].move(offsetX, offsetY); + } + + moveTo(id: CropperId, x: number, y: number) { + return this.cropperInstances[id].moveTo(x, y); + } + + replace(id: CropperId, url: string, onlyColorChanged?: boolean) { + return this.cropperInstances[id].replace(url, onlyColorChanged); + } + + reset(id: CropperId) { + return this.cropperInstances[id].reset(); + } + + rotate(id: CropperId, degree: number) { + return this.cropperInstances[id].rotate(degree); + } + + rotateTo(id: CropperId, degree: number) { + return this.cropperInstances[id].rotateTo(degree); + } + + scale(id: CropperId, x: number, y: number) { + return this.cropperInstances[id].scale(x, y); + } + + scaleX(id: CropperId, x: number) { + return this.cropperInstances[id].scaleX(x); + } + + scaleY(id: CropperId, y: number) { + return this.cropperInstances[id].scaleY(y); + } + + setAspectRatio(id: CropperId, ratio: number) { + return this.cropperInstances[id].setAspectRatio(ratio); + } + + setCanvasData(id: CropperId, data: Cropper.default.CanvasData) { + return this.cropperInstances[id].setCanvasData(data); + } + + setCropBoxData(id: CropperId, data: any) { + return this.cropperInstances[id].setCropBoxData(data); + } + + setData(id: CropperId, data: Cropper.default.Data) { + return this.cropperInstances[id].setData(data); + } + + setDragMode(id: CropperId, mode: Cropper.default.DragMode) { + return this.cropperInstances[id].setDragMode(mode); + } + + zoom(id: CropperId, ratio: number) { + return this.cropperInstances[id].zoom(ratio); + } + + zoomTo(id: CropperId, ratio: number, pivotX: number, pivotY: number) { + return this.cropperInstances[id].zoomTo(ratio, { x: pivotX, y: pivotY }); + } + + noConflict() { + return Cropper.default.noConflict(); + } + + setDefaults(options: Cropper.default.Options) { + Cropper.default.setDefaults(options); + } + + // -------------------------- + // Event serialization helpers + // -------------------------- + + getJSEventData(instance: any, correlationId: any) { + return { + isTrusted: instance.isTrusted, + detail: this.getJSEventDataDetail(instance), + type: instance.type, + eventPhase: instance.eventPhase, + bubbles: instance.bubbles, + cancelable: instance.cancelable, + defaultPrevented: instance.defaultPrevented, + composed: instance.composed, + timeStamp: instance.timeStamp, + returnValue: instance.returnValue, + cancelBubble: instance.cancelBubble, + correlationId + }; + } + + getJSEventDataDetail(instance: any): any { + if (instance.type === "zoom") { + return { + oldRatio: instance.detail.oldRatio, + ratio: instance.detail.ratio, + originalEvent: instance.detail.originalEvent + ? DotNet.createJSObjectReference(instance.detail.originalEvent) + : null + }; + } + + if (["cropstart", "cropend", "cropmove"].includes(instance.type)) { + return { + action: instance.detail.action, + originalEvent: instance.detail.originalEvent + ? DotNet.createJSObjectReference(instance.detail.originalEvent) + : null + }; + } + + return instance.detail; + } + + onReady(imageObject: any, e: any, id: any) { + imageObject.invokeMethodAsync("IsReady", this.getJSEventData(e, id)); + } + + onCropStart(imageObject: any, e: any, id: any) { + imageObject.invokeMethodAsync("CropperIsStarted", this.getJSEventData(e, id)); + } + + onCropMove(imageObject: any, e: any, id: any) { + imageObject.invokeMethodAsync("CropperIsMoved", this.getJSEventData(e, id)); + } + + onCropEnd(imageObject: any, e: any, id: any) { + imageObject.invokeMethodAsync("CropperIsEnded", this.getJSEventData(e, id)); + } + + onCrop(imageObject: any, e: any, id: any) { + imageObject.invokeMethodAsync("CropperIsCroped", this.getJSEventData(e, id)); + } + + onZoom(imageObject: any, e: any, id: any) { + imageObject.invokeMethodAsync("CropperIsZoomed", this.getJSEventData(e, id)); + } + + initCropper( + id: CropperId, + image: HTMLImageElement, + optionsImage: CropperExtendedOptions, + imageObject?: any + ) { + if (!image) throw new Error("Parameter 'image' must not be null"); + if (!optionsImage) throw new Error("Parameter 'optionsImage' must not be null"); + + const options: Cropper.default.Options = {}; + const correlationId = optionsImage.correlationId; + + if (imageObject) { + options.ready = (e: any) => this.onReady(imageObject, e, correlationId); + options.cropstart = (e: any) => this.onCropStart(imageObject, e, correlationId); + options.cropmove = (e: any) => this.onCropMove(imageObject, e, correlationId); + options.cropend = (e: any) => this.onCropEnd(imageObject, e, correlationId); + options.crop = (e: any) => this.onCrop(imageObject, e, correlationId); + options.zoom = (e: any) => this.onZoom(imageObject, e, correlationId); + } + + Object.assign(options, optionsImage); + + const cropper = new Cropper.default(image, options); + this.cropperInstances[id] = cropper; + } + + // -------------------------- + // Chunked Blob Streaming + // -------------------------- + + async readBlobInChunks( + blob: Blob, + receiverRef: any, + maximumReceiveChunkSize?: number + ) { + if (!(blob instanceof Blob)) { + throw new TypeError("blob must be a valid Blob"); + } + + const reader = maximumReceiveChunkSize + ? this.buildChunkedReader(blob, maximumReceiveChunkSize) + : blob.stream().getReader(); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + await receiverRef.invokeMethodAsync("ReceiveImageChunk", value); + } + + await receiverRef.invokeMethodAsync("CompleteImageTransfer"); + } catch (err) { + await receiverRef.invokeMethodAsync( + "HandleImageProcessingError", + String(err) + ); + } + } + + private buildChunkedReader(blob: Blob, maxBytes: number): ReadableStreamDefaultReader { + const blobReader = blob.stream().getReader(); + + const stream = new ReadableStream({ + async pull(controller) { + const { done, value } = await blobReader.read(); + if (done || !value) { + controller.close(); + return; + } + + let offset = 0; + + while (offset < value.length) { + const slice = value.slice(offset, offset + maxBytes); + offset += slice.length; + controller.enqueue(slice); + } + } + }); + + return stream.getReader(); + } + + sendImageInChunks( + id: CropperId, + options: any, + receiverRef: any, + type?: string, + encoderOptions?: number, + maximumReceiveChunkSize?: number + ) { + const instance = this.cropperInstances[id]; + options.maxWidth ??= Infinity; + options.maxHeight ??= Infinity; + + setTimeout(() => { + instance.getCroppedCanvas(options).toBlob(async (blob) => { + if (blob) { + await this.readBlobInChunks(blob, receiverRef, maximumReceiveChunkSize); + } + }, type, encoderOptions); + }, 0); + } +} + +// --------------------------------------------------- +// URL Image Helper +// --------------------------------------------------- + +export class CropperUrlImageHelper { + static async getImageUsingStreaming(imageStream: any): Promise { + const buf = await imageStream.arrayBuffer(); + const blob = new Blob([buf]); + return URL.createObjectURL(blob); + } + + static revokeObjectUrl(url: string) { + URL.revokeObjectURL(url); + } +} + +window.cropper = new CropperDecorator(); +window.cropperUrlImageHelper = CropperUrlImageHelper; diff --git a/src/Cropper.Blazor/Cropper.Blazor/excubowebcompiler.json b/src/Cropper.Blazor/Cropper.Blazor/excubowebcompiler.json deleted file mode 100644 index 9e61bf06..00000000 --- a/src/Cropper.Blazor/Cropper.Blazor/excubowebcompiler.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "Minifiers": { - "GZip": false, - "Enabled": true, - "Css": { - "CommentMode": "Important", - "ColorNames": "Hex", - "TermSemicolons": true, - "OutputMode": "SingleLine", - "IndentSize": 2 - }, - "Javascript": { - "RenameLocals": false, - "PreserveImportantComments": true, - "EvalTreatment": "Ignore", - "TermSemicolons": true, - "OutputMode": "SingleLine", - "IndentSize": 2 - } - }, - "Autoprefix": { - "Enabled": false, - "ProcessingOptions": { - "Browsers": ["last 4 versions"], - "Cascade": true, - "Add": true, - "Remove": true, - "Supports": true, - "Flexbox": "All", - "Grid": "None", - "IgnoreUnknownVersions": false, - "Stats": "", - "SourceMap": true, - "InlineSourceMap": false, - "SourceMapIncludeContents": false, - "OmitSourceMapUrl": false - } - }, - "CompilerSettings": { - "Sass": { - "IndentType": "Space", - "IndentWidth": 2, - "OutputStyle": "Expanded", - "RelativeUrls": true, - "LineFeed": "Lf", - "SourceMap": false - } - }, - "Output": { - "Preserve": true, - "Directory": "./wwwroot" - } -} diff --git a/src/Cropper.Blazor/Cropper.Blazor/package.json b/src/Cropper.Blazor/Cropper.Blazor/package.json new file mode 100644 index 00000000..1a59e250 --- /dev/null +++ b/src/Cropper.Blazor/Cropper.Blazor/package.json @@ -0,0 +1,25 @@ +{ + "name": "Cropper.Blazor", + "version": "1.5.1", + "description": "", + "main": "index.js", + "scripts": { + "build:debug": "webpack --mode=development", + "build:production": "webpack --mode=production", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "MIT", + "devDependencies": { + "ts-loader": "9.5.4", + "clean-css": "5.3.3", + "copy-webpack-plugin": "13.0.1", + "terser-webpack-plugin": "5.3.14", + "typescript": "5.9.3", + "webpack": "5.103.0", + "webpack-cli": "6.0.1" + }, + "dependencies": { + "cropperjs": "1.6.2" + } +} \ No newline at end of file diff --git a/src/Cropper.Blazor/Cropper.Blazor/tsconfig.json b/src/Cropper.Blazor/Cropper.Blazor/tsconfig.json new file mode 100644 index 00000000..e7f1aa4e --- /dev/null +++ b/src/Cropper.Blazor/Cropper.Blazor/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "noImplicitAny": false, + "noEmitOnError": true, + "removeComments": true, + "sourceMap": true, + "target": "es6", + "module": "commonjs", + "lib": [ "es2016", "dom" ], + "strict": true, + "alwaysStrict": true, + "preserveConstEnums": true, + "allowJs": false + }, + "exclude": [ + "node_modules" + ] +} diff --git a/src/Cropper.Blazor/Cropper.Blazor/webpack.config.js b/src/Cropper.Blazor/Cropper.Blazor/webpack.config.js new file mode 100644 index 00000000..76f443b6 --- /dev/null +++ b/src/Cropper.Blazor/Cropper.Blazor/webpack.config.js @@ -0,0 +1,45 @@ +const path = require('path'); +const webpack = require('webpack'); +const CleanCSS = require('clean-css') +const CopyPlugin = require('copy-webpack-plugin') +const TerserPlugin = require('terser-webpack-plugin') + +module.exports = (env, args) => ({ + resolve: { + extensions: ['.ts', '.js', '.css'] + }, + devtool: args.mode === 'development' ? 'inline-source-map' : 'hidden-source-map', + module: { + rules: [{ + test: /\.ts?$/, + loader: 'ts-loader' + }] + }, + entry: { + "cropperJsInterop": './Cropper/cropperJsInterop.ts' + }, + output: { + path: path.join(__dirname, '/wwwroot'), + filename: '[name].min.js' + }, + plugins: [new CopyPlugin({ + patterns: [{ + to: 'cropper.min.css', + from: path.resolve(__dirname, 'node_modules/cropperjs/src/css', 'cropper.css'), + transform: content => (new CleanCSS({ + level: 2 + }).minify(content)).styles + }] + })], + optimization: { + minimize: true, + minimizer: [new TerserPlugin({ + terserOptions: { + format: { + comments: false, + }, + }, + extractComments: false, + })], + }, +}); \ No newline at end of file diff --git a/src/Cropper.Blazor/Cropper.Blazor/wwwroot/cropper.min.css b/src/Cropper.Blazor/Cropper.Blazor/wwwroot/cropper.min.css deleted file mode 100644 index 049cab58..00000000 --- a/src/Cropper.Blazor/Cropper.Blazor/wwwroot/cropper.min.css +++ /dev/null @@ -1,9 +0,0 @@ -/*! - * Cropper.js v1.6.1 - * https://fengyuanchen.github.io/cropperjs - * - * Copyright 2015-present Chen Fengyuan - * Released under the MIT license - * - * Date: 2023-09-17T03:44:17.565Z - */.cropper-container{direction:ltr;font-size:0;line-height:0;position:relative;-ms-touch-action:none;touch-action:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.cropper-container img{backface-visibility:hidden;display:block;height:100%;image-orientation:0deg;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%}.cropper-canvas,.cropper-crop-box,.cropper-drag-box,.cropper-modal,.cropper-wrap-box{bottom:0;left:0;position:absolute;right:0;top:0}.cropper-canvas,.cropper-wrap-box{overflow:hidden}.cropper-drag-box{background-color:#fff;opacity:0}.cropper-modal{background-color:#000;opacity:.5}.cropper-view-box{display:block;height:100%;outline:1px solid #39f;outline-color:rgba(51,153,255,.75);overflow:hidden;width:100%}.cropper-dashed{border:0 dashed #eee;display:block;opacity:.5;position:absolute}.cropper-dashed.dashed-h{border-bottom-width:1px;border-top-width:1px;height:33.33333%;left:0;top:33.33333%;width:100%}.cropper-dashed.dashed-v{border-left-width:1px;border-right-width:1px;height:100%;left:33.33333%;top:0;width:33.33333%}.cropper-center{display:block;height:0;left:50%;opacity:.75;position:absolute;top:50%;width:0}.cropper-center:after,.cropper-center:before{background-color:#eee;content:" ";display:block;position:absolute}.cropper-center:before{height:1px;left:-3px;top:0;width:7px}.cropper-center:after{height:7px;left:0;top:-3px;width:1px}.cropper-face,.cropper-line,.cropper-point{display:block;height:100%;opacity:.1;position:absolute;width:100%}.cropper-face{background-color:#fff;left:0;top:0}.cropper-line{background-color:#39f}.cropper-line.line-e{cursor:ew-resize;right:-3px;top:0;width:5px}.cropper-line.line-n{cursor:ns-resize;height:5px;left:0;top:-3px}.cropper-line.line-w{cursor:ew-resize;left:-3px;top:0;width:5px}.cropper-line.line-s{bottom:-3px;cursor:ns-resize;height:5px;left:0}.cropper-point{background-color:#39f;height:5px;opacity:.75;width:5px}.cropper-point.point-e{cursor:ew-resize;margin-top:-3px;right:-3px;top:50%}.cropper-point.point-n{cursor:ns-resize;left:50%;margin-left:-3px;top:-3px}.cropper-point.point-w{cursor:ew-resize;left:-3px;margin-top:-3px;top:50%}.cropper-point.point-s{bottom:-3px;cursor:s-resize;left:50%;margin-left:-3px}.cropper-point.point-ne{cursor:nesw-resize;right:-3px;top:-3px}.cropper-point.point-nw{cursor:nwse-resize;left:-3px;top:-3px}.cropper-point.point-sw{bottom:-3px;cursor:nesw-resize;left:-3px}.cropper-point.point-se{bottom:-3px;cursor:nwse-resize;height:20px;opacity:1;right:-3px;width:20px}@media (min-width:768px){.cropper-point.point-se{height:15px;width:15px}}@media (min-width:992px){.cropper-point.point-se{height:10px;width:10px}}@media (min-width:1200px){.cropper-point.point-se{height:5px;opacity:.75;width:5px}}.cropper-point.point-se:before{background-color:#39f;bottom:-50%;content:" ";display:block;height:200%;opacity:0;position:absolute;right:-50%;width:200%}.cropper-invisible{opacity:0}.cropper-bg{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC")}.cropper-hide{display:block;height:0;position:absolute;width:0}.cropper-hidden{display:none!important}.cropper-move{cursor:move}.cropper-crop{cursor:crosshair}.cropper-disabled .cropper-drag-box,.cropper-disabled .cropper-face,.cropper-disabled .cropper-line,.cropper-disabled .cropper-point{cursor:not-allowed} \ No newline at end of file diff --git a/src/Cropper.Blazor/Cropper.Blazor/wwwroot/cropper.min.js b/src/Cropper.Blazor/Cropper.Blazor/wwwroot/cropper.min.js deleted file mode 100644 index 3102cb54..00000000 --- a/src/Cropper.Blazor/Cropper.Blazor/wwwroot/cropper.min.js +++ /dev/null @@ -1,10 +0,0 @@ -/*! - * Cropper.js v1.6.2 - * https://fengyuanchen.github.io/cropperjs - * - * Copyright 2015-present Chen Fengyuan - * Released under the MIT license - * - * Date: 2024-04-21T07:43:05.335Z - */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Cropper=e()}(this,function(){"use strict";function C(e,t){var i,a=Object.keys(e);return Object.getOwnPropertySymbols&&(i=Object.getOwnPropertySymbols(e),t&&(i=i.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),a.push.apply(a,i)),a}function S(a){for(var t=1;tt.length)&&(e=t.length);for(var i=0,a=new Array(e);it.width?3===i?o=t.height*e:h=t.width/e:3===i?h=t.width/e:o=t.height*e,{aspectRatio:e,naturalWidth:n,naturalHeight:a,width:o,height:h});this.canvasData=e,this.limited=1===i||2===i,this.limitCanvas(!0,!0),e.width=Math.min(Math.max(e.width,e.minWidth),e.maxWidth),e.height=Math.min(Math.max(e.height,e.minHeight),e.maxHeight),e.left=(t.width-e.width)/2,e.top=(t.height-e.height)/2,e.oldLeft=e.left,e.oldTop=e.top,this.initialCanvasData=g({},e)},limitCanvas:function(t,e){var i=this.options,a=this.containerData,n=this.canvasData,o=this.cropBoxData,h=i.viewMode,r=n.aspectRatio,s=this.cropped&&o;t&&(t=Number(i.minCanvasWidth)||0,i=Number(i.minCanvasHeight)||0,1=a.width&&(n.minLeft=Math.min(0,r),n.maxLeft=Math.max(0,r)),n.height>=a.height)&&(n.minTop=Math.min(0,t),n.maxTop=Math.max(0,t))):(n.minLeft=-n.width,n.minTop=-n.height,n.maxLeft=a.width,n.maxTop=a.height))},renderCanvas:function(t,e){var i,a,n,o,h=this.canvasData,r=this.imageData;e&&(e={width:r.naturalWidth*Math.abs(r.scaleX||1),height:r.naturalHeight*Math.abs(r.scaleY||1),degree:r.rotate||0},r=e.width,o=e.height,e=e.degree,i=90==(e=Math.abs(e)%180)?{width:o,height:r}:(a=e%90*Math.PI/180,i=Math.sin(a),n=r*(a=Math.cos(a))+o*i,r=r*i+o*a,90h.maxWidth||h.widthh.maxHeight||h.heighte.width?a.height=a.width/i:a.width=a.height*i),this.cropBoxData=a,this.limitCropBox(!0,!0),a.width=Math.min(Math.max(a.width,a.minWidth),a.maxWidth),a.height=Math.min(Math.max(a.height,a.minHeight),a.maxHeight),a.width=Math.max(a.minWidth,a.width*t),a.height=Math.max(a.minHeight,a.height*t),a.left=e.left+(e.width-a.width)/2,a.top=e.top+(e.height-a.height)/2,a.oldLeft=a.left,a.oldTop=a.top,this.initialCropBoxData=g({},a)},limitCropBox:function(t,e){var i,a,n=this.options,o=this.containerData,h=this.canvasData,r=this.cropBoxData,s=this.limited,c=n.aspectRatio;t&&(t=Number(n.minCropBoxWidth)||0,n=Number(n.minCropBoxHeight)||0,i=s?Math.min(o.width,h.width,h.width+h.left,o.width-h.left):o.width,a=s?Math.min(o.height,h.height,h.height+h.top,o.height-h.top):o.height,t=Math.min(t,o.width),n=Math.min(n,o.height),c&&(t&&n?ti.maxWidth||i.widthi.maxHeight||i.height=e.width&&i.height>=e.height?q:I),f(this.cropBox,g({width:i.width,height:i.height},x({translateX:i.left,translateY:i.top}))),this.cropped&&this.limited&&this.limitCanvas(!0,!0),this.disabled||this.output()},output:function(){this.preview(),y(this.element,tt,this.getData())}},i={initPreview:function(){var t=this.element,i=this.crossOrigin,e=this.options.preview,a=i?this.crossOriginUrl:this.url,n=t.alt||"The image to preview",o=document.createElement("img");i&&(o.crossOrigin=i),o.src=a,o.alt=n,this.viewBox.appendChild(o),this.viewBoxImage=o,e&&("string"==typeof(o=e)?o=t.ownerDocument.querySelectorAll(e):e.querySelector&&(o=[e]),z(this.previews=o,function(t){var e=document.createElement("img");w(t,m,{width:t.offsetWidth,height:t.offsetHeight,html:t.innerHTML}),i&&(e.crossOrigin=i),e.src=a,e.alt=n,e.style.cssText='display:block;width:100%;height:auto;min-width:0!important;min-height:0!important;max-width:none!important;max-height:none!important;image-orientation:0deg!important;"',t.innerHTML="",t.appendChild(e)}))},resetPreview:function(){z(this.previews,function(e){var i=Bt(e,m),i=(f(e,{width:i.width,height:i.height}),e.innerHTML=i.html,e),e=m;if(o(i[e]))try{delete i[e]}catch(t){i[e]=void 0}else if(i.dataset)try{delete i.dataset[e]}catch(t){i.dataset[e]=void 0}else i.removeAttribute("data-".concat(Dt(e)))})},preview:function(){var h=this.imageData,t=this.canvasData,e=this.cropBoxData,r=e.width,s=e.height,c=h.width,d=h.height,l=e.left-t.left-h.left,p=e.top-t.top-h.top;this.cropped&&!this.disabled&&(f(this.viewBoxImage,g({width:c,height:d},x(g({translateX:-l,translateY:-p},h)))),z(this.previews,function(t){var e=Bt(t,m),i=e.width,e=e.height,a=i,n=e,o=1;r&&(n=s*(o=i/r)),s&&eMath.abs(a-1)?i:a)&&(t.restore&&(o=this.getCanvasData(),h=this.getCropBoxData()),this.render(),t.restore)&&(this.setCanvasData(z(o,function(t,e){o[e]=t*n})),this.setCropBoxData(z(h,function(t,e){h[e]=t*n}))))},dblclick:function(){var t,e;this.disabled||this.options.dragMode===_||this.setDragMode((t=this.dragBox,e=Q,(t.classList?t.classList.contains(e):-1y&&(D.x=y-f);break;case k:p+D.xx&&(D.y=x-v)}}var i,a,o,n=this.options,h=this.canvasData,r=this.containerData,s=this.cropBoxData,c=this.pointers,d=this.action,l=n.aspectRatio,p=s.left,m=s.top,u=s.width,g=s.height,f=p+u,v=m+g,w=0,b=0,y=r.width,x=r.height,M=!0,C=(!l&&t.shiftKey&&(l=u&&g?u/g:1),this.limited&&(w=s.minLeft,b=s.minTop,y=w+Math.min(r.width,h.width,h.left+h.width),x=b+Math.min(r.height,h.height,h.top+h.height)),c[Object.keys(c)[0]]),D={x:C.endX-C.startX,y:C.endY-C.startY};switch(d){case I:p+=D.x,m+=D.y;break;case B:0<=D.x&&(y<=f||l&&(m<=b||x<=v))?M=!1:(e(B),(u+=D.x)<0&&(d=k,p-=u=-u),l&&(m+=(s.height-(g=u/l))/2));break;case T:D.y<=0&&(m<=b||l&&(p<=w||y<=f))?M=!1:(e(T),g-=D.y,m+=D.y,g<0&&(d=O,m-=g=-g),l&&(p+=(s.width-(u=g*l))/2));break;case k:D.x<=0&&(p<=w||l&&(m<=b||x<=v))?M=!1:(e(k),u-=D.x,p+=D.x,u<0&&(d=B,p-=u=-u),l&&(m+=(s.height-(g=u/l))/2));break;case O:0<=D.y&&(x<=v||l&&(p<=w||y<=f))?M=!1:(e(O),(g+=D.y)<0&&(d=T,m-=g=-g),l&&(p+=(s.width-(u=g*l))/2));break;case E:if(l){if(D.y<=0&&(m<=b||y<=f)){M=!1;break}e(T),g-=D.y,m+=D.y,u=g*l}else e(T),e(B),!(0<=D.x)||fMath.abs(o)&&(o=i)})}),o),t),M=!1;break;case U:D.x&&D.y?(i=Wt(this.cropper),p=C.startX-i.left,m=C.startY-i.top,u=s.minWidth,g=s.minHeight,0 or element.");this.element=t,this.options=g({},ut,u(e)&&e),this.cropped=!1,this.disabled=!1,this.pointers={},this.ready=!1,this.reloading=!1,this.replaced=!1,this.sized=!1,this.sizing=!1,this.init()}return t=n,i=[{key:"noConflict",value:function(){return window.Cropper=Pt,n}},{key:"setDefaults",value:function(t){g(ut,u(t)&&t)}}],(e=[{key:"init",value:function(){var t,e=this.element,i=e.tagName.toLowerCase();if(!e[c]){if(e[c]=this,"img"===i){if(this.isImg=!0,t=e.getAttribute("src")||"",!(this.originalUrl=t))return;t=e.src}else"canvas"===i&&window.HTMLCanvasElement&&(t=e.toDataURL());this.load(t)}}},{key:"load",value:function(t){var e,i,a,n,o,h,r=this;t&&(this.url=t,this.imageData={},e=this.element,(i=this.options).rotatable||i.scalable||(i.checkOrientation=!1),i.checkOrientation&&window.ArrayBuffer?lt.test(t)?pt.test(t)?this.read((h=(h=t).replace(Xt,""),a=atob(h),h=new ArrayBuffer(a.length),z(n=new Uint8Array(h),function(t,e){n[e]=a.charCodeAt(e)}),h)):this.clone():(o=new XMLHttpRequest,h=this.clone.bind(this),this.reloading=!0,(this.xhr=o).onabort=h,o.onerror=h,o.ontimeout=h,o.onprogress=function(){o.getResponseHeader("content-type")!==ct&&o.abort()},o.onload=function(){r.read(o.response)},o.onloadend=function(){r.reloading=!1,r.xhr=null},i.checkCrossOrigin&&Lt(t)&&e.crossOrigin&&(t=zt(t)),o.open("GET",t,!0),o.responseType="arraybuffer",o.withCredentials="use-credentials"===e.crossOrigin,o.send()):this.clone())}},{key:"read",value:function(t){var e=this.options,i=this.imageData,a=Rt(t),n=0,o=1,h=1;1
',o=(n=n.querySelector(".".concat(c,"-container"))).querySelector(".".concat(c,"-canvas")),h=n.querySelector(".".concat(c,"-drag-box")),s=(r=n.querySelector(".".concat(c,"-crop-box"))).querySelector(".".concat(c,"-face")),this.container=a,this.cropper=n,this.canvas=o,this.dragBox=h,this.cropBox=r,this.viewBox=n.querySelector(".".concat(c,"-view-box")),this.face=s,o.appendChild(i),v(t,L),a.insertBefore(n,t.nextSibling),X(i,Z),this.initPreview(),this.bind(),e.initialAspectRatio=Math.max(0,e.initialAspectRatio)||NaN,e.aspectRatio=Math.max(0,e.aspectRatio)||NaN,e.viewMode=Math.max(0,Math.min(3,Math.round(e.viewMode)))||0,v(r,L),e.guides||v(r.getElementsByClassName("".concat(c,"-dashed")),L),e.center||v(r.getElementsByClassName("".concat(c,"-center")),L),e.background&&v(n,"".concat(c,"-bg")),e.highlight||v(s,G),e.cropBoxMovable&&(v(s,V),w(s,d,I)),e.cropBoxResizable||(v(r.getElementsByClassName("".concat(c,"-line")),L),v(r.getElementsByClassName("".concat(c,"-point")),L)),this.render(),this.ready=!0,this.setDragMode(e.dragMode),e.autoCrop&&this.crop(),this.setData(e.data),l(e.ready)&&b(t,"ready",e.ready,{once:!0}),y(t,"ready"))}},{key:"unbuild",value:function(){var t;this.ready&&(this.ready=!1,this.unbind(),this.resetPreview(),(t=this.cropper.parentNode)&&t.removeChild(this.cropper),X(this.element,L))}},{key:"uncreate",value:function(){this.ready?(this.unbuild(),this.ready=!1,this.cropped=!1):this.sizing?(this.sizingImage.onload=null,this.sizing=!1,this.sized=!1):this.reloading?(this.xhr.onabort=null,this.xhr.abort()):this.image&&this.stop()}}])&&A(t.prototype,e),i&&A(t,i),Object.defineProperty(t,"prototype",{writable:!1}),t;var t,e,i}();return g(It.prototype,t,i,e,St,jt,At),It}); \ No newline at end of file diff --git a/src/Cropper.Blazor/Cropper.Blazor/wwwroot/cropperJsInterop.js b/src/Cropper.Blazor/Cropper.Blazor/wwwroot/cropperJsInterop.js deleted file mode 100644 index dacc6cab..00000000 --- a/src/Cropper.Blazor/Cropper.Blazor/wwwroot/cropperJsInterop.js +++ /dev/null @@ -1,425 +0,0 @@ -class CropperDecorator { - constructor () { - this.cropperInstances = {} - } - - clear (cropperComponentId) { - return this.cropperInstances[cropperComponentId] - .clear() - } - - crop (cropperComponentId) { - return this.cropperInstances[cropperComponentId] - .crop() - } - - destroy (cropperComponentId) { - const cropperInstance = this.cropperInstances[cropperComponentId] - - if (cropperInstance) { - cropperInstance - .destroy() - - delete this.cropperInstances[cropperComponentId] - } - } - - disable (cropperComponentId) { - return this.cropperInstances[cropperComponentId] - .disable() - } - - enable (cropperComponentId) { - return this.cropperInstances[cropperComponentId] - .enable() - } - - getCanvasData (cropperComponentId) { - return this.cropperInstances[cropperComponentId] - .getCanvasData() - } - - getContainerData (cropperComponentId) { - return this.cropperInstances[cropperComponentId] - .getContainerData() - } - - getCropBoxData (cropperComponentId) { - return this.cropperInstances[cropperComponentId] - .getCropBoxData() - } - - getCroppedCanvas (cropperComponentId, options) { - options.maxWidth ??= Infinity - options.maxHeight ??= Infinity - - return this.cropperInstances[cropperComponentId] - .getCroppedCanvas(options) - } - - getCroppedCanvasInBackground (cropperComponentId, options, dotNetCanvasReceiverRef) { - setTimeout(async () => { - const croppedCanvas = this.getCroppedCanvas(cropperComponentId, options) - const jsCroppedCanvasRef = DotNet.createJSObjectReference(croppedCanvas) // eslint-disable-line no-undef - - await dotNetCanvasReceiverRef.invokeMethodAsync('ReceiveCanvasReference', jsCroppedCanvasRef) - }, 0) - } - - getCroppedCanvasDataURL (cropperComponentId, options, type, encoderOptions) { - options.maxWidth ??= Infinity - options.maxHeight ??= Infinity - - return this.cropperInstances[cropperComponentId] - .getCroppedCanvas(options) - .toDataURL(type, encoderOptions) - } - - getData (cropperComponentId, rounded) { - return this.cropperInstances[cropperComponentId] - .getData(rounded) - } - - getImageData (cropperComponentId) { - return this.cropperInstances[cropperComponentId] - .getImageData() - } - - move (cropperComponentId, offsetX, offsetY) { - return this.cropperInstances[cropperComponentId] - .move(offsetX, offsetY) - } - - moveTo (cropperComponentId, x, y) { - return this.cropperInstances[cropperComponentId] - .moveTo(x, y) - } - - replace (cropperComponentId, url, onlyColorChanged) { - return this.cropperInstances[cropperComponentId] - .replace(url, onlyColorChanged) - } - - reset (cropperComponentId) { - return this.cropperInstances[cropperComponentId] - .reset() - } - - rotate (cropperComponentId, degree) { - return this.cropperInstances[cropperComponentId] - .rotate(degree) - } - - rotateTo (cropperComponentId, degree) { - return this.cropperInstances[cropperComponentId] - .rotateTo(degree) - } - - scale (cropperComponentId, scaleX, scaleY) { - return this.cropperInstances[cropperComponentId] - .scale(scaleX, scaleY) - } - - scaleX (cropperComponentId, scaleX) { - return this.cropperInstances[cropperComponentId] - .scaleX(scaleX) - } - - scaleY (cropperComponentId, scaleY) { - return this.cropperInstances[cropperComponentId] - .scaleY(scaleY) - } - - setAspectRatio (cropperComponentId, aspectRatio) { - return this.cropperInstances[cropperComponentId] - .setAspectRatio(aspectRatio) - } - - setCanvasData (cropperComponentId, data) { - return this.cropperInstances[cropperComponentId] - .setCanvasData(data) - } - - setCropBoxData (cropperComponentId, data) { - return this.cropperInstances[cropperComponentId] - .setCropBoxData(data) - } - - setData (cropperComponentId, data) { - return this.cropperInstances[cropperComponentId] - .setData(data) - } - - setDragMode (cropperComponentId, dragMode) { - return this.cropperInstances[cropperComponentId] - .setDragMode(dragMode) - } - - zoom (cropperComponentId, ratio) { - return this.cropperInstances[cropperComponentId] - .zoom(ratio) - } - - zoomTo (cropperComponentId, ratio, pivotX, pivotY) { - return this.cropperInstances[cropperComponentId] - .zoomTo(ratio, { pivotX, pivotY }) - } - - noConflict () { - return Cropper.noConflict() // eslint-disable-line no-undef - } - - setDefaults (options) { - return Cropper.setDefaults(options) // eslint-disable-line no-undef - } - - getJSEventData (instance, correlationId) { - return { - isTrusted: instance.isTrusted, - detail: this.getJSEventDataDetail(instance), - type: instance.type, - eventPhase: instance.eventPhase, - bubbles: instance.bubbles, - cancelable: instance.cancelable, - defaultPrevented: instance.defaultPrevented, - composed: instance.composed, - timeStamp: instance.timeStamp, - returnValue: instance.returnValue, - cancelBubble: instance.cancelBubble, - correlationId - } - } - - getJSEventDataDetail (instance) { - if (instance.type === 'zoom') { - return { - oldRatio: instance.detail.oldRatio, - ratio: instance.detail.ratio, - originalEvent: instance.detail.originalEvent - ? DotNet.createJSObjectReference(instance.detail.originalEvent) // eslint-disable-line no-undef - : null - } - } else if (instance.type === 'cropstart' || instance.type === 'cropend' || instance.type === 'cropmove') { - return { - action: instance.detail.action, - originalEvent: instance.detail.originalEvent - ? DotNet.createJSObjectReference(instance.detail.originalEvent) // eslint-disable-line no-undef - : null - } - } - - return instance.detail - } - - onReady (imageObject, event, correlationId) { - const jSEventData = this.getJSEventData(event, correlationId) - imageObject.invokeMethodAsync('IsReady', jSEventData) - } - - onCropStart (imageObject, event, correlationId) { - const jSEventData = this.getJSEventData(event, correlationId) - imageObject.invokeMethodAsync('CropperIsStarted', jSEventData) - } - - onCropMove (imageObject, event, correlationId) { - const jSEventData = this.getJSEventData(event, correlationId) - imageObject.invokeMethodAsync('CropperIsMoved', jSEventData) - } - - onCropEnd (imageObject, event, correlationId) { - const jSEventData = this.getJSEventData(event, correlationId) - imageObject.invokeMethodAsync('CropperIsEnded', jSEventData) - } - - onCrop (imageObject, event, correlationId) { - const jSEventData = this.getJSEventData(event, correlationId) - imageObject.invokeMethodAsync('CropperIsCroped', jSEventData) - } - - onZoom (imageObject, event, correlationId) { - const jSEventData = this.getJSEventData(event, correlationId) - imageObject.invokeMethodAsync('CropperIsZoomed', jSEventData) - } - - initCropper (cropperComponentId, image, optionsImage, imageObject) { - if (image == null) { - throw new Error("Parameter 'image' must be is not null!") - } - - if (optionsImage == null) { - throw new Error("Parameter 'optionsImage' must be is not null!") - } - - const options = {} - const correlationId = optionsImage.correlationId - - if (imageObject != null) { - const self = this - - options.ready = function (event) { - self.onReady(imageObject, event, correlationId) - } - options.cropstart = function (event) { - self.onCropStart(imageObject, event, correlationId) - } - options.cropmove = function (event) { - self.onCropMove(imageObject, event, correlationId) - } - options.cropend = function (event) { - self.onCropEnd(imageObject, event, correlationId) - } - options.crop = function (event) { - self.onCrop(imageObject, event, correlationId) - } - options.zoom = function (event) { - self.onZoom(imageObject, event, correlationId) - } - } - - if (optionsImage != null) { - Object.entries(optionsImage)?.forEach(([key, value]) => { - options[key] = value - }) - } - - const cropper = new Cropper(image, options) // eslint-disable-line no-undef - - this.cropperInstances[cropperComponentId] = cropper - } - - async readBlobInChunks (blob, dotNetImageReceiverRef, maximumReceiveChunkSize) { - // Validate blob - if (!(blob instanceof Blob)) { - throw new TypeError('blob must be a valid Blob object.') - } - - // Validate dotNetImageReceiverRef - if (!dotNetImageReceiverRef || typeof dotNetImageReceiverRef.invokeMethodAsync !== 'function') { - throw new TypeError('dotNetImageReceiverRef must be a valid .NET object reference with an invokeMethodAsync function.') - } - - // Validate maximumReceiveChunkSize - if (maximumReceiveChunkSize != null && maximumReceiveChunkSize <= 0) { - throw new RangeError('maximumReceiveChunkSize must be greater than 0 bytes when specified.') - } - - // By default, blob.stream() reads the blob using internal chunking (typically 65536 bytes per chunk). - // To enforce a custom chunk size, especially to control serialized message size for JS interop or SignalR limits, we wrap it in a transformed ReadableStream. - // This allows us to split the default chunks further to stay within a maximum size constraint (e.g., for Blazor's JS interop or SignalR message limits). - let reader = null - - if (maximumReceiveChunkSize == null) { - reader = blob.stream().getReader() - } else { - const blobStream = blob.stream().getReader() - - // Binary estimation of JSON size - const getJsonSizeBinary = (chunk) => { - const length = chunk.length - - // Max 3 digits for the number (0 to 255) - const bytesPerElement = 3 - // Comma between elements - const commas = length - 1 - // For '[' and ']' - const brackets = 2 - - return (length * bytesPerElement) + commas + brackets - } - - // Create a custom stream that enforces max chunk size - const transformedStream = new ReadableStream({ - async pull (controller) { - const { done, value } = await blobStream.read() - - if (done) { - controller.close() - - return - } - - // Function to calculate JSON size for the current chunk using binary estimation - let offset = 0 - let lastGoodChunkSize = maximumReceiveChunkSize - - while (offset < value.length) { - // Start with the last known good chunk size, or the remaining length - let chunkSize = Math.min(lastGoodChunkSize, value.length - offset) - let chunk = value.slice(offset, offset + chunkSize) - let jsonSize = getJsonSizeBinary(chunk) - - // If the JSON size is too large, reduce the chunk size gradually - while (jsonSize > maximumReceiveChunkSize && chunkSize > 1) { - // Reduce the chunk size in steps of 512 bytes, but not below 1 byte - chunkSize = Math.max(chunkSize - 512, 1) - chunk = value.slice(offset, offset + chunkSize) - jsonSize = getJsonSizeBinary(chunk) - - // Stop reducing if the chunk size is already very small - if (chunkSize <= 512) { - break - } - } - - // Move the offset forward by the size of the chunk just sent with update the last good chunk size - lastGoodChunkSize = chunkSize - - offset += chunkSize - - controller.enqueue(chunk) - } - } - }) - - reader = transformedStream.getReader() - } - - try { - while (true) { - const { done, value } = await reader.read() - if (done) break - - await dotNetImageReceiverRef.invokeMethodAsync('ReceiveImageChunk', value) - } - - await dotNetImageReceiverRef.invokeMethodAsync('CompleteImageTransfer') - } catch (error) { - await dotNetImageReceiverRef.invokeMethodAsync('HandleImageProcessingError', error.toString()) - } - } - - sendImageInChunks (cropperComponentId, options, dotNetImageReceiverRef, type, encoderOptions, maximumReceiveChunkSize) { - options.maxWidth ??= Infinity - options.maxHeight ??= Infinity - - const cropperInstance = this.cropperInstances[cropperComponentId] - - setTimeout(() => { - cropperInstance.getCroppedCanvas(options).toBlob(async (blob) => { - await this.readBlobInChunks(blob, dotNetImageReceiverRef, maximumReceiveChunkSize) - }, type, encoderOptions) - }, 0) - } -} - -class CropperUrlImageHelper { - static async getImageUsingStreaming (imageStream) { - if (!imageStream || typeof imageStream.arrayBuffer !== 'function') { - throw new TypeError('Invalid image stream provided.') - } - - const arrayBuffer = await imageStream.arrayBuffer() - const blob = new Blob([arrayBuffer]) - return URL.createObjectURL(blob) - } - - static revokeObjectUrl (url) { - if (typeof url !== 'string') { - throw new TypeError('Expected a string URL to revoke.') - } - URL.revokeObjectURL(url) - } -} - -window.cropperUrlImageHelper = CropperUrlImageHelper -window.cropper = new CropperDecorator() From 1c59600399264b17376d6e91c3364790948e6291 Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Mon, 1 Dec 2025 00:03:41 +0200 Subject: [PATCH 02/58] fix ts file --- .../Cropper/cropperJsInterop.ts | 151 +++++++++++------- 1 file changed, 95 insertions(+), 56 deletions(-) diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts index 4d61dad3..b28ffc56 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts @@ -22,7 +22,7 @@ interface CropperExtendedOptions } export class CropperDecorator { - private cropperInstances: Record = {}; + private cropperInstances: Record = {}; clear(id: CropperId) { return this.cropperInstances[id].clear(); @@ -274,79 +274,118 @@ export class CropperDecorator { // Chunked Blob Streaming // -------------------------- - async readBlobInChunks( - blob: Blob, - receiverRef: any, - maximumReceiveChunkSize?: number - ) { + async readBlobInChunks(blob: Blob, dotNetImageReceiverRef: any, maximumReceiveChunkSize?: number) { + // Validate blob if (!(blob instanceof Blob)) { - throw new TypeError("blob must be a valid Blob"); + throw new TypeError('blob must be a valid Blob object.') } - const reader = maximumReceiveChunkSize - ? this.buildChunkedReader(blob, maximumReceiveChunkSize) - : blob.stream().getReader(); + // Validate dotNetImageReceiverRef + if (!dotNetImageReceiverRef || typeof dotNetImageReceiverRef.invokeMethodAsync !== 'function') { + throw new TypeError('dotNetImageReceiverRef must be a valid .NET object reference with an invokeMethodAsync function.') + } - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; + // Validate maximumReceiveChunkSize + if (maximumReceiveChunkSize != null && maximumReceiveChunkSize <= 0) { + throw new RangeError('maximumReceiveChunkSize must be greater than 0 bytes when specified.') + } + + // By default, blob.stream() reads the blob using internal chunking (typically 65536 bytes per chunk). + // To enforce a custom chunk size, especially to control serialized message size for JS interop or SignalR limits, we wrap it in a transformed ReadableStream. + // This allows us to split the default chunks further to stay within a maximum size constraint (e.g., for Blazor's JS interop or SignalR message limits). + let reader: ReadableStreamDefaultReader | null = null + + if (maximumReceiveChunkSize == null) { + reader = blob.stream().getReader() + } else { + const blobStream = blob.stream().getReader() + + // Binary estimation of JSON size + const getJsonSizeBinary = (chunk) => { + const length = chunk.length + + // Max 3 digits for the number (0 to 255) + const bytesPerElement = 3 + // Comma between elements + const commas = length - 1 + // For '[' and ']' + const brackets = 2 - await receiverRef.invokeMethodAsync("ReceiveImageChunk", value); + return (length * bytesPerElement) + commas + brackets } - await receiverRef.invokeMethodAsync("CompleteImageTransfer"); - } catch (err) { - await receiverRef.invokeMethodAsync( - "HandleImageProcessingError", - String(err) - ); - } - } + // Create a custom stream that enforces max chunk size + const transformedStream = new ReadableStream({ + async pull(controller) { + const { done, value } = await blobStream.read() - private buildChunkedReader(blob: Blob, maxBytes: number): ReadableStreamDefaultReader { - const blobReader = blob.stream().getReader(); + if (done) { + controller.close() - const stream = new ReadableStream({ - async pull(controller) { - const { done, value } = await blobReader.read(); - if (done || !value) { - controller.close(); - return; - } + return + } + + // Function to calculate JSON size for the current chunk using binary estimation + let offset = 0 + let lastGoodChunkSize = maximumReceiveChunkSize + + while (offset < value.length) { + // Start with the last known good chunk size, or the remaining length + let chunkSize = Math.min(lastGoodChunkSize, value.length - offset) + let chunk = value.slice(offset, offset + chunkSize) + let jsonSize = getJsonSizeBinary(chunk) + + // If the JSON size is too large, reduce the chunk size gradually + while (jsonSize > maximumReceiveChunkSize && chunkSize > 1) { + // Reduce the chunk size in steps of 512 bytes, but not below 1 byte + chunkSize = Math.max(chunkSize - 512, 1) + chunk = value.slice(offset, offset + chunkSize) + jsonSize = getJsonSizeBinary(chunk) + + // Stop reducing if the chunk size is already very small + if (chunkSize <= 512) { + break + } + } + + // Move the offset forward by the size of the chunk just sent with update the last good chunk size + lastGoodChunkSize = chunkSize - let offset = 0; + offset += chunkSize - while (offset < value.length) { - const slice = value.slice(offset, offset + maxBytes); - offset += slice.length; - controller.enqueue(slice); + controller.enqueue(chunk) + } } + }) + + reader = transformedStream.getReader() + } + + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + + await dotNetImageReceiverRef.invokeMethodAsync('ReceiveImageChunk', value) } - }); - return stream.getReader(); + await dotNetImageReceiverRef.invokeMethodAsync('CompleteImageTransfer') + } catch (error) { + await dotNetImageReceiverRef.invokeMethodAsync('HandleImageProcessingError', String(error)) + } } - sendImageInChunks( - id: CropperId, - options: any, - receiverRef: any, - type?: string, - encoderOptions?: number, - maximumReceiveChunkSize?: number - ) { - const instance = this.cropperInstances[id]; - options.maxWidth ??= Infinity; - options.maxHeight ??= Infinity; + sendImageInChunks(cropperComponentId: CropperId, options, dotNetImageReceiverRef: any, type?: string, encoderOptions?: number, maximumReceiveChunkSize?: number) { + options.maxWidth ??= Infinity + options.maxHeight ??= Infinity + + const cropperInstance = this.cropperInstances[cropperComponentId] setTimeout(() => { - instance.getCroppedCanvas(options).toBlob(async (blob) => { - if (blob) { - await this.readBlobInChunks(blob, receiverRef, maximumReceiveChunkSize); - } - }, type, encoderOptions); - }, 0); + cropperInstance.getCroppedCanvas(options).toBlob(async (blob) => { + await this.readBlobInChunks(blob, dotNetImageReceiverRef, maximumReceiveChunkSize) + }, type, encoderOptions) + }, 0) } } From e13b96a68c55d4432c93b330823cd406e23d4776 Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Fri, 5 Dec 2025 19:41:37 +0200 Subject: [PATCH 03/58] fix configs --- .github/workflows/ci.yml | 3 ++- .../Cropper.Blazor/Cropper.Blazor.csproj | 23 +++++++++++-------- .../Cropper/cropperJsInterop.ts | 4 ++-- .../Cropper.Blazor/tsconfig.json | 6 ++++- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0675cc3c..3968e5c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,8 +61,9 @@ jobs: VALIDATE_MARKDOWN_PRETTIER: false VALIDATE_YAML_PRETTIER: false VALIDATE_JSON_PRETTIER: false + VALIDATE_TYPESCRIPT_PRETTIER: false - FILTER_REGEX_EXCLUDE: '(\W|^)(obj/|bin/)|(\W|^)(.*([.]min[.]css))($)|(\W|^)(.*([.]min[.]js))($)' + FILTER_REGEX_EXCLUDE: '(\W|^)(obj/|bin/|node_modules/)|(\W|^)(.*([.]min[.]css))($)|(\W|^)(.*([.]min[.]js))($)' FILTER_REGEX_INCLUDE: "/github/workspace/src/Cropper.Blazor/.*|/github/workspace/.github/.*" JSCPD_CONFIG_FILE: ".jscpd.json" diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj index 89d5bf6a..a5bc6a35 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj @@ -75,7 +75,9 @@ + + @@ -83,17 +85,19 @@ - + + + - + @@ -113,6 +117,7 @@ + @@ -140,22 +145,22 @@
- + - + - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts index b28ffc56..206a8366 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts @@ -1,4 +1,4 @@ -import * as Cropper from 'cropperjs/src'; +import * as Cropper from 'cropperjs'; declare global { @@ -274,7 +274,7 @@ export class CropperDecorator { // Chunked Blob Streaming // -------------------------- - async readBlobInChunks(blob: Blob, dotNetImageReceiverRef: any, maximumReceiveChunkSize?: number) { + async readBlobInChunks(blob: Blob | null, dotNetImageReceiverRef: any, maximumReceiveChunkSize?: number) { // Validate blob if (!(blob instanceof Blob)) { throw new TypeError('blob must be a valid Blob object.') diff --git a/src/Cropper.Blazor/Cropper.Blazor/tsconfig.json b/src/Cropper.Blazor/Cropper.Blazor/tsconfig.json index e7f1aa4e..e39335d5 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/tsconfig.json +++ b/src/Cropper.Blazor/Cropper.Blazor/tsconfig.json @@ -10,7 +10,11 @@ "strict": true, "alwaysStrict": true, "preserveConstEnums": true, - "allowJs": false + "allowJs": false, + "baseUrl": ".", + "paths": { + "cropperjs": [ "node_modules/cropperjs/dist/cropper.esm.js" ] + } }, "exclude": [ "node_modules" From f191cd0a883f79f0d1190baa72b8eb5bf92d91a4 Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Fri, 5 Dec 2025 19:49:22 +0200 Subject: [PATCH 04/58] small fix --- src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj index a5bc6a35..138f1b4e 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj @@ -95,7 +95,7 @@ - + From f4b851d0566cce967373874baab9103d85cc5aae Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Fri, 5 Dec 2025 19:53:57 +0200 Subject: [PATCH 05/58] small fix --- src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj index 138f1b4e..4bb0046e 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj @@ -95,7 +95,7 @@ - + From 53314e8b875074890652731aec7c235f733da25d Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Fri, 5 Dec 2025 19:54:54 +0200 Subject: [PATCH 06/58] run webpack only one time --- src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj index 4bb0046e..10006a0e 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj @@ -100,7 +100,7 @@ - + From 06e9681d0653d142782c6ec027c0225231f246ac Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Fri, 5 Dec 2025 19:57:23 +0200 Subject: [PATCH 07/58] fix --- src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj index 10006a0e..e09e5dfb 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj @@ -97,7 +97,7 @@ - + From 23f8adda2184274b8b262d23a67d4f3a960e80ee Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Fri, 5 Dec 2025 20:17:37 +0200 Subject: [PATCH 08/58] fix errors --- .github/linters/.jscpd.json | 3 ++- .github/workflows/ci.yml | 2 +- .../Cropper.Blazor/Cropper/cropperJsInterop.ts | 2 +- src/Cropper.Blazor/Cropper.Blazor/tsconfig.json | 6 +----- src/Cropper.Blazor/Cropper.Blazor/webpack.config.js | 2 +- 5 files changed, 6 insertions(+), 9 deletions(-) diff --git a/.github/linters/.jscpd.json b/.github/linters/.jscpd.json index 05cb2c3d..8e44c666 100644 --- a/.github/linters/.jscpd.json +++ b/.github/linters/.jscpd.json @@ -7,7 +7,8 @@ "**/*.md", "**/*excubowebcompiler.json", "**/bin/**", - "**/obj/**" + "**/obj/**", + "**/node_modules/**" ], "absolute": true, "minTokens": 75 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc994c1f..aace4b6d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: persist-credentials: false - name: Super-Linter - uses: super-linter/super-linter@v8.2.1 + uses: super-linter/super-linter@v8.3.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts index 206a8366..16dd4653 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts @@ -1,4 +1,4 @@ -import * as Cropper from 'cropperjs'; +import * as Cropper from 'cropperjs/src'; declare global { diff --git a/src/Cropper.Blazor/Cropper.Blazor/tsconfig.json b/src/Cropper.Blazor/Cropper.Blazor/tsconfig.json index e39335d5..e7f1aa4e 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/tsconfig.json +++ b/src/Cropper.Blazor/Cropper.Blazor/tsconfig.json @@ -10,11 +10,7 @@ "strict": true, "alwaysStrict": true, "preserveConstEnums": true, - "allowJs": false, - "baseUrl": ".", - "paths": { - "cropperjs": [ "node_modules/cropperjs/dist/cropper.esm.js" ] - } + "allowJs": false }, "exclude": [ "node_modules" diff --git a/src/Cropper.Blazor/Cropper.Blazor/webpack.config.js b/src/Cropper.Blazor/Cropper.Blazor/webpack.config.js index 76f443b6..607e0fdc 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/webpack.config.js +++ b/src/Cropper.Blazor/Cropper.Blazor/webpack.config.js @@ -1,5 +1,5 @@ const path = require('path'); -const webpack = require('webpack'); +require('webpack'); const CleanCSS = require('clean-css') const CopyPlugin = require('copy-webpack-plugin') const TerserPlugin = require('terser-webpack-plugin') From 8e743c0c2b5e01dc1b9e18a8810d6d0c59ece3a2 Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Sat, 13 Dec 2025 20:43:37 +0200 Subject: [PATCH 09/58] add new structure for cropper *.ts files --- .../Client/Cropper.Blazor.Client.csproj | 6 +- .../Cropper.Blazor.Shared.csproj | 2 +- .../Cropper.Blazor.Testing.csproj | 8 +- .../Cropper.Blazor.UnitTests.csproj | 2 +- .../Cropper.Blazor/Cropper.Blazor.csproj | 8 +- .../Cropper/cropperJsInterop.ts | 170 ++++++++++-------- .../helpers/cropper-url-image-helper.ts | 12 ++ .../components/cropped-canvas-receiver.d.ts | 5 + .../components/cropper-component-base.d.ts | 22 +++ .../types/components/image-receiver.d.ts | 11 ++ .../Cropper/types/cropper-event-data.ts | 29 +++ .../Cropper/types/cropper-extended-options.ts | 8 + .../Cropper/types/global/dotnet-global.d.ts | 19 ++ .../Cropper.Blazor/tsconfig.json | 12 +- .../Cropper.Blazor/webpack.config.js | 7 +- .../Server/Cropper.Blazor.Server.csproj | 2 +- 16 files changed, 237 insertions(+), 86 deletions(-) create mode 100644 src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/cropper-url-image-helper.ts create mode 100644 src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/cropped-canvas-receiver.d.ts create mode 100644 src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/cropper-component-base.d.ts create mode 100644 src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/image-receiver.d.ts create mode 100644 src/Cropper.Blazor/Cropper.Blazor/Cropper/types/cropper-event-data.ts create mode 100644 src/Cropper.Blazor/Cropper.Blazor/Cropper/types/cropper-extended-options.ts create mode 100644 src/Cropper.Blazor/Cropper.Blazor/Cropper/types/global/dotnet-global.d.ts diff --git a/src/Cropper.Blazor/Client/Cropper.Blazor.Client.csproj b/src/Cropper.Blazor/Client/Cropper.Blazor.Client.csproj index 0da5ec72..d70cbf89 100644 --- a/src/Cropper.Blazor/Client/Cropper.Blazor.Client.csproj +++ b/src/Cropper.Blazor/Client/Cropper.Blazor.Client.csproj @@ -19,9 +19,9 @@ - - - + + + diff --git a/src/Cropper.Blazor/Cropper.Blazor.Shared/Cropper.Blazor.Shared.csproj b/src/Cropper.Blazor/Cropper.Blazor.Shared/Cropper.Blazor.Shared.csproj index e7e2ca5b..a3d5708b 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.Shared/Cropper.Blazor.Shared.csproj +++ b/src/Cropper.Blazor/Cropper.Blazor.Shared/Cropper.Blazor.Shared.csproj @@ -6,7 +6,7 @@ - + diff --git a/src/Cropper.Blazor/Cropper.Blazor.Testing/Cropper.Blazor.Testing.csproj b/src/Cropper.Blazor/Cropper.Blazor.Testing/Cropper.Blazor.Testing.csproj index 8916ab2d..135da976 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.Testing/Cropper.Blazor.Testing.csproj +++ b/src/Cropper.Blazor/Cropper.Blazor.Testing/Cropper.Blazor.Testing.csproj @@ -22,18 +22,18 @@ - + - + - - + + diff --git a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Cropper.Blazor.UnitTests.csproj b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Cropper.Blazor.UnitTests.csproj index a6bb8c4c..e2b7ef47 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Cropper.Blazor.UnitTests.csproj +++ b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Cropper.Blazor.UnitTests.csproj @@ -57,7 +57,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj index e09e5dfb..61f66fbe 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj @@ -95,7 +95,7 @@ - + @@ -120,6 +120,10 @@ + + + + @@ -141,7 +145,7 @@ - + diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts index 16dd4653..e2fee7be 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts @@ -1,13 +1,12 @@ -import * as Cropper from 'cropperjs/src'; - +import Cropper from 'cropperjs'; +import { ICropperComponentBase } from './types/components/cropper-component-base'; +import { CroppedCanvasReceiver } from './types/components/cropped-canvas-receiver'; +import { ImageReceiver } from './types/components/image-receiver'; +import type { CropperBlazor as CropperEventTypes } from './types/cropper-event-data'; +import type { CropperBlazor as CropperOptionsTypes } from './types/cropper-extended-options'; +import { CropperUrlImageHelper } from './helpers/cropper-url-image-helper'; declare global { - interface DotNet { - createJSObjectReference(obj: any): any; - } - - const DotNet: DotNet; - interface Window { cropper: CropperDecorator; cropperUrlImageHelper: typeof CropperUrlImageHelper; @@ -16,13 +15,8 @@ declare global { type CropperId = string; -interface CropperExtendedOptions - extends Cropper.default.Options { - correlationId?: string; -} - export class CropperDecorator { - private cropperInstances: Record = {}; + private cropperInstances: Record = {}; clear(id: CropperId) { return this.cropperInstances[id].clear(); @@ -34,8 +28,10 @@ export class CropperDecorator { destroy(id: CropperId) { const instance = this.cropperInstances[id]; + if (instance) { instance.destroy(); + delete this.cropperInstances[id]; } } @@ -48,11 +44,11 @@ export class CropperDecorator { return this.cropperInstances[id].enable(); } - getCanvasData(id: CropperId): Cropper.default.CanvasData { + getCanvasData(id: CropperId): Cropper.CanvasData { return this.cropperInstances[id].getCanvasData(); } - getContainerData(id: CropperId): Cropper.default.ContainerData { + getContainerData(id: CropperId): Cropper.ContainerData { return this.cropperInstances[id].getContainerData(); } @@ -60,16 +56,17 @@ export class CropperDecorator { return this.cropperInstances[id].getCropBoxData(); } - getCroppedCanvas(id: CropperId, options: any) { + getCroppedCanvas(id: CropperId, options: Cropper.GetCroppedCanvasOptions) { options.maxWidth ??= Infinity; options.maxHeight ??= Infinity; + return this.cropperInstances[id].getCroppedCanvas(options); } async getCroppedCanvasInBackground( id: CropperId, - options: any, - dotNetCanvasReceiverRef: any + options: Cropper.GetCroppedCanvasOptions, + dotNetCanvasReceiverRef: DotNetObjectReference ) { setTimeout(async () => { const canvas = this.getCroppedCanvas(id, options); @@ -83,7 +80,7 @@ export class CropperDecorator { getCroppedCanvasDataURL( id: CropperId, - options: any, + options: Cropper.GetCroppedCanvasOptions, type?: string, encoderOptions?: number ) { @@ -95,11 +92,11 @@ export class CropperDecorator { .toDataURL(type, encoderOptions); } - getData(id: CropperId, rounded?: boolean): Cropper.default.Data { + getData(id: CropperId, rounded?: boolean): Cropper.Data { return this.cropperInstances[id].getData(rounded); } - getImageData(id: CropperId): Cropper.default.ImageData { + getImageData(id: CropperId): Cropper.ImageData { return this.cropperInstances[id].getImageData(); } @@ -143,19 +140,19 @@ export class CropperDecorator { return this.cropperInstances[id].setAspectRatio(ratio); } - setCanvasData(id: CropperId, data: Cropper.default.CanvasData) { + setCanvasData(id: CropperId, data: Cropper.CanvasData) { return this.cropperInstances[id].setCanvasData(data); } - setCropBoxData(id: CropperId, data: any) { + setCropBoxData(id: CropperId, data: Cropper.SetCropBoxDataOptions) { return this.cropperInstances[id].setCropBoxData(data); } - setData(id: CropperId, data: Cropper.default.Data) { + setData(id: CropperId, data: Cropper.Data) { return this.cropperInstances[id].setData(data); } - setDragMode(id: CropperId, mode: Cropper.default.DragMode) { + setDragMode(id: CropperId, mode: Cropper.DragMode) { return this.cropperInstances[id].setDragMode(mode); } @@ -168,18 +165,21 @@ export class CropperDecorator { } noConflict() { - return Cropper.default.noConflict(); + return Cropper.noConflict(); } - setDefaults(options: Cropper.default.Options) { - Cropper.default.setDefaults(options); + setDefaults(options: Cropper.Options) { + Cropper.setDefaults(options); } // -------------------------- // Event serialization helpers // -------------------------- - getJSEventData(instance: any, correlationId: any) { + getJSEventData( + instance: Cropper.CropperEvent | Cropper.CropEvent | Cropper.CropStartEvent | Cropper.CropMoveEvent | Cropper.CropEndEvent, + correlationId: string | undefined): CropperEventTypes.CropperJSEventData { + return { isTrusted: instance.isTrusted, detail: this.getJSEventDataDetail(instance), @@ -196,85 +196,121 @@ export class CropperDecorator { }; } - getJSEventDataDetail(instance: any): any { + getJSEventDataDetail( + instance: Cropper.CropperEvent | Cropper.CropEvent | Cropper.CropStartEvent | Cropper.CropMoveEvent | Cropper.CropEndEvent) + : CropperEventTypes.CropperEventDataJS { if (instance.type === "zoom") { - return { + const zoomEventData: CropperEventTypes.CropperEventDataJS = { oldRatio: instance.detail.oldRatio, ratio: instance.detail.ratio, originalEvent: instance.detail.originalEvent ? DotNet.createJSObjectReference(instance.detail.originalEvent) : null }; - } - if (["cropstart", "cropend", "cropmove"].includes(instance.type)) { - return { + return zoomEventData; + } + else if (["cropstart", "cropend", "cropmove"].includes(instance.type)) { + const cropEventData: CropperEventTypes.CropperEventDataJS = { action: instance.detail.action, originalEvent: instance.detail.originalEvent ? DotNet.createJSObjectReference(instance.detail.originalEvent) : null }; + + return cropEventData; } return instance.detail; } - onReady(imageObject: any, e: any, id: any) { + onReady( + imageObject: DotNetObjectReference, + e: Cropper.ReadyEvent, id: string | undefined) + { imageObject.invokeMethodAsync("IsReady", this.getJSEventData(e, id)); } - onCropStart(imageObject: any, e: any, id: any) { + onCropStart( + imageObject: DotNetObjectReference, + e: Cropper.CropStartEvent, id: string | undefined) + { imageObject.invokeMethodAsync("CropperIsStarted", this.getJSEventData(e, id)); } - onCropMove(imageObject: any, e: any, id: any) { + onCropMove( + imageObject: DotNetObjectReference, + e: Cropper.CropMoveEvent, id: string | undefined) + { imageObject.invokeMethodAsync("CropperIsMoved", this.getJSEventData(e, id)); } - onCropEnd(imageObject: any, e: any, id: any) { + onCropEnd( + imageObject: DotNetObjectReference, + e: Cropper.CropEndEvent, id: string | undefined) + { imageObject.invokeMethodAsync("CropperIsEnded", this.getJSEventData(e, id)); } - onCrop(imageObject: any, e: any, id: any) { + onCrop( + imageObject: DotNetObjectReference, + e: Cropper.CropEvent, id: string | undefined) + { imageObject.invokeMethodAsync("CropperIsCroped", this.getJSEventData(e, id)); } - onZoom(imageObject: any, e: any, id: any) { + onZoom( + imageObject: DotNetObjectReference, + e: Cropper.ZoomEvent, id: string | undefined) + { imageObject.invokeMethodAsync("CropperIsZoomed", this.getJSEventData(e, id)); } initCropper( id: CropperId, - image: HTMLImageElement, - optionsImage: CropperExtendedOptions, - imageObject?: any + image: HTMLImageElement | HTMLCanvasElement, + optionsImage: CropperOptionsTypes.CropperExtendedOptions, + imageObject?: DotNetObjectReference ) { if (!image) throw new Error("Parameter 'image' must not be null"); if (!optionsImage) throw new Error("Parameter 'optionsImage' must not be null"); - const options: Cropper.default.Options = {}; - const correlationId = optionsImage.correlationId; + const options: Cropper.Options | Cropper.Options = {}; + const correlationId: string | undefined = optionsImage.correlationId; if (imageObject) { - options.ready = (e: any) => this.onReady(imageObject, e, correlationId); - options.cropstart = (e: any) => this.onCropStart(imageObject, e, correlationId); - options.cropmove = (e: any) => this.onCropMove(imageObject, e, correlationId); - options.cropend = (e: any) => this.onCropEnd(imageObject, e, correlationId); - options.crop = (e: any) => this.onCrop(imageObject, e, correlationId); - options.zoom = (e: any) => this.onZoom(imageObject, e, correlationId); + options.ready = (e: Cropper.ReadyEvent) => this.onReady(imageObject, e, correlationId); + options.cropstart = (e: Cropper.CropStartEvent) => this.onCropStart(imageObject, e, correlationId); + options.cropmove = (e: Cropper.CropMoveEvent) => this.onCropMove(imageObject, e, correlationId); + options.cropend = (e: Cropper.CropEndEvent) => this.onCropEnd(imageObject, e, correlationId); + options.crop = (e: Cropper.CropEvent) => this.onCrop(imageObject, e, correlationId); + options.zoom = (e: Cropper.ZoomEvent) => this.onZoom(imageObject, e, correlationId); } Object.assign(options, optionsImage); - const cropper = new Cropper.default(image, options); - this.cropperInstances[id] = cropper; + if (image instanceof HTMLImageElement) { + const cropper: Cropper = new Cropper(image, options as Cropper.Options); + this.cropperInstances[id] = cropper; + } else if (image instanceof HTMLCanvasElement) { + const cropper: Cropper = new Cropper(image, options as Cropper.Options); + this.cropperInstances[id] = cropper; + } else { + throw new Error( + `Unsupported element type for Cropper: ${Object.prototype.toString.call(image)}` + ); + } } // -------------------------- // Chunked Blob Streaming // -------------------------- - async readBlobInChunks(blob: Blob | null, dotNetImageReceiverRef: any, maximumReceiveChunkSize?: number) { + async readBlobInChunks( + blob: Blob | null, + dotNetImageReceiverRef: DotNetObjectReference, + maximumReceiveChunkSize?: number) + { // Validate blob if (!(blob instanceof Blob)) { throw new TypeError('blob must be a valid Blob object.') @@ -375,7 +411,14 @@ export class CropperDecorator { } } - sendImageInChunks(cropperComponentId: CropperId, options, dotNetImageReceiverRef: any, type?: string, encoderOptions?: number, maximumReceiveChunkSize?: number) { + sendImageInChunks( + cropperComponentId: CropperId, + options: Cropper.GetCroppedCanvasOptions, + dotNetImageReceiverRef: DotNetObjectReference, + type?: string, + encoderOptions?: number, + maximumReceiveChunkSize?: number) + { options.maxWidth ??= Infinity options.maxHeight ??= Infinity @@ -389,21 +432,6 @@ export class CropperDecorator { } } -// --------------------------------------------------- -// URL Image Helper -// --------------------------------------------------- - -export class CropperUrlImageHelper { - static async getImageUsingStreaming(imageStream: any): Promise { - const buf = await imageStream.arrayBuffer(); - const blob = new Blob([buf]); - return URL.createObjectURL(blob); - } - - static revokeObjectUrl(url: string) { - URL.revokeObjectURL(url); - } -} window.cropper = new CropperDecorator(); window.cropperUrlImageHelper = CropperUrlImageHelper; diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/cropper-url-image-helper.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/cropper-url-image-helper.ts new file mode 100644 index 00000000..16acb561 --- /dev/null +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/cropper-url-image-helper.ts @@ -0,0 +1,12 @@ +export class CropperUrlImageHelper { + static async getImageUsingStreaming(imageStream: DotNetStreamReference): Promise { + const buf = await imageStream.arrayBuffer(); + const blob = new Blob([buf]); + + return URL.createObjectURL(blob); + } + + static revokeObjectUrl(url: string) { + URL.revokeObjectURL(url); + } +} \ No newline at end of file diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/cropped-canvas-receiver.d.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/cropped-canvas-receiver.d.ts new file mode 100644 index 00000000..1be0f4fd --- /dev/null +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/cropped-canvas-receiver.d.ts @@ -0,0 +1,5 @@ +/** Represents a receiver for a cropped canvas streamed from JS to .NET */ +export interface CroppedCanvasReceiver { + /** Called when a cropped canvas is received from JS */ + ReceiveCanvasReference(canvasRef: HTMLCanvasElement): void; +} \ No newline at end of file diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/cropper-component-base.d.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/cropper-component-base.d.ts new file mode 100644 index 00000000..7db213a1 --- /dev/null +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/cropper-component-base.d.ts @@ -0,0 +1,22 @@ +import Cropper from 'cropperjs'; + +/** Represents the C# cropper component methods callable from JS */ +export interface ICropperComponentBase { + /** Called when the cropper is ready */ + IsReady(eventData: Cropper.CropEventData | Cropper.ZoomEventData): void; + + /** Called when cropping starts */ + CropperIsStarted(eventData: Cropper.CropEventData): void; + + /** Called when cropping is moving */ + CropperIsMoved(eventData: Cropper.CropEventData): void; + + /** Called when cropping ends */ + CropperIsEnded(eventData: Cropper.CropEventData): void; + + /** Called on crop event */ + CropperIsCroped(eventData: Cropper.CropEventData): void; + + /** Called on zoom event */ + CropperIsZoomed(eventData: Cropper.ZoomEventData): void; +} diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/image-receiver.d.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/image-receiver.d.ts new file mode 100644 index 00000000..19501c8c --- /dev/null +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/image-receiver.d.ts @@ -0,0 +1,11 @@ +/** Represents a receiver for image data streamed from JS to .NET */ +export interface ImageReceiver { + /** Called when an error occurs during image processing */ + HandleImageProcessingError(errorMessage: string): void; + + /** Called to receive a chunk of image data from JS */ + ReceiveImageChunk(chunk: Uint8Array): Promise; + + /** Called when the image transfer is complete */ + CompleteImageTransfer(): void; +} \ No newline at end of file diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/cropper-event-data.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/cropper-event-data.ts new file mode 100644 index 00000000..c8105e55 --- /dev/null +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/cropper-event-data.ts @@ -0,0 +1,29 @@ +export namespace CropperBlazor { + export type CropEventDataJS = { + action: string; + originalEvent: JsObjectReference | null; + }; + + export type ZoomEventDataJS = { + oldRatio: number; + ratio: number; + originalEvent: JsObjectReference | null; + }; + + export type CropperEventDataJS = CropEventDataJS | ZoomEventDataJS; + + export type CropperJSEventData = { + isTrusted: boolean; + detail: CropperEventDataJS; + type: string; + eventPhase: number; + bubbles: boolean; + cancelable: boolean; + defaultPrevented: boolean; + composed: boolean; + timeStamp: number; + returnValue: any; + cancelBubble: boolean; + correlationId?: string; + }; +} \ No newline at end of file diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/cropper-extended-options.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/cropper-extended-options.ts new file mode 100644 index 00000000..bd1b5379 --- /dev/null +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/cropper-extended-options.ts @@ -0,0 +1,8 @@ +import Cropper from 'cropperjs'; + +export namespace CropperBlazor { + export type CropperExtendedOptions = + Cropper.Options & { + correlationId?: string; + }; +} \ No newline at end of file diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/global/dotnet-global.d.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/global/dotnet-global.d.ts new file mode 100644 index 00000000..c8a77243 --- /dev/null +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/global/dotnet-global.d.ts @@ -0,0 +1,19 @@ +declare const DotNet: DotNetNamespace; + +interface DotNetNamespace { + invokeMethodAsync(assemblyName: string, methodIdentifier: string, ...args: any[]): Promise; + + createJSObjectReference(jsObject: any): JsObjectReference; +} + +interface DotNetStreamReference { + arrayBuffer(): Promise; +} + +interface DotNetObjectReference { + invokeMethodAsync(methodName: keyof T, ...args: any[]): Promise; +} + +interface JsObjectReference { + __jsObjectId: number; +} diff --git a/src/Cropper.Blazor/Cropper.Blazor/tsconfig.json b/src/Cropper.Blazor/Cropper.Blazor/tsconfig.json index e7f1aa4e..9220bade 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/tsconfig.json +++ b/src/Cropper.Blazor/Cropper.Blazor/tsconfig.json @@ -6,13 +6,23 @@ "sourceMap": true, "target": "es6", "module": "commonjs", + "moduleResolution": "node", "lib": [ "es2016", "dom" ], "strict": true, "alwaysStrict": true, "preserveConstEnums": true, - "allowJs": false + "allowJs": false, + "baseUrl": ".", + "paths": { + "cropperjs/*": [ "node_modules/cropperjs/src/*" ] + } }, "exclude": [ "node_modules" + ], + "include": [ + "Cropper/types/global/*.d.ts", + "Cropper/**/*.ts", + "Cropper/*.ts" ] } diff --git a/src/Cropper.Blazor/Cropper.Blazor/webpack.config.js b/src/Cropper.Blazor/Cropper.Blazor/webpack.config.js index 607e0fdc..986c48c1 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/webpack.config.js +++ b/src/Cropper.Blazor/Cropper.Blazor/webpack.config.js @@ -1,12 +1,15 @@ -const path = require('path'); require('webpack'); +const path = require('path'); const CleanCSS = require('clean-css') const CopyPlugin = require('copy-webpack-plugin') const TerserPlugin = require('terser-webpack-plugin') module.exports = (env, args) => ({ resolve: { - extensions: ['.ts', '.js', '.css'] + extensions: ['.ts', '.js', '.css'], + alias: { + cropperjs: path.resolve(__dirname, 'node_modules/cropperjs/src') + } }, devtool: args.mode === 'development' ? 'inline-source-map' : 'hidden-source-map', module: { diff --git a/src/Cropper.Blazor/Server/Cropper.Blazor.Server.csproj b/src/Cropper.Blazor/Server/Cropper.Blazor.Server.csproj index aad7e013..5acc3f99 100644 --- a/src/Cropper.Blazor/Server/Cropper.Blazor.Server.csproj +++ b/src/Cropper.Blazor/Server/Cropper.Blazor.Server.csproj @@ -7,7 +7,7 @@ - + From a71d8aa97032ab50885dbbda7b2beaa88f43a1f2 Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Sat, 13 Dec 2025 20:53:33 +0200 Subject: [PATCH 10/58] fix copy css for cropper --- src/Cropper.Blazor/Cropper.Blazor/webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cropper.Blazor/Cropper.Blazor/webpack.config.js b/src/Cropper.Blazor/Cropper.Blazor/webpack.config.js index 986c48c1..468e6846 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/webpack.config.js +++ b/src/Cropper.Blazor/Cropper.Blazor/webpack.config.js @@ -28,7 +28,7 @@ module.exports = (env, args) => ({ plugins: [new CopyPlugin({ patterns: [{ to: 'cropper.min.css', - from: path.resolve(__dirname, 'node_modules/cropperjs/src/css', 'cropper.css'), + from: path.resolve(__dirname, 'node_modules/cropperjs/dist', 'cropper.min.css'), transform: content => (new CleanCSS({ level: 2 }).minify(content)).styles From 90806c29f2e6088f06b43f95249edd65a68a8469 Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Mon, 15 Dec 2025 00:40:17 +0200 Subject: [PATCH 11/58] fix building project for multi-TFM build --- .../Cropper.Blazor/Cropper.Blazor.csproj | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj index 61f66fbe..98b4e902 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj @@ -75,7 +75,6 @@ - @@ -90,24 +89,51 @@ - - - + + + + + + + + + - + + + + + + + + + + + @@ -156,7 +182,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 69b0ee21e5c7343e291fa234c8e615d79749a20c Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim <50423072+MaxymGorn@users.noreply.github.com> Date: Mon, 15 Dec 2025 01:25:47 +0200 Subject: [PATCH 12/58] Separate DotNet build steps for library and project --- .github/workflows/build-test-template.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-test-template.yml b/.github/workflows/build-test-template.yml index 66162493..a5035b53 100644 --- a/.github/workflows/build-test-template.yml +++ b/.github/workflows/build-test-template.yml @@ -56,8 +56,12 @@ jobs: - name: Restore dotnet tool run: dotnet tool restore working-directory: src/Cropper.Blazor/Cropper.Blazor - - - name: DotNet Build + + - name: DotNet Build library + run: dotnet build -c "$CONFIGURATION" --no-restore + working-directory: src/Cropper.Blazor/Cropper.Blazor + + - name: DotNet Build project run: dotnet build -c "$CONFIGURATION" --no-restore working-directory: src/Cropper.Blazor From e1f0e37fb3c966c435536f2a1669c641742b639d Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Mon, 15 Dec 2025 02:19:53 +0200 Subject: [PATCH 13/58] small fix --- .../Cropper.Blazor/Cropper.Blazor.csproj | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj index 98b4e902..4ac44bc7 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj @@ -118,6 +118,12 @@ for multi-target builds. --> + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + @@ -182,13 +188,6 @@ - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - From eaea10dbf700646b30443170386148bea0ebe843 Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Sat, 27 Dec 2025 22:06:48 +0200 Subject: [PATCH 14/58] fix namespaces for .ts files --- .../Cropper.Blazor/Cropper.Blazor.csproj | 1 + .../Cropper/cropperJsInterop.ts | 50 ++++++++++--------- .../helpers/cropper-url-image-helper.ts | 22 ++++---- .../components/cropped-canvas-receiver.d.ts | 10 ++-- .../components/cropper-component-base.d.ts | 30 +++++------ .../types/components/image-receiver.d.ts | 18 ++++--- .../Cropper/types/data/crop-event-data.ts | 8 +++ .../types/{ => data}/cropper-event-data.ts | 16 ++---- .../{ => data}/cropper-extended-options.ts | 2 +- .../Cropper/types/data/zoom-event-data.ts | 9 ++++ .../Cropper/types/global/dotnet-global.d.ts | 32 ++++++------ .../Cropper.Blazor/tsconfig.json | 3 +- 12 files changed, 115 insertions(+), 86 deletions(-) create mode 100644 src/Cropper.Blazor/Cropper.Blazor/Cropper/types/data/crop-event-data.ts rename src/Cropper.Blazor/Cropper.Blazor/Cropper/types/{ => data}/cropper-event-data.ts (51%) rename src/Cropper.Blazor/Cropper.Blazor/Cropper/types/{ => data}/cropper-extended-options.ts (85%) create mode 100644 src/Cropper.Blazor/Cropper.Blazor/Cropper/types/data/zoom-event-data.ts diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj index 4ac44bc7..9a03b202 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj @@ -118,6 +118,7 @@ for multi-target builds. --> + all diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts index e2fee7be..d0cbe854 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts @@ -1,18 +1,22 @@ import Cropper from 'cropperjs'; -import { ICropperComponentBase } from './types/components/cropper-component-base'; -import { CroppedCanvasReceiver } from './types/components/cropped-canvas-receiver'; -import { ImageReceiver } from './types/components/image-receiver'; -import type { CropperBlazor as CropperEventTypes } from './types/cropper-event-data'; -import type { CropperBlazor as CropperOptionsTypes } from './types/cropper-extended-options'; -import { CropperUrlImageHelper } from './helpers/cropper-url-image-helper'; +import type { CropperBlazor as CropperComponentBaseTypes } from './types/components/cropper-component-base'; +import type { CropperBlazor as CroppedCanvasReceiverTypes } from './types/components/cropped-canvas-receiver'; +import type { CropperBlazor as ImageReceiverTypes } from './types/components/image-receiver'; +import type { CropperBlazor as DataEventTypes } from './types/data/cropper-event-data'; +import type { CropperBlazor as DataOptionsTypes } from './types/data/cropper-extended-options'; +import type { CropperBlazor as DotNetTypes } from './types/global/dotnet-global'; +import { CropperBlazor } from './helpers/cropper-url-image-helper'; + declare global { interface Window { cropper: CropperDecorator; - cropperUrlImageHelper: typeof CropperUrlImageHelper; + cropperUrlImageHelper: CropperBlazor.Helpers.CropperUrlImageHelper; } } +declare const DotNet: DotNetTypes.Global.DotNetNamespace; + type CropperId = string; export class CropperDecorator { @@ -66,7 +70,7 @@ export class CropperDecorator { async getCroppedCanvasInBackground( id: CropperId, options: Cropper.GetCroppedCanvasOptions, - dotNetCanvasReceiverRef: DotNetObjectReference + dotNetCanvasReceiverRef: DotNetTypes.Global.DotNetObjectReference ) { setTimeout(async () => { const canvas = this.getCroppedCanvas(id, options); @@ -178,7 +182,7 @@ export class CropperDecorator { getJSEventData( instance: Cropper.CropperEvent | Cropper.CropEvent | Cropper.CropStartEvent | Cropper.CropMoveEvent | Cropper.CropEndEvent, - correlationId: string | undefined): CropperEventTypes.CropperJSEventData { + correlationId: string | undefined): DataEventTypes.Data.CropperJSEventData { return { isTrusted: instance.isTrusted, @@ -198,9 +202,9 @@ export class CropperDecorator { getJSEventDataDetail( instance: Cropper.CropperEvent | Cropper.CropEvent | Cropper.CropStartEvent | Cropper.CropMoveEvent | Cropper.CropEndEvent) - : CropperEventTypes.CropperEventDataJS { + : DataEventTypes.Data.CropperEventDataJS { if (instance.type === "zoom") { - const zoomEventData: CropperEventTypes.CropperEventDataJS = { + const zoomEventData: DataEventTypes.Data.CropperEventDataJS = { oldRatio: instance.detail.oldRatio, ratio: instance.detail.ratio, originalEvent: instance.detail.originalEvent @@ -211,7 +215,7 @@ export class CropperDecorator { return zoomEventData; } else if (["cropstart", "cropend", "cropmove"].includes(instance.type)) { - const cropEventData: CropperEventTypes.CropperEventDataJS = { + const cropEventData: DataEventTypes.Data.CropperEventDataJS = { action: instance.detail.action, originalEvent: instance.detail.originalEvent ? DotNet.createJSObjectReference(instance.detail.originalEvent) @@ -225,42 +229,42 @@ export class CropperDecorator { } onReady( - imageObject: DotNetObjectReference, + imageObject: DotNetTypes.Global.DotNetObjectReference, e: Cropper.ReadyEvent, id: string | undefined) { imageObject.invokeMethodAsync("IsReady", this.getJSEventData(e, id)); } onCropStart( - imageObject: DotNetObjectReference, + imageObject: DotNetTypes.Global.DotNetObjectReference, e: Cropper.CropStartEvent, id: string | undefined) { imageObject.invokeMethodAsync("CropperIsStarted", this.getJSEventData(e, id)); } onCropMove( - imageObject: DotNetObjectReference, + imageObject: DotNetTypes.Global.DotNetObjectReference, e: Cropper.CropMoveEvent, id: string | undefined) { imageObject.invokeMethodAsync("CropperIsMoved", this.getJSEventData(e, id)); } onCropEnd( - imageObject: DotNetObjectReference, + imageObject: DotNetTypes.Global.DotNetObjectReference, e: Cropper.CropEndEvent, id: string | undefined) { imageObject.invokeMethodAsync("CropperIsEnded", this.getJSEventData(e, id)); } onCrop( - imageObject: DotNetObjectReference, + imageObject: DotNetTypes.Global.DotNetObjectReference, e: Cropper.CropEvent, id: string | undefined) { imageObject.invokeMethodAsync("CropperIsCroped", this.getJSEventData(e, id)); } onZoom( - imageObject: DotNetObjectReference, + imageObject: DotNetTypes.Global.DotNetObjectReference, e: Cropper.ZoomEvent, id: string | undefined) { imageObject.invokeMethodAsync("CropperIsZoomed", this.getJSEventData(e, id)); @@ -269,8 +273,8 @@ export class CropperDecorator { initCropper( id: CropperId, image: HTMLImageElement | HTMLCanvasElement, - optionsImage: CropperOptionsTypes.CropperExtendedOptions, - imageObject?: DotNetObjectReference + optionsImage: DataOptionsTypes.Data.CropperExtendedOptions, + imageObject?: DotNetTypes.Global.DotNetObjectReference ) { if (!image) throw new Error("Parameter 'image' must not be null"); if (!optionsImage) throw new Error("Parameter 'optionsImage' must not be null"); @@ -308,7 +312,7 @@ export class CropperDecorator { async readBlobInChunks( blob: Blob | null, - dotNetImageReceiverRef: DotNetObjectReference, + dotNetImageReceiverRef: DotNetTypes.Global.DotNetObjectReference, maximumReceiveChunkSize?: number) { // Validate blob @@ -414,7 +418,7 @@ export class CropperDecorator { sendImageInChunks( cropperComponentId: CropperId, options: Cropper.GetCroppedCanvasOptions, - dotNetImageReceiverRef: DotNetObjectReference, + dotNetImageReceiverRef: DotNetTypes.Global.DotNetObjectReference, type?: string, encoderOptions?: number, maximumReceiveChunkSize?: number) @@ -434,4 +438,4 @@ export class CropperDecorator { window.cropper = new CropperDecorator(); -window.cropperUrlImageHelper = CropperUrlImageHelper; +window.cropperUrlImageHelper = new CropperBlazor.Helpers.CropperUrlImageHelper(); diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/cropper-url-image-helper.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/cropper-url-image-helper.ts index 16acb561..3a902715 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/cropper-url-image-helper.ts +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/cropper-url-image-helper.ts @@ -1,12 +1,16 @@ -export class CropperUrlImageHelper { - static async getImageUsingStreaming(imageStream: DotNetStreamReference): Promise { - const buf = await imageStream.arrayBuffer(); - const blob = new Blob([buf]); +import type { CropperBlazor as DotNetTypes } from '../types/global/dotnet-global'; - return URL.createObjectURL(blob); - } +export namespace CropperBlazor.Helpers { + export class CropperUrlImageHelper { + static async getImageUsingStreaming(imageStream: DotNetTypes.Global.DotNetStreamReference): Promise { + const buf = await imageStream.arrayBuffer(); + const blob = new Blob([buf]); + + return URL.createObjectURL(blob); + } - static revokeObjectUrl(url: string) { - URL.revokeObjectURL(url); + static revokeObjectUrl(url: string) { + URL.revokeObjectURL(url); + } } -} \ No newline at end of file +} diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/cropped-canvas-receiver.d.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/cropped-canvas-receiver.d.ts index 1be0f4fd..18475b34 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/cropped-canvas-receiver.d.ts +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/cropped-canvas-receiver.d.ts @@ -1,5 +1,7 @@ -/** Represents a receiver for a cropped canvas streamed from JS to .NET */ -export interface CroppedCanvasReceiver { - /** Called when a cropped canvas is received from JS */ - ReceiveCanvasReference(canvasRef: HTMLCanvasElement): void; +export namespace CropperBlazor.Components { + /** Represents a receiver for a cropped canvas streamed from JS to .NET */ + export interface CroppedCanvasReceiver { + /** Called when a cropped canvas is received from JS */ + ReceiveCanvasReference(canvasRef: HTMLCanvasElement): void; + } } \ No newline at end of file diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/cropper-component-base.d.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/cropper-component-base.d.ts index 7db213a1..d107c745 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/cropper-component-base.d.ts +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/cropper-component-base.d.ts @@ -1,22 +1,24 @@ import Cropper from 'cropperjs'; -/** Represents the C# cropper component methods callable from JS */ -export interface ICropperComponentBase { - /** Called when the cropper is ready */ - IsReady(eventData: Cropper.CropEventData | Cropper.ZoomEventData): void; +export namespace CropperBlazor.Components { + /** Represents the C# cropper component methods callable from JS */ + export interface ICropperComponentBase { + /** Called when the cropper is ready */ + IsReady(eventData: Cropper.CropEventData | Cropper.ZoomEventData): void; - /** Called when cropping starts */ - CropperIsStarted(eventData: Cropper.CropEventData): void; + /** Called when cropping starts */ + CropperIsStarted(eventData: Cropper.CropEventData): void; - /** Called when cropping is moving */ - CropperIsMoved(eventData: Cropper.CropEventData): void; + /** Called when cropping is moving */ + CropperIsMoved(eventData: Cropper.CropEventData): void; - /** Called when cropping ends */ - CropperIsEnded(eventData: Cropper.CropEventData): void; + /** Called when cropping ends */ + CropperIsEnded(eventData: Cropper.CropEventData): void; - /** Called on crop event */ - CropperIsCroped(eventData: Cropper.CropEventData): void; + /** Called on crop event */ + CropperIsCroped(eventData: Cropper.CropEventData): void; - /** Called on zoom event */ - CropperIsZoomed(eventData: Cropper.ZoomEventData): void; + /** Called on zoom event */ + CropperIsZoomed(eventData: Cropper.ZoomEventData): void; + } } diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/image-receiver.d.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/image-receiver.d.ts index 19501c8c..d30c846a 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/image-receiver.d.ts +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/image-receiver.d.ts @@ -1,11 +1,13 @@ -/** Represents a receiver for image data streamed from JS to .NET */ -export interface ImageReceiver { - /** Called when an error occurs during image processing */ - HandleImageProcessingError(errorMessage: string): void; +export namespace CropperBlazor.Components { + /** Represents a receiver for image data streamed from JS to .NET */ + export interface ImageReceiver { + /** Called when an error occurs during image processing */ + HandleImageProcessingError(errorMessage: string): void; - /** Called to receive a chunk of image data from JS */ - ReceiveImageChunk(chunk: Uint8Array): Promise; + /** Called to receive a chunk of image data from JS */ + ReceiveImageChunk(chunk: Uint8Array): Promise; - /** Called when the image transfer is complete */ - CompleteImageTransfer(): void; + /** Called when the image transfer is complete */ + CompleteImageTransfer(): void; + } } \ No newline at end of file diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/data/crop-event-data.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/data/crop-event-data.ts new file mode 100644 index 00000000..31c6519e --- /dev/null +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/data/crop-event-data.ts @@ -0,0 +1,8 @@ +import type { CropperBlazor as DotNetTypes } from '../../types/global/dotnet-global'; + +export namespace CropperBlazor.Data { + export type CropEventDataJS = { + action: string; + originalEvent: DotNetTypes.Global.JsObjectReference | null; + }; +} \ No newline at end of file diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/cropper-event-data.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/data/cropper-event-data.ts similarity index 51% rename from src/Cropper.Blazor/Cropper.Blazor/Cropper/types/cropper-event-data.ts rename to src/Cropper.Blazor/Cropper.Blazor/Cropper/types/data/cropper-event-data.ts index c8105e55..149b4b57 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/cropper-event-data.ts +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/data/cropper-event-data.ts @@ -1,16 +1,8 @@ -export namespace CropperBlazor { - export type CropEventDataJS = { - action: string; - originalEvent: JsObjectReference | null; - }; - - export type ZoomEventDataJS = { - oldRatio: number; - ratio: number; - originalEvent: JsObjectReference | null; - }; +import type { CropperBlazor as CropEventDataTypes } from './crop-event-data'; +import type { CropperBlazor as ZoomEventDataTypes } from './zoom-event-data'; - export type CropperEventDataJS = CropEventDataJS | ZoomEventDataJS; +export namespace CropperBlazor.Data { + export type CropperEventDataJS = CropEventDataTypes.Data.CropEventDataJS | ZoomEventDataTypes.Data.ZoomEventDataJS; export type CropperJSEventData = { isTrusted: boolean; diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/cropper-extended-options.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/data/cropper-extended-options.ts similarity index 85% rename from src/Cropper.Blazor/Cropper.Blazor/Cropper/types/cropper-extended-options.ts rename to src/Cropper.Blazor/Cropper.Blazor/Cropper/types/data/cropper-extended-options.ts index bd1b5379..634c9780 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/cropper-extended-options.ts +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/data/cropper-extended-options.ts @@ -1,6 +1,6 @@ import Cropper from 'cropperjs'; -export namespace CropperBlazor { +export namespace CropperBlazor.Data { export type CropperExtendedOptions = Cropper.Options & { correlationId?: string; diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/data/zoom-event-data.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/data/zoom-event-data.ts new file mode 100644 index 00000000..28f9b2a8 --- /dev/null +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/data/zoom-event-data.ts @@ -0,0 +1,9 @@ +import type { CropperBlazor as DotNetTypes } from '../../types/global/dotnet-global'; + +export namespace CropperBlazor.Data { + export type ZoomEventDataJS = { + oldRatio: number; + ratio: number; + originalEvent: DotNetTypes.Global.JsObjectReference | null; + }; +} \ No newline at end of file diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/global/dotnet-global.d.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/global/dotnet-global.d.ts index c8a77243..e1d9ae7d 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/global/dotnet-global.d.ts +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/global/dotnet-global.d.ts @@ -1,19 +1,23 @@ -declare const DotNet: DotNetNamespace; +/** + * Represents the global DotNet object exposed by Blazor. + * Used to invoke .NET methods from JavaScript. + */ +export namespace CropperBlazor.Global { + interface DotNetNamespace { + invokeMethodAsync(assemblyName: string, methodIdentifier: string, ...args: any[]): Promise; -interface DotNetNamespace { - invokeMethodAsync(assemblyName: string, methodIdentifier: string, ...args: any[]): Promise; + createJSObjectReference(jsObject: any): JsObjectReference; + } - createJSObjectReference(jsObject: any): JsObjectReference; -} - -interface DotNetStreamReference { - arrayBuffer(): Promise; -} + interface DotNetStreamReference { + arrayBuffer(): Promise; + } -interface DotNetObjectReference { - invokeMethodAsync(methodName: keyof T, ...args: any[]): Promise; -} + interface DotNetObjectReference { + invokeMethodAsync(methodName: keyof T, ...args: any[]): Promise; + } -interface JsObjectReference { - __jsObjectId: number; + interface JsObjectReference { + __jsObjectId: number; + } } diff --git a/src/Cropper.Blazor/Cropper.Blazor/tsconfig.json b/src/Cropper.Blazor/Cropper.Blazor/tsconfig.json index 9220bade..31d64c52 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/tsconfig.json +++ b/src/Cropper.Blazor/Cropper.Blazor/tsconfig.json @@ -15,7 +15,8 @@ "baseUrl": ".", "paths": { "cropperjs/*": [ "node_modules/cropperjs/src/*" ] - } + }, + "declaration": false }, "exclude": [ "node_modules" From 44ab07edeb4ef3f0f8a3dd81bbccea91d82e316f Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Mon, 29 Dec 2025 19:41:27 +0200 Subject: [PATCH 15/58] add vitest --- .gitignore | 1 + src/Cropper.Blazor/Cropper.Blazor.sln | 1 + .../Cropper/cropperJsInterop.test.ts | 11 +++++ .../Cropper/cropperJsInterop.ts | 8 ++-- .../helpers/cropper-url-image-helper.test.ts | 44 +++++++++++++++++++ .../Cropper.Blazor/package.json | 9 +++- .../Cropper.Blazor/vitest.setup.ts | 23 ++++++++++ .../Cropper.Blazor/webpack.config.js | 8 +++- 8 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.test.ts create mode 100644 src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/cropper-url-image-helper.test.ts create mode 100644 src/Cropper.Blazor/Cropper.Blazor/vitest.setup.ts diff --git a/.gitignore b/.gitignore index aaf655f4..28ffa305 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,5 @@ _ReSharper*/ packages/ # Node.js node_modules/ +coverage/ package-lock.json diff --git a/src/Cropper.Blazor/Cropper.Blazor.sln b/src/Cropper.Blazor/Cropper.Blazor.sln index 79ea63f1..b1a1ff5b 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.sln +++ b/src/Cropper.Blazor/Cropper.Blazor.sln @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject ..\..\.editorconfig = ..\..\.editorconfig ..\..\.gitattributes = ..\..\.gitattributes + ..\..\.gitignore = ..\..\.gitignore ..\..\.github\codecov.yml = ..\..\.github\codecov.yml ..\..\.github\CODE_OF_CONDUCT.md = ..\..\.github\CODE_OF_CONDUCT.md ..\..\.github\PULL_REQUEST_TEMPLATE.md = ..\..\.github\PULL_REQUEST_TEMPLATE.md diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.test.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.test.ts new file mode 100644 index 00000000..c99620da --- /dev/null +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.test.ts @@ -0,0 +1,11 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { CropperDecorator } from './cropperJsInterop'; +import { CropperBlazor } from './helpers/cropper-url-image-helper'; + + +describe('CropperDecorator', () => { + it('should be instantiable', () => { + const instance = new CropperDecorator(); + expect(instance).toBeInstanceOf(CropperDecorator); + }); +}); \ No newline at end of file diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts index d0cbe854..d3ee3803 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts @@ -436,6 +436,8 @@ export class CropperDecorator { } } - -window.cropper = new CropperDecorator(); -window.cropperUrlImageHelper = new CropperBlazor.Helpers.CropperUrlImageHelper(); +if (typeof window !== 'undefined') +{ + window.cropper = new CropperDecorator(); + window.cropperUrlImageHelper = new CropperBlazor.Helpers.CropperUrlImageHelper(); +} \ No newline at end of file diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/cropper-url-image-helper.test.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/cropper-url-image-helper.test.ts new file mode 100644 index 00000000..3438a222 --- /dev/null +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/cropper-url-image-helper.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { CropperBlazor } from './cropper-url-image-helper'; + +// Minimal mock of DotNetStreamReference +class MockDotNetStreamReference { + constructor(private data: ArrayBuffer) { } + + async arrayBuffer(): Promise { + return this.data; + } +} + +describe('CropperUrlImageHelper', () => { + const mockObjectUrl = 'blob:http://localhost/mock-url'; + + beforeEach(() => { + vi.spyOn(URL, 'createObjectURL').mockReturnValue(mockObjectUrl); + vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => { }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should create object URL from streamed image', async () => { + const buffer = new Uint8Array([1, 2, 3]).buffer; + const stream = new MockDotNetStreamReference(buffer) as any; + + const result = await CropperBlazor.Helpers.CropperUrlImageHelper.getImageUsingStreaming(stream); + + expect(result).toBe(mockObjectUrl); + expect(URL.createObjectURL).toHaveBeenCalledOnce(); + expect(URL.createObjectURL).toHaveBeenCalledWith( + expect.any(Blob) + ); + }); + + it('should revoke object URL', () => { + CropperBlazor.Helpers.CropperUrlImageHelper.revokeObjectUrl(mockObjectUrl); + + expect(URL.revokeObjectURL).toHaveBeenCalledOnce(); + expect(URL.revokeObjectURL).toHaveBeenCalledWith(mockObjectUrl); + }); +}); diff --git a/src/Cropper.Blazor/Cropper.Blazor/package.json b/src/Cropper.Blazor/Cropper.Blazor/package.json index 1a59e250..a5dff924 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/package.json +++ b/src/Cropper.Blazor/Cropper.Blazor/package.json @@ -6,7 +6,9 @@ "scripts": { "build:debug": "webpack --mode=development", "build:production": "webpack --mode=production", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "vitest", + "test-ui": "vitest --ui --coverage.enabled=true", + "coverage": "vitest run --coverage" }, "author": "", "license": "MIT", @@ -17,7 +19,10 @@ "terser-webpack-plugin": "5.3.14", "typescript": "5.9.3", "webpack": "5.103.0", - "webpack-cli": "6.0.1" + "webpack-cli": "6.0.1", + "vitest": "4.0.16", + "@vitest/coverage-v8": "4.0.16", + "@vitest/ui": "4.0.16" }, "dependencies": { "cropperjs": "1.6.2" diff --git a/src/Cropper.Blazor/Cropper.Blazor/vitest.setup.ts b/src/Cropper.Blazor/Cropper.Blazor/vitest.setup.ts new file mode 100644 index 00000000..26b9d285 --- /dev/null +++ b/src/Cropper.Blazor/Cropper.Blazor/vitest.setup.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'jsdom', + include: ['**/*.{test,spec}.{ts,tsx,js,jsx}'], + isolate: true, + coverage: { + provider: 'v8', + enabled: true, + reportsDirectory: './coverage', + reporter: ['text', 'lcov', 'cobertura'], + exclude: [ + '**/*.d.ts', + '**/*.spec.*', + '**/*.test.*', + 'node_modules/**', + 'dist/**' + ] + } + } +}); diff --git a/src/Cropper.Blazor/Cropper.Blazor/webpack.config.js b/src/Cropper.Blazor/Cropper.Blazor/webpack.config.js index 468e6846..5501e1db 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/webpack.config.js +++ b/src/Cropper.Blazor/Cropper.Blazor/webpack.config.js @@ -15,7 +15,13 @@ module.exports = (env, args) => ({ module: { rules: [{ test: /\.ts?$/, - loader: 'ts-loader' + loader: 'ts-loader', + exclude: [ + /node_modules/, + /\.test\.ts$/, // exclude test files + /\.spec\.ts$/, // exclude spec files + /vitest\.setup\.ts$/ // exclude Vitest setup + ] }] }, entry: { From 5a924c629180266f3662789ad61722a9807f199b Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Tue, 30 Dec 2025 18:41:09 +0200 Subject: [PATCH 16/58] add creation cobertura file for vitest coverage report --- .../Cropper.Blazor/Cropper.Blazor.csproj | 10 ++-------- .../Cropper/cropperJsInterop.test.ts | 1 - .../Cropper.Blazor/Cropper/cropperJsInterop.ts | 8 ++++---- .../Cropper/helpers/cropper-url-image-helper.ts | 2 +- ...r.d.ts => cropped-canvas-receiver.custom.d.ts} | 0 ...se.d.ts => cropper-component-base.custom.d.ts} | 0 ...e-receiver.d.ts => image-receiver.custom.d.ts} | 0 .../Cropper/types/data/crop-event-data.ts | 2 +- .../Cropper/types/data/zoom-event-data.ts | 2 +- ...tnet-global.d.ts => dotnet-global.custom.d.ts} | 0 src/Cropper.Blazor/Cropper.Blazor/package.json | 15 ++++++++------- src/Cropper.Blazor/Cropper.Blazor/tsconfig.json | 10 ++++++---- .../{vitest.setup.ts => vitest.config.ts} | 8 ++++---- 13 files changed, 27 insertions(+), 31 deletions(-) rename src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/{cropped-canvas-receiver.d.ts => cropped-canvas-receiver.custom.d.ts} (100%) rename src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/{cropper-component-base.d.ts => cropper-component-base.custom.d.ts} (100%) rename src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/{image-receiver.d.ts => image-receiver.custom.d.ts} (100%) rename src/Cropper.Blazor/Cropper.Blazor/Cropper/types/global/{dotnet-global.d.ts => dotnet-global.custom.d.ts} (100%) rename src/Cropper.Blazor/Cropper.Blazor/{vitest.setup.ts => vitest.config.ts} (76%) diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj index 9a03b202..5f82a66e 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj @@ -157,6 +157,8 @@ + @@ -181,14 +183,6 @@ - - - - - - - - diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.test.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.test.ts index c99620da..9c8b4b8a 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.test.ts +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.test.ts @@ -1,6 +1,5 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { CropperDecorator } from './cropperJsInterop'; -import { CropperBlazor } from './helpers/cropper-url-image-helper'; describe('CropperDecorator', () => { diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts index d3ee3803..41aad599 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts @@ -1,10 +1,10 @@ import Cropper from 'cropperjs'; -import type { CropperBlazor as CropperComponentBaseTypes } from './types/components/cropper-component-base'; -import type { CropperBlazor as CroppedCanvasReceiverTypes } from './types/components/cropped-canvas-receiver'; -import type { CropperBlazor as ImageReceiverTypes } from './types/components/image-receiver'; +import type { CropperBlazor as CropperComponentBaseTypes } from './types/components/cropper-component-base.custom'; +import type { CropperBlazor as CroppedCanvasReceiverTypes } from './types/components/cropped-canvas-receiver.custom'; +import type { CropperBlazor as ImageReceiverTypes } from './types/components/image-receiver.custom'; import type { CropperBlazor as DataEventTypes } from './types/data/cropper-event-data'; import type { CropperBlazor as DataOptionsTypes } from './types/data/cropper-extended-options'; -import type { CropperBlazor as DotNetTypes } from './types/global/dotnet-global'; +import type { CropperBlazor as DotNetTypes } from './types/global/dotnet-global.custom'; import { CropperBlazor } from './helpers/cropper-url-image-helper'; diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/cropper-url-image-helper.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/cropper-url-image-helper.ts index 3a902715..1bafde2f 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/cropper-url-image-helper.ts +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/cropper-url-image-helper.ts @@ -1,4 +1,4 @@ -import type { CropperBlazor as DotNetTypes } from '../types/global/dotnet-global'; +import type { CropperBlazor as DotNetTypes } from '../types/global/dotnet-global.custom'; export namespace CropperBlazor.Helpers { export class CropperUrlImageHelper { diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/cropped-canvas-receiver.d.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/cropped-canvas-receiver.custom.d.ts similarity index 100% rename from src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/cropped-canvas-receiver.d.ts rename to src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/cropped-canvas-receiver.custom.d.ts diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/cropper-component-base.d.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/cropper-component-base.custom.d.ts similarity index 100% rename from src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/cropper-component-base.d.ts rename to src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/cropper-component-base.custom.d.ts diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/image-receiver.d.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/image-receiver.custom.d.ts similarity index 100% rename from src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/image-receiver.d.ts rename to src/Cropper.Blazor/Cropper.Blazor/Cropper/types/components/image-receiver.custom.d.ts diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/data/crop-event-data.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/data/crop-event-data.ts index 31c6519e..96587e80 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/data/crop-event-data.ts +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/data/crop-event-data.ts @@ -1,4 +1,4 @@ -import type { CropperBlazor as DotNetTypes } from '../../types/global/dotnet-global'; +import type { CropperBlazor as DotNetTypes } from '../../types/global/dotnet-global.custom'; export namespace CropperBlazor.Data { export type CropEventDataJS = { diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/data/zoom-event-data.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/data/zoom-event-data.ts index 28f9b2a8..f5b8bea7 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/data/zoom-event-data.ts +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/data/zoom-event-data.ts @@ -1,4 +1,4 @@ -import type { CropperBlazor as DotNetTypes } from '../../types/global/dotnet-global'; +import type { CropperBlazor as DotNetTypes } from '../../types/global/dotnet-global.custom'; export namespace CropperBlazor.Data { export type ZoomEventDataJS = { diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/global/dotnet-global.d.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/types/global/dotnet-global.custom.d.ts similarity index 100% rename from src/Cropper.Blazor/Cropper.Blazor/Cropper/types/global/dotnet-global.d.ts rename to src/Cropper.Blazor/Cropper.Blazor/Cropper/types/global/dotnet-global.custom.d.ts diff --git a/src/Cropper.Blazor/Cropper.Blazor/package.json b/src/Cropper.Blazor/Cropper.Blazor/package.json index a5dff924..0db52e9d 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/package.json +++ b/src/Cropper.Blazor/Cropper.Blazor/package.json @@ -6,25 +6,26 @@ "scripts": { "build:debug": "webpack --mode=development", "build:production": "webpack --mode=production", - "test": "vitest", + "test": "vitest run", + "test-watch": "vitest", "test-ui": "vitest --ui --coverage.enabled=true", "coverage": "vitest run --coverage" }, "author": "", "license": "MIT", "devDependencies": { - "ts-loader": "9.5.4", + "@vitest/coverage-istanbul": "^4.0.16", + "@vitest/ui": "4.0.16", "clean-css": "5.3.3", "copy-webpack-plugin": "13.0.1", "terser-webpack-plugin": "5.3.14", + "ts-loader": "9.5.4", "typescript": "5.9.3", + "vitest": "^4.0.16", "webpack": "5.103.0", - "webpack-cli": "6.0.1", - "vitest": "4.0.16", - "@vitest/coverage-v8": "4.0.16", - "@vitest/ui": "4.0.16" + "webpack-cli": "6.0.1" }, "dependencies": { "cropperjs": "1.6.2" } -} \ No newline at end of file +} diff --git a/src/Cropper.Blazor/Cropper.Blazor/tsconfig.json b/src/Cropper.Blazor/Cropper.Blazor/tsconfig.json index 31d64c52..b46090c9 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/tsconfig.json +++ b/src/Cropper.Blazor/Cropper.Blazor/tsconfig.json @@ -5,8 +5,8 @@ "removeComments": true, "sourceMap": true, "target": "es6", - "module": "commonjs", - "moduleResolution": "node", + "module": "nodenext", + "moduleResolution": "nodenext", "lib": [ "es2016", "dom" ], "strict": true, "alwaysStrict": true, @@ -14,7 +14,8 @@ "allowJs": false, "baseUrl": ".", "paths": { - "cropperjs/*": [ "node_modules/cropperjs/src/*" ] + "cropperjs/*": [ "node_modules/cropperjs/src/*" ], + "vitest/*": [ "node_modules/vitest/*" ] }, "declaration": false }, @@ -24,6 +25,7 @@ "include": [ "Cropper/types/global/*.d.ts", "Cropper/**/*.ts", - "Cropper/*.ts" + "Cropper/*.ts", + "vitest.config.ts" ] } diff --git a/src/Cropper.Blazor/Cropper.Blazor/vitest.setup.ts b/src/Cropper.Blazor/Cropper.Blazor/vitest.config.ts similarity index 76% rename from src/Cropper.Blazor/Cropper.Blazor/vitest.setup.ts rename to src/Cropper.Blazor/Cropper.Blazor/vitest.config.ts index 26b9d285..d8f8e260 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/vitest.setup.ts +++ b/src/Cropper.Blazor/Cropper.Blazor/vitest.config.ts @@ -3,14 +3,14 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { globals: true, - environment: 'jsdom', + environment: 'node', include: ['**/*.{test,spec}.{ts,tsx,js,jsx}'], isolate: true, coverage: { - provider: 'v8', - enabled: true, + provider: 'istanbul', + enabled: false, reportsDirectory: './coverage', - reporter: ['text', 'lcov', 'cobertura'], + reporter: ['text', 'html', 'cobertura'], exclude: [ '**/*.d.ts', '**/*.spec.*', From 034d3ecaf80c2021111ebf0fa0e420a9e377acd9 Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Tue, 30 Dec 2025 19:13:40 +0200 Subject: [PATCH 17/58] Enable JS tests and set permissions in CI workflows --- .github/workflows/cd.yml | 4 ++++ .github/workflows/ci.yml | 4 ++++ .github/workflows/release.yml | 1 + 3 files changed, 9 insertions(+) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index c4fcf8fd..79132321 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -20,6 +20,10 @@ jobs: uses: CropperBlazor/Cropper.Blazor/.github/workflows/build-test-template.yml@dev secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + permissions: + contents: read + with: + run-js-tests: true deploy-to-github-pages: runs-on: ubuntu-latest diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aace4b6d..3d1d6e7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,10 @@ jobs: uses: CropperBlazor/Cropper.Blazor/.github/workflows/build-test-template.yml@dev secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + permissions: + contents: read + with: + run-js-tests: true code-linting: permissions: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 10e2d2fa..cabadca3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,6 +17,7 @@ jobs: with: configuration: "Release" publish-coverage: true + run-js-tests: true deploy-to-nuget: name: Deploy to NuGet From 6a0196958f5c18120b4cc98d32871c372654ed49 Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Tue, 30 Dec 2025 19:42:22 +0200 Subject: [PATCH 18/58] test --- .gitignore | 1 + .../ServiceCollectionExtensions_Should.cs | 48 +++++++++---------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index 28ffa305..391f649b 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ _ReSharper*/ [Tt]est[Rr]esult* .vs/ .idea/ +*.cobertura.xml #Nuget packages folder packages/ # Node.js diff --git a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Extensions/ServiceCollectionExtensions_Should.cs b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Extensions/ServiceCollectionExtensions_Should.cs index 2815e7ec..97d619b8 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Extensions/ServiceCollectionExtensions_Should.cs +++ b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Extensions/ServiceCollectionExtensions_Should.cs @@ -11,34 +11,34 @@ namespace Cropper.Blazor.UnitTests.Extensions { public class ServiceCollectionExtensions_Should { - private ServiceCollectionMock ServiceCollectionMock = null!; - private readonly Mock ServiceCollection = new(); + //private ServiceCollectionMock ServiceCollectionMock = null!; + //private readonly Mock ServiceCollection = new(); - [Theory, MemberData(nameof(TestData_AddCropper_Service))] - public void Verify_Cropper_Service_Is_Registered(CropperJsInteropOptions? cropperJsInteropOptions) - { - // act - ServiceCollection.Object.AddCropper(cropperJsInteropOptions); + //[Theory, MemberData(nameof(TestData_AddCropper_Service))] + //public void Verify_Cropper_Service_Is_Registered(CropperJsInteropOptions? cropperJsInteropOptions) + //{ + // // act + // ServiceCollection.Object.AddCropper(cropperJsInteropOptions); - // assert - ServiceCollectionMock = new(ServiceCollection); - ServiceCollectionMock.ContainsSingletonService(); - ServiceCollectionMock.TryContainsScopedService(); - ServiceCollectionMock.TryContainsScopedService(); - } + // // assert + // ServiceCollectionMock = new(ServiceCollection); + // ServiceCollectionMock.ContainsSingletonService(); + // ServiceCollectionMock.TryContainsScopedService(); + // ServiceCollectionMock.TryContainsScopedService(); + //} - public static IEnumerable TestData_AddCropper_Service() - { - yield return WrapArgs(null); + //public static IEnumerable TestData_AddCropper_Service() + //{ + // yield return WrapArgs(null); - yield return WrapArgs(new CropperJsInteropOptions()); + // yield return WrapArgs(new CropperJsInteropOptions()); - static object[] WrapArgs( - CropperJsInteropOptions? cropperJsInteropOptions) - => new object[] - { - cropperJsInteropOptions! - }; - } + // static object[] WrapArgs( + // CropperJsInteropOptions? cropperJsInteropOptions) + // => new object[] + // { + // cropperJsInteropOptions! + // }; + //} } } From f3cc460746d10a0a2d89a9ada3a52a7c59dd87fb Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Tue, 30 Dec 2025 19:54:19 +0200 Subject: [PATCH 19/58] revert unit test --- .../ServiceCollectionExtensions_Should.cs | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Extensions/ServiceCollectionExtensions_Should.cs b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Extensions/ServiceCollectionExtensions_Should.cs index 97d619b8..2815e7ec 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Extensions/ServiceCollectionExtensions_Should.cs +++ b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Extensions/ServiceCollectionExtensions_Should.cs @@ -11,34 +11,34 @@ namespace Cropper.Blazor.UnitTests.Extensions { public class ServiceCollectionExtensions_Should { - //private ServiceCollectionMock ServiceCollectionMock = null!; - //private readonly Mock ServiceCollection = new(); + private ServiceCollectionMock ServiceCollectionMock = null!; + private readonly Mock ServiceCollection = new(); - //[Theory, MemberData(nameof(TestData_AddCropper_Service))] - //public void Verify_Cropper_Service_Is_Registered(CropperJsInteropOptions? cropperJsInteropOptions) - //{ - // // act - // ServiceCollection.Object.AddCropper(cropperJsInteropOptions); + [Theory, MemberData(nameof(TestData_AddCropper_Service))] + public void Verify_Cropper_Service_Is_Registered(CropperJsInteropOptions? cropperJsInteropOptions) + { + // act + ServiceCollection.Object.AddCropper(cropperJsInteropOptions); - // // assert - // ServiceCollectionMock = new(ServiceCollection); - // ServiceCollectionMock.ContainsSingletonService(); - // ServiceCollectionMock.TryContainsScopedService(); - // ServiceCollectionMock.TryContainsScopedService(); - //} + // assert + ServiceCollectionMock = new(ServiceCollection); + ServiceCollectionMock.ContainsSingletonService(); + ServiceCollectionMock.TryContainsScopedService(); + ServiceCollectionMock.TryContainsScopedService(); + } - //public static IEnumerable TestData_AddCropper_Service() - //{ - // yield return WrapArgs(null); + public static IEnumerable TestData_AddCropper_Service() + { + yield return WrapArgs(null); - // yield return WrapArgs(new CropperJsInteropOptions()); + yield return WrapArgs(new CropperJsInteropOptions()); - // static object[] WrapArgs( - // CropperJsInteropOptions? cropperJsInteropOptions) - // => new object[] - // { - // cropperJsInteropOptions! - // }; - //} + static object[] WrapArgs( + CropperJsInteropOptions? cropperJsInteropOptions) + => new object[] + { + cropperJsInteropOptions! + }; + } } } From 22667cb9311e43ab248e44cd38ab1d31f49cc4c0 Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Tue, 30 Dec 2025 19:58:16 +0200 Subject: [PATCH 20/58] test --- .../Extensions/DataUrlDecoder_Should.cs | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Extensions/DataUrlDecoder_Should.cs b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Extensions/DataUrlDecoder_Should.cs index 1c9d23dc..0be5e492 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Extensions/DataUrlDecoder_Should.cs +++ b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Extensions/DataUrlDecoder_Should.cs @@ -8,34 +8,34 @@ namespace Cropper.Blazor.UnitTests.Extensions { public class DataUrlDecoder_Should { - [Theory, MemberData(nameof(TestData_Decode))] - public void DecodeDataUrlIntoByteArrayAndMediaType( - string dataUrl, - string expectedBase64ImageData, - string expectedMediaType) - { - // act - var (imageData, mediaType) = dataUrl.Decode(); + //[Theory, MemberData(nameof(TestData_Decode))] + //public void DecodeDataUrlIntoByteArrayAndMediaType( + // string dataUrl, + // string expectedBase64ImageData, + // string expectedMediaType) + //{ + // // act + // var (imageData, mediaType) = dataUrl.Decode(); - // assert - imageData.Should().BeEquivalentTo(expectedBase64ImageData); - mediaType.Should().BeEquivalentTo(expectedMediaType); - } + // // assert + // imageData.Should().BeEquivalentTo(expectedBase64ImageData); + // mediaType.Should().BeEquivalentTo(expectedMediaType); + //} - [Theory, MemberData(nameof(TestData_Throw_ArgumentException_When_Decode))] - public void Throw_ArgumentException_When_Decode( - string dataUrl, - string expectedMessage) - { - // arrange - Action act = () => dataUrl.Decode(); + //[Theory, MemberData(nameof(TestData_Throw_ArgumentException_When_Decode))] + //public void Throw_ArgumentException_When_Decode( + // string dataUrl, + // string expectedMessage) + //{ + // // arrange + // Action act = () => dataUrl.Decode(); - // act & assert - act - .Should() - .Throw() - .WithMessage(expectedMessage); - } + // // act & assert + // act + // .Should() + // .Throw() + // .WithMessage(expectedMessage); + //} public static IEnumerable TestData_Decode() { From 6afda9c60721d732be38ad2336f7c87a5140dfcf Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim <50423072+MaxymGorn@users.noreply.github.com> Date: Wed, 31 Dec 2025 02:16:47 +0200 Subject: [PATCH 21/58] Remove 'contents: read' permission from CI workflow Removed unnecessary permissions for code quality check. --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6de2abe2..81ada8c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,8 +27,6 @@ jobs: uses: CropperBlazor/Cropper.Blazor/.github/workflows/build-test-template.yml@dev secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - permissions: - contents: read with: run-js-tests: true From 3437b11e6a5f4eb6f6973469b5c3bc3eeef327c3 Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim <50423072+MaxymGorn@users.noreply.github.com> Date: Wed, 31 Dec 2025 02:17:21 +0200 Subject: [PATCH 22/58] Remove contents read permission in cd.yml Removed permissions for contents in the CI workflow. --- .github/workflows/cd.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index d5566eea..3eccca07 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -20,8 +20,6 @@ jobs: uses: CropperBlazor/Cropper.Blazor/.github/workflows/build-test-template.yml@dev secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - permissions: - contents: read with: run-js-tests: true From 23a7846ab2ae77f918180f8072cb4baaf26e5b82 Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim <50423072+MaxymGorn@users.noreply.github.com> Date: Wed, 31 Dec 2025 02:20:15 +0200 Subject: [PATCH 23/58] Downgrade Super-Linter version from v8.3.0 to v8.2.1 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9096cfb3..ab145575 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: persist-credentials: false - name: Super-Linter - uses: super-linter/super-linter@v8.3.0 + uses: super-linter/super-linter@v8.2.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 0b93324fad19e146e650ed5441f5919524a4e5a3 Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Wed, 31 Dec 2025 13:18:54 +0200 Subject: [PATCH 24/58] revert test --- .../Extensions/DataUrlDecoder_Should.cs | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Extensions/DataUrlDecoder_Should.cs b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Extensions/DataUrlDecoder_Should.cs index 0be5e492..1c9d23dc 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Extensions/DataUrlDecoder_Should.cs +++ b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Extensions/DataUrlDecoder_Should.cs @@ -8,34 +8,34 @@ namespace Cropper.Blazor.UnitTests.Extensions { public class DataUrlDecoder_Should { - //[Theory, MemberData(nameof(TestData_Decode))] - //public void DecodeDataUrlIntoByteArrayAndMediaType( - // string dataUrl, - // string expectedBase64ImageData, - // string expectedMediaType) - //{ - // // act - // var (imageData, mediaType) = dataUrl.Decode(); + [Theory, MemberData(nameof(TestData_Decode))] + public void DecodeDataUrlIntoByteArrayAndMediaType( + string dataUrl, + string expectedBase64ImageData, + string expectedMediaType) + { + // act + var (imageData, mediaType) = dataUrl.Decode(); - // // assert - // imageData.Should().BeEquivalentTo(expectedBase64ImageData); - // mediaType.Should().BeEquivalentTo(expectedMediaType); - //} + // assert + imageData.Should().BeEquivalentTo(expectedBase64ImageData); + mediaType.Should().BeEquivalentTo(expectedMediaType); + } - //[Theory, MemberData(nameof(TestData_Throw_ArgumentException_When_Decode))] - //public void Throw_ArgumentException_When_Decode( - // string dataUrl, - // string expectedMessage) - //{ - // // arrange - // Action act = () => dataUrl.Decode(); + [Theory, MemberData(nameof(TestData_Throw_ArgumentException_When_Decode))] + public void Throw_ArgumentException_When_Decode( + string dataUrl, + string expectedMessage) + { + // arrange + Action act = () => dataUrl.Decode(); - // // act & assert - // act - // .Should() - // .Throw() - // .WithMessage(expectedMessage); - //} + // act & assert + act + .Should() + .Throw() + .WithMessage(expectedMessage); + } public static IEnumerable TestData_Decode() { From 2d18cceacef15fc62f8b372163279e34ee89c1b2 Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim <50423072+MaxymGorn@users.noreply.github.com> Date: Wed, 31 Dec 2025 14:05:34 +0200 Subject: [PATCH 25/58] Update build-test-template.yml --- .github/workflows/build-test-template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test-template.yml b/.github/workflows/build-test-template.yml index 2434a0d7..178f1724 100644 --- a/.github/workflows/build-test-template.yml +++ b/.github/workflows/build-test-template.yml @@ -106,7 +106,7 @@ jobs: uses: codecov/codecov-action@v5.5.2 with: token: ${{ secrets.CODECOV_TOKEN }} - files: coverage.net6.0.cobertura.xml, coverage.net7.0.cobertura.xml, coverage.net8.0.cobertura.xml, coverage.net9.0.cobertura.xml, coverage.net10.0.cobertura.xml + files: coverage.net6.0.cobertura.xml,coverage.net7.0.cobertura.xml,coverage.net8.0.cobertura.xml,coverage.net9.0.cobertura.xml,coverage.net10.0.cobertura.xml flags: DotNet fail_ci_if_error: true disable_search: true From d7f2beeebe6c515b53a3debb205015d3b96c6bbf Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim <50423072+MaxymGorn@users.noreply.github.com> Date: Wed, 31 Dec 2025 14:11:39 +0200 Subject: [PATCH 26/58] Update build-test-template.yml --- .github/workflows/build-test-template.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-test-template.yml b/.github/workflows/build-test-template.yml index 178f1724..6a94dd7e 100644 --- a/.github/workflows/build-test-template.yml +++ b/.github/workflows/build-test-template.yml @@ -106,11 +106,11 @@ jobs: uses: codecov/codecov-action@v5.5.2 with: token: ${{ secrets.CODECOV_TOKEN }} - files: coverage.net6.0.cobertura.xml,coverage.net7.0.cobertura.xml,coverage.net8.0.cobertura.xml,coverage.net9.0.cobertura.xml,coverage.net10.0.cobertura.xml + files: ./coverage.net6.0.cobertura.xml,./coverage.net7.0.cobertura.xml,./coverage.net8.0.cobertura.xml,./coverage.net9.0.cobertura.xml,./coverage.net10.0.cobertura.xml flags: DotNet fail_ci_if_error: true disable_search: true - directory: src/Cropper.Blazor + directory: ./src/Cropper.Blazor/Cropper.Blazor.UnitTests/ verbose: true - name: Upload TypeScript coverage to Codecov @@ -118,9 +118,9 @@ jobs: uses: codecov/codecov-action@v5.5.2 with: token: ${{ secrets.CODECOV_TOKEN }} - files: cobertura-coverage.xml + files: ./cobertura-coverage.xml flags: TypeScript fail_ci_if_error: true disable_search: true - directory: src/Cropper.Blazor + directory: ./src/Cropper.Blazor/Cropper.Blazor verbose: true From 8ea7c9f737095c8f6f285ca911d92cab39ee96fd Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim <50423072+MaxymGorn@users.noreply.github.com> Date: Wed, 31 Dec 2025 14:20:01 +0200 Subject: [PATCH 27/58] Update build-test-template.yml --- .github/workflows/build-test-template.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build-test-template.yml b/.github/workflows/build-test-template.yml index 6a94dd7e..f73b5061 100644 --- a/.github/workflows/build-test-template.yml +++ b/.github/workflows/build-test-template.yml @@ -106,11 +106,10 @@ jobs: uses: codecov/codecov-action@v5.5.2 with: token: ${{ secrets.CODECOV_TOKEN }} - files: ./coverage.net6.0.cobertura.xml,./coverage.net7.0.cobertura.xml,./coverage.net8.0.cobertura.xml,./coverage.net9.0.cobertura.xml,./coverage.net10.0.cobertura.xml + files: src/Cropper.Blazor/Cropper.Blazor.UnitTests/coverage.net6.0.cobertura.xml,src/Cropper.Blazor/Cropper.Blazor.UnitTests/coverage.net7.0.cobertura.xml,src/Cropper.Blazor/Cropper.Blazor.UnitTests/coverage.net8.0.cobertura.xml,src/Cropper.Blazor/Cropper.Blazor.UnitTests/coverage.net9.0.cobertura.xml,src/Cropper.Blazor/Cropper.Blazor.UnitTests/coverage.net10.0.cobertura.xml flags: DotNet fail_ci_if_error: true disable_search: true - directory: ./src/Cropper.Blazor/Cropper.Blazor.UnitTests/ verbose: true - name: Upload TypeScript coverage to Codecov From 87ded168e92788fa1810d0f1decf0541ec437303 Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim <50423072+MaxymGorn@users.noreply.github.com> Date: Wed, 31 Dec 2025 14:24:26 +0200 Subject: [PATCH 28/58] Update build-test-template.yml --- .github/workflows/build-test-template.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build-test-template.yml b/.github/workflows/build-test-template.yml index f73b5061..9d773bda 100644 --- a/.github/workflows/build-test-template.yml +++ b/.github/workflows/build-test-template.yml @@ -117,9 +117,8 @@ jobs: uses: codecov/codecov-action@v5.5.2 with: token: ${{ secrets.CODECOV_TOKEN }} - files: ./cobertura-coverage.xml + files: src/Cropper.Blazor/Cropper.Blazor/coverage/cobertura-coverage.xml flags: TypeScript fail_ci_if_error: true disable_search: true - directory: ./src/Cropper.Blazor/Cropper.Blazor verbose: true From 2c75b5715a44dbfdd1875564e1813bc0294e8585 Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Wed, 31 Dec 2025 17:17:23 +0200 Subject: [PATCH 29/58] add eslint --- .github/workflows/ci.yml | 5 + src/Cropper.Blazor/Cropper.Blazor/.prettierrc | 8 + .../Cropper.Blazor/Cropper.Blazor.csproj | 2 + .../Cropper/cropperJsInterop.test.ts | 11 +- .../Cropper/cropperJsInterop.ts | 858 +++++++++--------- .../helpers/cropper-url-image-helper.test.ts | 58 +- .../helpers/cropper-url-image-helper.ts | 22 +- .../cropped-canvas-receiver.custom.d.ts | 12 +- .../cropper-component-base.custom.d.ts | 32 +- .../components/image-receiver.custom.d.ts | 20 +- .../Cropper/types/data/crop-event-data.ts | 12 +- .../Cropper/types/data/cropper-event-data.ts | 38 +- .../types/data/cropper-extended-options.ts | 12 +- .../Cropper/types/data/zoom-event-data.ts | 14 +- .../types/global/dotnet-global.custom.d.ts | 30 +- .../Cropper.Blazor/eslint.config.cjs | 36 + .../Cropper.Blazor/package.json | 14 +- 17 files changed, 634 insertions(+), 550 deletions(-) create mode 100644 src/Cropper.Blazor/Cropper.Blazor/.prettierrc create mode 100644 src/Cropper.Blazor/Cropper.Blazor/eslint.config.cjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab145575..086f9274 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,6 +42,10 @@ jobs: fetch-depth: 0 persist-credentials: false + - name: Run ESLint + run: npm run lint + working-directory: src/Cropper.Blazor/Cropper.Blazor + - name: Super-Linter uses: super-linter/super-linter@v8.2.1 env: @@ -66,6 +70,7 @@ jobs: VALIDATE_YAML_PRETTIER: false VALIDATE_JSON_PRETTIER: false VALIDATE_TYPESCRIPT_PRETTIER: false + VALIDATE_TYPESCRIPT_ES: false FILTER_REGEX_EXCLUDE: '(\W|^)(obj/|bin/|node_modules/)|(\W|^)(.*([.]min[.]css))($)|(\W|^)(.*([.]min[.]js))($)' FILTER_REGEX_INCLUDE: "/github/workspace/src/Cropper.Blazor/.*|/github/workspace/.github/.*" diff --git a/src/Cropper.Blazor/Cropper.Blazor/.prettierrc b/src/Cropper.Blazor/Cropper.Blazor/.prettierrc new file mode 100644 index 00000000..365e7396 --- /dev/null +++ b/src/Cropper.Blazor/Cropper.Blazor/.prettierrc @@ -0,0 +1,8 @@ +{ + "singleQuote": false, + "semi": true, + "trailingComma": "all", + "printWidth": 100, + "bracketSpacing": true, + "arrowParens": "always" +} diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj index 5f82a66e..0d4c3f78 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj @@ -76,6 +76,7 @@ + @@ -90,6 +91,7 @@ + - + @@ -159,8 +157,7 @@ - + From be048c09f1021183c0d65f80650406734f9a45ae Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Wed, 31 Dec 2025 18:00:28 +0200 Subject: [PATCH 36/58] fix: Wrong indentation type(spaces instead of tabs) --- src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj index 19d49c0e..0cafbb03 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj @@ -99,8 +99,7 @@ This target runs ONLY during the cross-targeting build (IsCrossTargetingBuild) (i.e., once for the entire multi-TFM build). --> - - + From 9efd8b5571a69a642ee34bcd067ed50e4910135a Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Wed, 31 Dec 2025 18:13:00 +0200 Subject: [PATCH 37/58] move logic to blob helper --- .../Cropper/cropperJsInterop.ts | 106 +--------------- .../Cropper/helpers/blob-helper.ts | 117 ++++++++++++++++++ 2 files changed, 123 insertions(+), 100 deletions(-) create mode 100644 src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/blob-helper.ts diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts index e6b26575..5ff8ca0d 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts @@ -5,6 +5,7 @@ import type { CropperBlazor as ImageReceiverTypes } from "./types/components/ima import type { CropperBlazor as DataEventTypes } from "./types/data/cropper-event-data"; import type { CropperBlazor as DataOptionsTypes } from "./types/data/cropper-extended-options"; import type { CropperBlazor as DotNetTypes } from "./types/global/dotnet-global.custom"; +import { CropperBlazor as BlobHelper } from "./helpers/blob-helper"; import { CropperBlazor } from "./helpers/cropper-url-image-helper"; declare global { @@ -330,106 +331,11 @@ export class CropperDecorator { dotNetImageReceiverRef: DotNetTypes.Global.DotNetObjectReference, maximumReceiveChunkSize?: number, ) { - // Validate blob - if (!(blob instanceof Blob)) { - throw new TypeError("blob must be a valid Blob object."); - } - - // Validate dotNetImageReceiverRef - if (!dotNetImageReceiverRef || typeof dotNetImageReceiverRef.invokeMethodAsync !== "function") { - throw new TypeError( - "dotNetImageReceiverRef must be a valid .NET object reference with an invokeMethodAsync function.", - ); - } - - // Validate maximumReceiveChunkSize - if (maximumReceiveChunkSize != null && maximumReceiveChunkSize <= 0) { - throw new RangeError("maximumReceiveChunkSize must be greater than 0 bytes when specified."); - } - - // By default, blob.stream() reads the blob using internal chunking (typically 65536 bytes per chunk). - // To enforce a custom chunk size, especially to control serialized message size for JS interop or SignalR limits, we wrap it in a transformed ReadableStream. - // This allows us to split the default chunks further to stay within a maximum size constraint (e.g., for Blazor's JS interop or SignalR message limits). - let reader: ReadableStreamDefaultReader | null = null; - - if (maximumReceiveChunkSize == null) { - reader = blob.stream().getReader(); - } else { - const blobStream = blob.stream().getReader(); - - // Binary estimation of JSON size - const getJsonSizeBinary = (chunk) => { - const length = chunk.length; - - // Max 3 digits for the number (0 to 255) - const bytesPerElement = 3; - // Comma between elements - const commas = length - 1; - // For '[' and ']' - const brackets = 2; - - return length * bytesPerElement + commas + brackets; - }; - - // Create a custom stream that enforces max chunk size - const transformedStream = new ReadableStream({ - async pull(controller) { - const { done, value } = await blobStream.read(); - - if (done) { - controller.close(); - - return; - } - - // Function to calculate JSON size for the current chunk using binary estimation - let offset = 0; - let lastGoodChunkSize = maximumReceiveChunkSize; - - while (offset < value.length) { - // Start with the last known good chunk size, or the remaining length - let chunkSize = Math.min(lastGoodChunkSize, value.length - offset); - let chunk = value.slice(offset, offset + chunkSize); - let jsonSize = getJsonSizeBinary(chunk); - - // If the JSON size is too large, reduce the chunk size gradually - while (jsonSize > maximumReceiveChunkSize && chunkSize > 1) { - // Reduce the chunk size in steps of 512 bytes, but not below 1 byte - chunkSize = Math.max(chunkSize - 512, 1); - chunk = value.slice(offset, offset + chunkSize); - jsonSize = getJsonSizeBinary(chunk); - - // Stop reducing if the chunk size is already very small - if (chunkSize <= 512) { - break; - } - } - - // Move the offset forward by the size of the chunk just sent with update the last good chunk size - lastGoodChunkSize = chunkSize; - - offset += chunkSize; - - controller.enqueue(chunk); - } - }, - }); - - reader = transformedStream.getReader(); - } - - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - await dotNetImageReceiverRef.invokeMethodAsync("ReceiveImageChunk", value); - } - - await dotNetImageReceiverRef.invokeMethodAsync("CompleteImageTransfer"); - } catch (error) { - await dotNetImageReceiverRef.invokeMethodAsync("HandleImageProcessingError", String(error)); - } + await BlobHelper.Helpers.readBlobInChunks( + blob, + dotNetImageReceiverRef, + maximumReceiveChunkSize, + ); } sendImageInChunks( diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/blob-helper.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/blob-helper.ts new file mode 100644 index 00000000..8740d747 --- /dev/null +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/blob-helper.ts @@ -0,0 +1,117 @@ +import type { CropperBlazor as ImageReceiverTypes } from "../types/components/image-receiver.custom"; +import type { CropperBlazor as DotNetTypes } from "../types/global/dotnet-global.custom"; + +// -------------------------- +// Chunked Blob Streaming +// Provides a utility to read a Blob in chunks and stream it to a .NET receiver +// via JS interop. Supports optional maximum chunk size to respect message limits. +// Useful for Blazor interop, SignalR, or other large file transfers. +// -------------------------- +export namespace CropperBlazor.Helpers { + export async function readBlobInChunks( + blob: Blob | null, + dotNetImageReceiverRef: DotNetTypes.Global.DotNetObjectReference, + maximumReceiveChunkSize?: number, + ) { + // Validate blob + if (!(blob instanceof Blob)) { + throw new TypeError("blob must be a valid Blob object."); + } + + // Validate dotNetImageReceiverRef + if (!dotNetImageReceiverRef || typeof dotNetImageReceiverRef.invokeMethodAsync !== "function") { + throw new TypeError( + "dotNetImageReceiverRef must be a valid .NET object reference with an invokeMethodAsync function.", + ); + } + + // Validate maximumReceiveChunkSize + if (maximumReceiveChunkSize != null && maximumReceiveChunkSize <= 0) { + throw new RangeError("maximumReceiveChunkSize must be greater than 0 bytes when specified."); + } + + // By default, blob.stream() reads the blob using internal chunking (typically 65536 bytes per chunk). + // To enforce a custom chunk size, especially to control serialized message size for JS interop or SignalR limits, we wrap it in a transformed ReadableStream. + // This allows us to split the default chunks further to stay within a maximum size constraint (e.g., for Blazor's JS interop or SignalR message limits). + let reader: ReadableStreamDefaultReader | null = null; + + if (maximumReceiveChunkSize == null) { + reader = blob.stream().getReader(); + } else { + const blobStream = blob.stream().getReader(); + + // Binary estimation of JSON size + const getJsonSizeBinary = (chunk) => { + const length = chunk.length; + + // Max 3 digits for the number (0 to 255) + const bytesPerElement = 3; + // Comma between elements + const commas = length - 1; + // For '[' and ']' + const brackets = 2; + + return length * bytesPerElement + commas + brackets; + }; + + // Create a custom stream that enforces max chunk size + const transformedStream = new ReadableStream({ + async pull(controller) { + const { done, value } = await blobStream.read(); + + if (done) { + controller.close(); + + return; + } + + // Function to calculate JSON size for the current chunk using binary estimation + let offset = 0; + let lastGoodChunkSize = maximumReceiveChunkSize; + + while (offset < value.length) { + // Start with the last known good chunk size, or the remaining length + let chunkSize = Math.min(lastGoodChunkSize, value.length - offset); + let chunk = value.slice(offset, offset + chunkSize); + let jsonSize = getJsonSizeBinary(chunk); + + // If the JSON size is too large, reduce the chunk size gradually + while (jsonSize > maximumReceiveChunkSize && chunkSize > 1) { + // Reduce the chunk size in steps of 512 bytes, but not below 1 byte + chunkSize = Math.max(chunkSize - 512, 1); + chunk = value.slice(offset, offset + chunkSize); + jsonSize = getJsonSizeBinary(chunk); + + // Stop reducing if the chunk size is already very small + if (chunkSize <= 512) { + break; + } + } + + // Move the offset forward by the size of the chunk just sent with update the last good chunk size + lastGoodChunkSize = chunkSize; + + offset += chunkSize; + + controller.enqueue(chunk); + } + }, + }); + + reader = transformedStream.getReader(); + } + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + await dotNetImageReceiverRef.invokeMethodAsync("ReceiveImageChunk", value); + } + + await dotNetImageReceiverRef.invokeMethodAsync("CompleteImageTransfer"); + } catch (error) { + await dotNetImageReceiverRef.invokeMethodAsync("HandleImageProcessingError", String(error)); + } + } +} From 0e7a4f946e3c30fc757046449fa9cfdcd7277c2e Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Wed, 31 Dec 2025 18:17:18 +0200 Subject: [PATCH 38/58] fix formatting --- .../Cropper.Blazor/Cropper.Blazor.csproj | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj index 0cafbb03..6382648a 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj @@ -95,9 +95,9 @@ @@ -111,10 +111,10 @@ From 66a66f5c28fcc2d115c018d3d97c81e8f687d7de Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim <50423072+MaxymGorn@users.noreply.github.com> Date: Wed, 31 Dec 2025 18:17:58 +0200 Subject: [PATCH 39/58] Update codecov.yml --- .github/codecov.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/codecov.yml b/.github/codecov.yml index 5b52d0e1..d096b019 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -23,7 +23,7 @@ coverage: typescript: flags: - TypeScript - target: 100% + target: 5% threshold: 0% patch: @@ -36,5 +36,5 @@ coverage: typescript: flags: - TypeScript - target: 100% + target: 5% threshold: 0% From edacfef915d9ac7dc2e9a72045cf7b5dd6e8ff86 Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Wed, 31 Dec 2025 18:42:42 +0200 Subject: [PATCH 40/58] fix error in helper --- .../Cropper.Blazor/Cropper/cropperJsInterop.ts | 6 +++--- .../Cropper/helpers/cropper-url-image-helper.test.ts | 6 ++++-- .../Cropper/helpers/cropper-url-image-helper.ts | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts index 5ff8ca0d..66282b52 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts @@ -6,12 +6,12 @@ import type { CropperBlazor as DataEventTypes } from "./types/data/cropper-event import type { CropperBlazor as DataOptionsTypes } from "./types/data/cropper-extended-options"; import type { CropperBlazor as DotNetTypes } from "./types/global/dotnet-global.custom"; import { CropperBlazor as BlobHelper } from "./helpers/blob-helper"; -import { CropperBlazor } from "./helpers/cropper-url-image-helper"; +import { CropperBlazor as UrlImageHelper } from "./helpers/cropper-url-image-helper"; declare global { interface Window { cropper: CropperDecorator; - cropperUrlImageHelper: CropperBlazor.Helpers.CropperUrlImageHelper; + cropperUrlImageHelper: UrlImageHelper.Helpers.CropperUrlImageHelper; } } @@ -365,5 +365,5 @@ export class CropperDecorator { if (typeof window !== "undefined") { window.cropper = new CropperDecorator(); - window.cropperUrlImageHelper = new CropperBlazor.Helpers.CropperUrlImageHelper(); + window.cropperUrlImageHelper = new UrlImageHelper.Helpers.CropperUrlImageHelper(); } diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/cropper-url-image-helper.test.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/cropper-url-image-helper.test.ts index c5c5bf00..2a4b1d59 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/cropper-url-image-helper.test.ts +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/cropper-url-image-helper.test.ts @@ -26,7 +26,9 @@ describe("CropperUrlImageHelper", () => { const buffer = new Uint8Array([1, 2, 3]).buffer; const stream = new MockDotNetStreamReference(buffer) as any; - const result = await CropperBlazor.Helpers.CropperUrlImageHelper.getImageUsingStreaming(stream); + const result = await new CropperBlazor.Helpers.CropperUrlImageHelper().getImageUsingStreaming( + stream, + ); expect(result).toBe(mockObjectUrl); expect(URL.createObjectURL).toHaveBeenCalledOnce(); @@ -34,7 +36,7 @@ describe("CropperUrlImageHelper", () => { }); it("should revoke object URL", () => { - CropperBlazor.Helpers.CropperUrlImageHelper.revokeObjectUrl(mockObjectUrl); + new CropperBlazor.Helpers.CropperUrlImageHelper().revokeObjectUrl(mockObjectUrl); expect(URL.revokeObjectURL).toHaveBeenCalledOnce(); expect(URL.revokeObjectURL).toHaveBeenCalledWith(mockObjectUrl); diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/cropper-url-image-helper.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/cropper-url-image-helper.ts index b92acf51..7f7ab76b 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/cropper-url-image-helper.ts +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/cropper-url-image-helper.ts @@ -2,7 +2,7 @@ export namespace CropperBlazor.Helpers { export class CropperUrlImageHelper { - static async getImageUsingStreaming( + async getImageUsingStreaming( imageStream: DotNetTypes.Global.DotNetStreamReference, ): Promise { const buf = await imageStream.arrayBuffer(); @@ -11,7 +11,7 @@ export namespace CropperBlazor.Helpers { return URL.createObjectURL(blob); } - static revokeObjectUrl(url: string) { + revokeObjectUrl(url: string) { URL.revokeObjectURL(url); } } From ae1123269117e7945deea3ed8b1adbb45c062b96 Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim <50423072+MaxymGorn@users.noreply.github.com> Date: Wed, 31 Dec 2025 18:45:19 +0200 Subject: [PATCH 41/58] Update codecov.yml --- .github/codecov.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/codecov.yml b/.github/codecov.yml index d096b019..00afda1f 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -7,12 +7,6 @@ comment: coverage: precision: 2 status: - project: - default: off - - patch: - default: off - project: dotnet: flags: From 95279f81cdae11bbe530f0d93c131080afaf0f92 Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim <50423072+MaxymGorn@users.noreply.github.com> Date: Wed, 31 Dec 2025 18:55:07 +0200 Subject: [PATCH 42/58] Update codecov.yml --- .github/codecov.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/codecov.yml b/.github/codecov.yml index 00afda1f..b68c55ed 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -17,7 +17,7 @@ coverage: typescript: flags: - TypeScript - target: 5% + target: 100% threshold: 0% patch: @@ -30,5 +30,5 @@ coverage: typescript: flags: - TypeScript - target: 5% + target: 100% threshold: 0% From be8f3d06e53acdd76616dbcb4bc27a3332698c4c Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim <50423072+MaxymGorn@users.noreply.github.com> Date: Wed, 31 Dec 2025 20:38:02 +0200 Subject: [PATCH 43/58] Update codecov.yml --- .github/codecov.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/codecov.yml b/.github/codecov.yml index b68c55ed..96e02eb1 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -8,6 +8,7 @@ coverage: precision: 2 status: project: + default: false dotnet: flags: - DotNet @@ -21,6 +22,7 @@ coverage: threshold: 0% patch: + default: false dotnet: flags: - DotNet From e6102ae30dd92c703a60d850e8d031635ba35b7b Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim <50423072+MaxymGorn@users.noreply.github.com> Date: Wed, 31 Dec 2025 20:42:09 +0200 Subject: [PATCH 44/58] Update codecov.yml --- .github/codecov.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/codecov.yml b/.github/codecov.yml index 96e02eb1..5ef15763 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -26,7 +26,7 @@ coverage: dotnet: flags: - DotNet - target: 100% + target: 5% threshold: 0% typescript: From 007be450b69c983d7d8c36d33b6bce8ce1615404 Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim <50423072+MaxymGorn@users.noreply.github.com> Date: Wed, 31 Dec 2025 20:48:12 +0200 Subject: [PATCH 45/58] Update codecov.yml --- .github/codecov.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/codecov.yml b/.github/codecov.yml index 5ef15763..17401763 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -26,11 +26,11 @@ coverage: dotnet: flags: - DotNet - target: 5% + target: 100% threshold: 0% typescript: flags: - TypeScript - target: 100% + target: 6% threshold: 0% From 0021ef314126a0cd0bdcbada3d23f4ba4305113b Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Fri, 2 Jan 2026 14:53:59 +0200 Subject: [PATCH 46/58] fix splitting webpack files to cropper and cropper js interop files;config ts --- src/Cropper.Blazor/Client/wwwroot/index.html | 1 + .../Cropper.Blazor/Cropper.Blazor.csproj | 7 +++++++ .../Cropper.Blazor/Cropper/helpers/blob-helper.ts | 2 +- src/Cropper.Blazor/Cropper.Blazor/tsconfig.json | 14 +++++--------- .../Cropper.Blazor/webpack.config.js | 12 ++++++++++++ 5 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/Cropper.Blazor/Client/wwwroot/index.html b/src/Cropper.Blazor/Client/wwwroot/index.html index 5ed8dfd4..2cf83604 100644 --- a/src/Cropper.Blazor/Client/wwwroot/index.html +++ b/src/Cropper.Blazor/Client/wwwroot/index.html @@ -67,6 +67,7 @@ }); + diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj index 6382648a..42a357d9 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj @@ -75,8 +75,11 @@ + + + @@ -90,8 +93,11 @@ + + + - + + @@ -155,8 +157,10 @@ + + diff --git a/src/Cropper.Blazor/Cropper.Blazor/package.json b/src/Cropper.Blazor/Cropper.Blazor/package.json index fc899185..d968bf76 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/package.json +++ b/src/Cropper.Blazor/Cropper.Blazor/package.json @@ -22,6 +22,7 @@ "@vitest/ui": "4.0.16", "clean-css": "5.3.3", "copy-webpack-plugin": "13.0.1", + "dts-bundle-generator": "^9.5.1", "eslint": "^9.39.2", "eslint-plugin-n": "^17.23.1", "eslint-plugin-prettier": "^5.5.4", diff --git a/src/Cropper.Blazor/Cropper.Blazor/webpack.config.js b/src/Cropper.Blazor/Cropper.Blazor/webpack.config.js index b593a2ab..6f6028e8 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/webpack.config.js +++ b/src/Cropper.Blazor/Cropper.Blazor/webpack.config.js @@ -1,8 +1,10 @@ -require('webpack'); +require('webpack'); const path = require('path'); const CleanCSS = require('clean-css') const CopyPlugin = require('copy-webpack-plugin') const TerserPlugin = require('terser-webpack-plugin') +const { writeFileSync } = require("fs"); +const { generateDtsBundle } = require("dts-bundle-generator"); module.exports = (env, args) => ({ resolve: { @@ -39,7 +41,77 @@ module.exports = (env, args) => ({ level: 2 }).minify(content)).styles }] - })], + }), + { + apply: (compiler) => { + compiler.hooks.afterEmit.tap("GenerateDeclarations", () => { + try { + const { generateDtsBundle } = require('dts-bundle-generator'); + const fs = require('fs'); + const path = require('path'); + + // Input TypeScript entry file + const entryFile = path.resolve(__dirname, 'Cropper/cropperJsInterop.ts'); + + // Output bundled .d.ts + const outputDir = path.resolve(__dirname, 'wwwroot'); + // Add any helper files you want to include in the bundle + const helperFiles = [ + path.resolve(__dirname, 'Cropper/helpers/blob-helper.ts'), + path.resolve(__dirname, 'Cropper/helpers/cropper-url-image-helper.ts'), + // add more helpers here if needed + ]; + + // Combine main entry + helpers + const filesToBundle = [entryFile, ...helperFiles]; + + // Create folder if it doesn't exist + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + // Generate the bundle + filesToBundle.forEach(filePath => { + const bundledDts = generateDtsBundle( + [ + { + config: path.resolve(__dirname, 'tsconfig.json'), + filePath, + output: { + inlineDeclareGlobals: true, + noBanner: true, + exportReferencedTypes: true, + sortNodes: true + }, + } + ] + ); + + // Clean up empty exports, relative re-exports, and extra blank lines + const cleanedDts = bundledDts[0] + .replace(/^export\s*{\s*};?\s*$/gmi, '') // remove empty exports + .replace(/^export\s+\*.*?\bfrom\s+"[\.~\/].*$/gmi, '') // remove relative re-exports + .replace(/^\s*[\r\n]+/gmi, '') // remove empty lines + .replace(/\/\/.*$/gmi, '') // remove // comments + .replace(/\/\*[\s\S]*?\*\//gmi, ''); // remove /* */ comments + + const fileName = path.basename(filePath, path.extname(filePath)); + const outputFile = path.join(outputDir, fileName + ".d.ts"); + + // Write to disk + fs.writeFileSync(outputFile, cleanedDts, 'utf8'); + + console.log('✅ Bundled .d.ts created at:', outputFile); + }); + + } catch (error) { + console.error("⚠️ Declaration generation failed:", error); + + throw error; + } + }); + } + }], optimization: { minimize: true, minimizer: [new TerserPlugin({ From 9d4708077fb6cf5a9f2b26e0eae0c6c228a74025 Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Sun, 11 Jan 2026 21:12:33 +0200 Subject: [PATCH 49/58] fix eslint --- .../Cropper.Blazor/eslint.config.cjs | 2 +- .../Cropper.Blazor/webpack.config.js | 247 +++++++++--------- 2 files changed, 126 insertions(+), 123 deletions(-) diff --git a/src/Cropper.Blazor/Cropper.Blazor/eslint.config.cjs b/src/Cropper.Blazor/Cropper.Blazor/eslint.config.cjs index 3fc50b00..a4de41c0 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/eslint.config.cjs +++ b/src/Cropper.Blazor/Cropper.Blazor/eslint.config.cjs @@ -7,7 +7,7 @@ module.exports = defineConfig([ globalIgnores(['**/node_modules/**']), { - files: ['Cropper/**/*.{ts,tsx,mts,cts}'], + files: ['Cropper/**/*.{ts,tsx,mts,cts}', 'webpack.config.js'], languageOptions: { parser: tsParser, diff --git a/src/Cropper.Blazor/Cropper.Blazor/webpack.config.js b/src/Cropper.Blazor/Cropper.Blazor/webpack.config.js index 6f6028e8..7a3c0df9 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/webpack.config.js +++ b/src/Cropper.Blazor/Cropper.Blazor/webpack.config.js @@ -1,138 +1,141 @@ -require('webpack'); -const path = require('path'); -const CleanCSS = require('clean-css') -const CopyPlugin = require('copy-webpack-plugin') -const TerserPlugin = require('terser-webpack-plugin') -const { writeFileSync } = require("fs"); -const { generateDtsBundle } = require("dts-bundle-generator"); +require("webpack"); +const path = require("path"); +const CleanCSS = require("clean-css"); +const CopyPlugin = require("copy-webpack-plugin"); +const TerserPlugin = require("terser-webpack-plugin"); module.exports = (env, args) => ({ - resolve: { - extensions: ['.ts', '.js', '.css'], - alias: { - cropperjs: path.resolve(__dirname, 'node_modules/cropperjs/src') - } + resolve: { + extensions: [".ts", ".js", ".css"], + alias: { + cropperjs: path.resolve(__dirname, "node_modules/cropperjs/src"), }, - devtool: args.mode === 'development' ? 'inline-source-map' : 'hidden-source-map', - module: { - rules: [{ - test: /\.ts?$/, - loader: 'ts-loader', - exclude: [ - /node_modules/, - /\.test\.ts$/, // exclude test files - /\.spec\.ts$/, // exclude spec files - /vitest\.setup\.ts$/ // exclude Vitest setup - ] - }] - }, - entry: { - "cropperJsInterop": './Cropper/cropperJsInterop.ts' - }, - output: { - path: path.join(__dirname, '/wwwroot'), - filename: '[name].min.js' - }, - plugins: [new CopyPlugin({ - patterns: [{ - to: 'cropper.min.css', - from: path.resolve(__dirname, 'node_modules/cropperjs/dist', 'cropper.min.css'), - transform: content => (new CleanCSS({ - level: 2 - }).minify(content)).styles - }] - }), + }, + devtool: args.mode === "development" ? "inline-source-map" : "hidden-source-map", + module: { + rules: [ + { + test: /\.ts?$/, + loader: "ts-loader", + exclude: [ + /node_modules/, + /\.test\.ts$/, // exclude test files + /\.spec\.ts$/, // exclude spec files + /vitest\.setup\.ts$/, // exclude Vitest setup + ], + }, + ], + }, + entry: { + cropperJsInterop: "./Cropper/cropperJsInterop.ts", + }, + output: { + path: path.join(__dirname, "/wwwroot"), + filename: "[name].min.js", + }, + plugins: [ + new CopyPlugin({ + patterns: [ { - apply: (compiler) => { - compiler.hooks.afterEmit.tap("GenerateDeclarations", () => { - try { - const { generateDtsBundle } = require('dts-bundle-generator'); - const fs = require('fs'); - const path = require('path'); - - // Input TypeScript entry file - const entryFile = path.resolve(__dirname, 'Cropper/cropperJsInterop.ts'); + to: "cropper.min.css", + from: path.resolve(__dirname, "node_modules/cropperjs/dist", "cropper.min.css"), + transform: (content) => + new CleanCSS({ + level: 2, + }).minify(content).styles, + }, + ], + }), + { + apply: (compiler) => { + compiler.hooks.afterEmit.tap("GenerateDeclarations", () => { + try { + const { generateDtsBundle } = require("dts-bundle-generator"); + const fs = require("fs"); - // Output bundled .d.ts - const outputDir = path.resolve(__dirname, 'wwwroot'); - // Add any helper files you want to include in the bundle - const helperFiles = [ - path.resolve(__dirname, 'Cropper/helpers/blob-helper.ts'), - path.resolve(__dirname, 'Cropper/helpers/cropper-url-image-helper.ts'), - // add more helpers here if needed - ]; + // Input TypeScript entry file + const entryFile = path.resolve(__dirname, "Cropper/cropperJsInterop.ts"); - // Combine main entry + helpers - const filesToBundle = [entryFile, ...helperFiles]; + // Output bundled .d.ts + const outputDir = path.resolve(__dirname, "wwwroot"); + // Add any helper files you want to include in the bundle + const helperFiles = [ + path.resolve(__dirname, "Cropper/helpers/blob-helper.ts"), + path.resolve(__dirname, "Cropper/helpers/cropper-url-image-helper.ts"), + // add more helpers here if needed + ]; - // Create folder if it doesn't exist - if (!fs.existsSync(outputDir)) { - fs.mkdirSync(outputDir, { recursive: true }); - } + // Combine main entry + helpers + const filesToBundle = [entryFile, ...helperFiles]; - // Generate the bundle - filesToBundle.forEach(filePath => { - const bundledDts = generateDtsBundle( - [ - { - config: path.resolve(__dirname, 'tsconfig.json'), - filePath, - output: { - inlineDeclareGlobals: true, - noBanner: true, - exportReferencedTypes: true, - sortNodes: true - }, - } - ] - ); + // Create folder if it doesn't exist + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } - // Clean up empty exports, relative re-exports, and extra blank lines - const cleanedDts = bundledDts[0] - .replace(/^export\s*{\s*};?\s*$/gmi, '') // remove empty exports - .replace(/^export\s+\*.*?\bfrom\s+"[\.~\/].*$/gmi, '') // remove relative re-exports - .replace(/^\s*[\r\n]+/gmi, '') // remove empty lines - .replace(/\/\/.*$/gmi, '') // remove // comments - .replace(/\/\*[\s\S]*?\*\//gmi, ''); // remove /* */ comments + // Generate the bundle + filesToBundle.forEach((filePath) => { + const bundledDts = generateDtsBundle([ + { + config: path.resolve(__dirname, "tsconfig.json"), + filePath, + output: { + inlineDeclareGlobals: true, + noBanner: true, + exportReferencedTypes: true, + sortNodes: true, + }, + }, + ]); - const fileName = path.basename(filePath, path.extname(filePath)); - const outputFile = path.join(outputDir, fileName + ".d.ts"); + // Clean up empty exports, relative re-exports, and extra blank lines + const cleanedDts = bundledDts[0] + .replace(/^export\s*{\s*};?\s*$/gim, "") // remove empty exports + .replace(/^export\s+\*.*?\bfrom\s+"[\.~\/].*$/gim, "") // remove relative re-exports + .replace(/^\s*[\r\n]+/gim, "") // remove empty lines + .replace(/\/\/.*$/gim, "") // remove // comments + .replace(/\/\*[\s\S]*?\*\//gim, ""); // remove /* */ comments - // Write to disk - fs.writeFileSync(outputFile, cleanedDts, 'utf8'); + const fileName = path.basename(filePath, path.extname(filePath)); + const outputFile = path.join(outputDir, fileName + ".d.ts"); - console.log('✅ Bundled .d.ts created at:', outputFile); - }); + // Write to disk + fs.writeFileSync(outputFile, cleanedDts, "utf8"); - } catch (error) { - console.error("⚠️ Declaration generation failed:", error); + console.log("✅ Bundled .d.ts created at:", outputFile); + }); + } catch (error) { + console.error("⚠️ Declaration generation failed:", error); - throw error; - } - }); - } - }], - optimization: { - minimize: true, - minimizer: [new TerserPlugin({ - terserOptions: { - format: { - comments: false, - }, - }, - extractComments: false, - })], - splitChunks: { - chunks: 'all', // Split all types of chunks (initial, async) - cacheGroups: { - // Group for node_modules (cropper) - cropper: { - test: /[\\/]node_modules[\\/]/, // Match modules in node_modules - name: 'cropper', // Output filename: cropper.js - chunks: 'all', - priority: -10 // Lower priority for cropper, higher for app - } - }, + throw error; + } + }); + }, + }, + ], + optimization: { + minimize: true, + minimizer: [ + new TerserPlugin({ + terserOptions: { + format: { + comments: false, + }, + }, + extractComments: false, + }), + ], + splitChunks: { + chunks: "all", // Split all types of chunks (initial, async) + cacheGroups: { + // Group for node_modules (cropper) + cropper: { + test: /[\\/]node_modules[\\/]/, // Match modules in node_modules + name: "cropper", // Output filename: cropper.js + chunks: "all", + priority: -10, // Lower priority for cropper, higher for app }, + }, }, -}); \ No newline at end of file + }, +}); From 6bd51c268da78e787a52f6d0a91b7f9bfc4a653e Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Sun, 11 Jan 2026 21:22:44 +0200 Subject: [PATCH 50/58] fix --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b974359..782f3fcf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,7 +88,7 @@ jobs: VALIDATE_TYPESCRIPT_PRETTIER: false VALIDATE_TYPESCRIPT_ES: false - FILTER_REGEX_EXCLUDE: '(\W|^)(obj/|bin/|node_modules/)|(\W|^)(.*([.]min[.]css))($)|(\W|^)(.*([.]min[.]js))($)' + FILTER_REGEX_EXCLUDE: '(\W|^)(obj/|bin/|node_modules/|webpack\.config\.js)|(\W|^).*([.]min[.]css)($)|(\W|^).*([.]min[.]js)($)' FILTER_REGEX_INCLUDE: "/github/workspace/src/Cropper.Blazor/.*|/github/workspace/.github/.*" JSCPD_CONFIG_FILE: ".jscpd.json" From 226d2b3aade977f33a91e090d7f782c22ed5f34d Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Mon, 12 Jan 2026 15:23:11 +0200 Subject: [PATCH 51/58] fix docs description --- NuGet_README.md | 1 + README.md | 1 + src/Cropper.Blazor/Client/Pages/Index.razor | 370 ++++++++++---------- 3 files changed, 187 insertions(+), 185 deletions(-) diff --git a/NuGet_README.md b/NuGet_README.md index 585f42b3..588b47cf 100644 --- a/NuGet_README.md +++ b/NuGet_README.md @@ -33,6 +33,7 @@ Cropper.Blazor is an essential component for building interactive image cropping | 1.5.x | [.NET 6](https://dotnet.microsoft.com/download/dotnet/6.0), [.NET 7](https://dotnet.microsoft.com/en-us/download/dotnet/7.0), [.NET 8](https://dotnet.microsoft.com/en-us/download/dotnet/8.0), [.NET 9](https://dotnet.microsoft.com/en-us/download/dotnet/9.0), [.NET 10](https://dotnet.microsoft.com/en-us/download/dotnet/10.0) | :heavy_check_mark: | - Supported .NET 10.0, .NET 9.0, .NET 8.0, .NET 7.0, .NET 6.0 versions for these web platforms: + - Blazor Web App - Blazor WebAssembly - Blazor Server - Blazor Server Hybrid with MVC diff --git a/README.md b/README.md index adbc85c8..cb7b469c 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ Cropper.Blazor is an essential component for building interactive image cropping | 1.5.x | [.NET 6](https://dotnet.microsoft.com/download/dotnet/6.0), [.NET 7](https://dotnet.microsoft.com/en-us/download/dotnet/7.0), [.NET 8](https://dotnet.microsoft.com/en-us/download/dotnet/8.0), [.NET 9](https://dotnet.microsoft.com/en-us/download/dotnet/9.0), [.NET 10](https://dotnet.microsoft.com/en-us/download/dotnet/10.0) | :heavy_check_mark: | - Supported .NET 10.0, .NET 9.0, .NET 8.0, .NET 7.0, .NET 6.0 versions for these web platforms: + - Blazor Web App - Blazor WebAssembly - Blazor Server - Blazor Server Hybrid with MVC diff --git a/src/Cropper.Blazor/Client/Pages/Index.razor b/src/Cropper.Blazor/Client/Pages/Index.razor index e8c714d5..b731584e 100644 --- a/src/Cropper.Blazor/Client/Pages/Index.razor +++ b/src/Cropper.Blazor/Client/Pages/Index.razor @@ -13,194 +13,194 @@ + new[] + { + "home", + "installation" + })" /> - - - - - - Cropper.Blazor - - - - - - - - - - Cropper.Blazor - is a component that wraps around - - Cropper.js - - version 1.6.2 - - - - - - View Demo - - - Star on GitHub - - -

- - Deploy to NuGet - - - Deploy to GitHub Pages - - - Code coverage - - - License - - - Last commit - - - Nuget downloads - - - Nuget version - -

- -
- - - - - - - Cropper.Blazor — most powerful image cropping tool for Blazor WebAssembly / Server, Hybrid with MAUI, MVC and other frameworks. - - Cropper.Blazor is an essential component for building interactive image cropping and manipulation features in Blazor web applications. This versatile Blazor library empowers developers to integrate intuitive image cropping functionality directly into their Blazor projects, offering users a seamless and responsive image editing experience. - - - - - - - - - - - - - - - - - - - - + + + + + + Cropper.Blazor + + + + + + + + + + Cropper.Blazor + is a component that wraps around + + Cropper.js + + version 1.6.2 + + + + + + View Demo + + + Star on GitHub + + +

+ + Deploy to NuGet + + + Deploy to GitHub Pages + + + Code coverage + + + License + + + Last commit + + + Nuget downloads + + + Nuget version + +

+ +
+ + + + + + + Cropper.Blazor — most powerful image cropping tool for Blazor Web App / WebAssembly / Server, Hybrid with MAUI, MVC and other frameworks. + + Cropper.Blazor is an essential component for building interactive image cropping and manipulation features in Blazor web applications. This versatile Blazor library empowers developers to integrate intuitive image cropping functionality directly into their Blazor projects, offering users a seamless and responsive image editing experience. + + + + + + + + + + + + + + + + + + + + + + + + If you already have a project and want to add Cropper.Blazor to it, either from a default template or a working application. + + + + + Find the package through NuGet Package Manager or install it with following command: + + + + + + + After the package is added, you need to add the following in your _Imports.razor + + + + + + + Add the following to your HTML head section, it's either index.html or _Layout.cshtml/_Host.cshtml depending on whether you're running WebAssembly/MAUI or Server. + + + + + + + In the same file but located in the end of it add the Cropper.Blazor js file, it should be in the same location as the default blazor script. + + + + + + + + Add the following in Program.cs +

+ You can change the path to the cropperJSInterop.min.js module if for some reason + it is located outside the server root folder using the examples below to override the internal or full global cropperJSInterop.min.js module path. + Actions are usually required when an application is deployed to an IIS Web Server. +
+
+
- - - If you already have a project and want to add Cropper.Blazor to it, either from a default template or a working application. - - - - - Find the package through NuGet Package Manager or install it with following command: - - - - - - - After the package is added, you need to add the following in your _Imports.razor - - - - - - - Add the following to your HTML head section, it's either index.html or _Layout.cshtml/_Host.cshtml depending on whether you're running WebAssembly/MAUI or Server. - - - - - - - In the same file but located in the end of it add the Cropper.Blazor js file, it should be in the same location as the default blazor script. - - - - - - - - Add the following in Program.cs -

- You can change the path to the cropperJSInterop.min.js module if for some reason - it is located outside the server root folder using the examples below to override the internal or full global cropperJSInterop.min.js module path. - Actions are usually required when an application is deployed to an IIS Web Server. -
-
- -
- - - - Also for server-side (Blazor Server or MVC with Blazor Server) you need add configuration SignalR, increase MaximumReceiveMessageSize of a single incoming hub message (default is 32KB) and map SignalR to your path. However, if your images are too large, the MaximumReceiveMessageSize variable should be increased to the desired value. - - - - - - - - Run following command and rebuilt the project. If that doesn't help, try the step below about override package versions. - - - - - - - - - When resolving MAUI project dependency conflicts, you can override an individual package version by using the VersionOverride property on a @("") item. - - - - - - - - - - Add the following components to your MainLayout.razor - - -
- -
-
-
-
-
-
-
+ + + Also for server-side (Blazor Server or MVC with Blazor Server) you need add configuration SignalR, increase MaximumReceiveMessageSize of a single incoming hub message (default is 32KB) and map SignalR to your path. However, if your images are too large, the MaximumReceiveMessageSize variable should be increased to the desired value. + + + + + + + + Run following command and rebuilt the project. If that doesn't help, try the step below about override package versions. + + + + + + + + + When resolving MAUI project dependency conflicts, you can override an individual package version by using the VersionOverride property on a @("") item. + + + + + + + + + + Add the following components to your MainLayout.razor + + +
+ +
+
+
+ + + + + From 095d82c11ca63c54c9567f5ae31a75a4313ae881 Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Mon, 12 Jan 2026 15:34:22 +0200 Subject: [PATCH 52/58] update npm packages version --- .../Cropper.Blazor/package.json | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Cropper.Blazor/Cropper.Blazor/package.json b/src/Cropper.Blazor/Cropper.Blazor/package.json index d968bf76..fdabe2b5 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/package.json +++ b/src/Cropper.Blazor/Cropper.Blazor/package.json @@ -16,22 +16,22 @@ "author": "", "license": "MIT", "devDependencies": { - "@typescript-eslint/eslint-plugin": "^8.51.0", - "@typescript-eslint/parser": "^8.51.0", - "@vitest/coverage-istanbul": "^4.0.16", - "@vitest/ui": "4.0.16", + "@typescript-eslint/eslint-plugin": "8.52.0", + "@typescript-eslint/parser": "8.52.0", + "@vitest/coverage-istanbul": "4.0.17", + "@vitest/ui": "4.0.17", "clean-css": "5.3.3", "copy-webpack-plugin": "13.0.1", "dts-bundle-generator": "^9.5.1", - "eslint": "^9.39.2", - "eslint-plugin-n": "^17.23.1", - "eslint-plugin-prettier": "^5.5.4", + "eslint": "9.39.2", + "eslint-plugin-n": "17.23.1", + "eslint-plugin-prettier": "5.5.4", "prettier": "^3.7.4", - "terser-webpack-plugin": "5.3.14", + "terser-webpack-plugin": "5.3.16", "ts-loader": "9.5.4", "typescript": "5.9.3", - "vitest": "^4.0.16", - "webpack": "5.103.0", + "vitest": "4.0.17", + "webpack": "5.104.1", "webpack-cli": "6.0.1" }, "dependencies": { From 2d2f3d00f9d22e3d96b1b8a22e584e071f7745e9 Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Mon, 12 Jan 2026 16:11:58 +0200 Subject: [PATCH 53/58] fix TypeScript types --- .../Cropper.Blazor/Cropper/cropperJsInterop.ts | 9 +++++---- .../Cropper.Blazor/Cropper/helpers/blob-helper.ts | 14 +++++++------- .../Cropper/helpers/cropper-url-image-helper.ts | 4 ++-- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts index 66282b52..52592b0a 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/cropperJsInterop.ts @@ -31,7 +31,7 @@ export class CropperDecorator { } destroy(id: CropperId) { - const instance = this.cropperInstances[id]; + const instance: Cropper = this.cropperInstances[id]; if (instance) { instance.destroy(); @@ -73,8 +73,9 @@ export class CropperDecorator { dotNetCanvasReceiverRef: DotNetTypes.Global.DotNetObjectReference, ) { setTimeout(async () => { - const canvas = this.getCroppedCanvas(id, options); - const jsRef = DotNet.createJSObjectReference(canvas); + const canvas: HTMLCanvasElement = this.getCroppedCanvas(id, options); + const jsRef: DotNetTypes.Global.JsObjectReference = DotNet.createJSObjectReference(canvas); + await dotNetCanvasReceiverRef.invokeMethodAsync("ReceiveCanvasReference", jsRef); }, 0); } @@ -349,7 +350,7 @@ export class CropperDecorator { options.maxWidth ??= Infinity; options.maxHeight ??= Infinity; - const cropperInstance = this.cropperInstances[cropperComponentId]; + const cropperInstance: Cropper = this.cropperInstances[cropperComponentId]; setTimeout(() => { cropperInstance.getCroppedCanvas(options).toBlob( diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/blob-helper.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/blob-helper.ts index d30504f7..1f7a8443 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/blob-helper.ts +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/blob-helper.ts @@ -10,7 +10,7 @@ import type { CropperBlazor as DotNetTypes } from "../types/global/dotnet-global export namespace CropperBlazor.Helpers { export async function readBlobInChunks( blob: Blob | null, - dotNetImageReceiverRef: DotNetTypes.Global.DotNetObjectReference, + dotNetImageReceiverRef: DotNetTypes.Global.DotNetObjectReference | null, maximumReceiveChunkSize?: number, ) { // Validate blob @@ -38,16 +38,16 @@ export namespace CropperBlazor.Helpers { if (maximumReceiveChunkSize == null) { reader = blob.stream().getReader(); } else { - const blobStream = blob.stream().getReader(); + const blobStream: ReadableStreamDefaultReader = blob.stream().getReader(); // Binary estimation of JSON size const getJsonSizeBinary = (chunk: Uint8Array) => { - const length = chunk.length; + const length: number = chunk.length; // Max 3 digits for the number (0 to 255) const bytesPerElement = 3; // Comma between elements - const commas = length - 1; + const commas: number = length - 1; // For '[' and ']' const brackets = 2; @@ -71,9 +71,9 @@ export namespace CropperBlazor.Helpers { while (offset < value.length) { // Start with the last known good chunk size, or the remaining length - let chunkSize = Math.min(lastGoodChunkSize, value.length - offset); - let chunk = value.slice(offset, offset + chunkSize); - let jsonSize = getJsonSizeBinary(chunk); + let chunkSize: number = Math.min(lastGoodChunkSize, value.length - offset); + let chunk: Uint8Array = value.slice(offset, offset + chunkSize); + let jsonSize: number = getJsonSizeBinary(chunk); // If the JSON size is too large, reduce the chunk size gradually while (jsonSize > maximumReceiveChunkSize && chunkSize > 1) { diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/cropper-url-image-helper.ts b/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/cropper-url-image-helper.ts index 7f7ab76b..3e11d08c 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/cropper-url-image-helper.ts +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper/helpers/cropper-url-image-helper.ts @@ -5,8 +5,8 @@ export namespace CropperBlazor.Helpers { async getImageUsingStreaming( imageStream: DotNetTypes.Global.DotNetStreamReference, ): Promise { - const buf = await imageStream.arrayBuffer(); - const blob = new Blob([buf]); + const buf: ArrayBuffer = await imageStream.arrayBuffer(); + const blob: Blob = new Blob([buf]); return URL.createObjectURL(blob); } From 3f282ae76fba928c62880017de2862d3fe1d9339 Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim Date: Mon, 12 Jan 2026 17:21:38 +0200 Subject: [PATCH 54/58] fix project file --- src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj index 676e6e70..cf8557ab 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj @@ -66,7 +66,6 @@ True \
- From 033b4337f0022e3bd27c80fcc4f210a5ae7129d6 Mon Sep 17 00:00:00 2001 From: Gornytskyi Maxim <50423072+MaxymGorn@users.noreply.github.com> Date: Mon, 12 Jan 2026 18:29:25 +0200 Subject: [PATCH 55/58] Update src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj index cf8557ab..7f6d12e2 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj @@ -113,7 +113,7 @@ - +
- Add receiver components to docs - Fix events cropper component desc - Fix error during blazor server prerendering in dispose methods #### Open Questions ## Checklist - [x] Tests cover new or modified code - [x] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the site documentation - [ ] I have made corresponding changes to the README, NuGet README file - [x] My changes generate no new warnings - [ ] New dependencies added or updated - [ ] Includes breaking changes - [ ] Version bumped ## Visuals --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .editorconfig | 51 +- .../Cropper.Blazor.MAUI.Net9.csproj | 2 +- .../Client/Components/Docs/ApiMethod.cs | 1 + .../Client/Components/Docs/DocsApi.razor | 528 ++++++++++-------- .../Client/Components/Docs/DocsApi.razor.cs | 159 ++++-- .../Client/Cropper.Blazor.Client.csproj | 6 +- .../Client/Models/DocStrings.cs | 27 +- src/Cropper.Blazor/Client/Pages/Api.razor | 39 -- .../Client/Pages/CropperDemo.razor.cs | 5 +- .../Client/Pages/DataContract.razor | 35 +- .../Client/Pages/DataContract.razor.cs | 36 +- .../Client/Services/MenuService.cs | 36 +- .../UserPreferences/UserPreferences.cs | 1 + .../UserPreferences/UserPreferencesService.cs | 1 + .../Client/Styles/components/docssection.scss | 2 +- .../wwwroot/service-worker.published.js | 22 +- .../Client/wwwroot/sw-registrator.js | 7 + .../DocStrings.cs | 43 +- .../Extensions/MethodInfoExtensions.cs | 89 ++- .../Extensions/TypeNameHelper.cs | 33 +- .../BunitContextExtensions.cs | 9 + .../Cropper.Blazor.Testing.csproj | 12 +- .../CropperComponent_Dispose_Should.cs | 149 +++++ .../Components/CropperComponent_Should.cs | 46 +- .../Cropper.Blazor.UnitTests.csproj | 6 +- .../Services/BaseJsInteropService.cs | 27 + .../Services/BaseJsInteropService_Should.cs | 76 ++- .../Services/CropperJsInterop_Should.cs | 2 +- .../Services/UrlImageInterop_Should.cs | 2 +- .../Components/CroppedCanvasReceiver.cs | 2 +- .../CropperComponent.razor.Commands.cs | 3 - .../CropperComponent.razor.Events.cs | 33 +- .../CropperComponent.razor.Queries.cs | 3 - .../Components/CropperComponent.razor.cs | 17 +- .../Cropper.Blazor/Cropper.Blazor.csproj | 6 +- .../Exceptions/ImageProcessingException.cs | 2 +- .../Cropper.Blazor/Services/BaseJsInterop.cs | 10 + .../Cropper.Blazor/Services/IBaseJsInterop.cs | 9 + .../Cropper.Blazor/package.json | 16 +- .../Server/Cropper.Blazor.Server.csproj | 2 +- 40 files changed, 994 insertions(+), 561 deletions(-) delete mode 100644 src/Cropper.Blazor/Client/Pages/Api.razor create mode 100644 src/Cropper.Blazor/Cropper.Blazor.UnitTests/Components/CropperComponent_Dispose_Should.cs create mode 100644 src/Cropper.Blazor/Cropper.Blazor.UnitTests/Services/BaseJsInteropService.cs diff --git a/.editorconfig b/.editorconfig index 436a2fbf..7f67143a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,24 +1,5 @@ root = true - -# All files -[*] indent_style = space -csharp_indent_labels = no_change -csharp_using_directive_placement = outside_namespace:silent -csharp_prefer_simple_using_statement = true:suggestion -csharp_prefer_braces = true:silent -csharp_style_namespace_declarations = block_scoped:silent -csharp_style_prefer_method_group_conversion = true:silent -csharp_style_prefer_top_level_statements = true:silent -csharp_style_prefer_primary_constructors = true:suggestion -csharp_style_expression_bodied_methods = false:silent -csharp_style_expression_bodied_constructors = false:silent -csharp_style_expression_bodied_operators = false:silent -csharp_style_expression_bodied_properties = true:silent -csharp_style_expression_bodied_indexers = true:silent -csharp_style_expression_bodied_accessors = true:silent -csharp_style_expression_bodied_lambdas = true:silent -csharp_style_expression_bodied_local_functions = false:silent [*.{csproj,sln,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] indent_size = 4 @@ -44,7 +25,23 @@ indent_style = space indent_size = 2 [*.{cs,vb}] -#### Naming styles #### +# Naming styles +csharp_indent_labels = no_change +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_primary_constructors = true:suggestion +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent # Naming rules @@ -64,33 +61,21 @@ dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case dotnet_naming_symbols.interface.applicable_kinds = interface dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.interface.required_modifiers = dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.types.required_modifiers = dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.non_field_members.required_modifiers = # Naming styles dotnet_naming_style.begins_with_i.required_prefix = I -dotnet_naming_style.begins_with_i.required_suffix = -dotnet_naming_style.begins_with_i.word_separator = dotnet_naming_style.begins_with_i.capitalization = pascal_case - -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case - -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case dotnet_style_operator_placement_when_wrapping = beginning_of_line + tab_width = 4 indent_size = 4 end_of_line = crlf diff --git a/examples/Cropper.Blazor.MAUI.Net9/Cropper.Blazor.MAUI.Net9.csproj b/examples/Cropper.Blazor.MAUI.Net9/Cropper.Blazor.MAUI.Net9.csproj index 3d6afa26..2ad68b79 100644 --- a/examples/Cropper.Blazor.MAUI.Net9/Cropper.Blazor.MAUI.Net9.csproj +++ b/examples/Cropper.Blazor.MAUI.Net9/Cropper.Blazor.MAUI.Net9.csproj @@ -60,7 +60,7 @@ - + diff --git a/src/Cropper.Blazor/Client/Components/Docs/ApiMethod.cs b/src/Cropper.Blazor/Client/Components/Docs/ApiMethod.cs index f9929cae..a32d8b0d 100644 --- a/src/Cropper.Blazor/Client/Components/Docs/ApiMethod.cs +++ b/src/Cropper.Blazor/Client/Components/Docs/ApiMethod.cs @@ -10,5 +10,6 @@ public class ApiMethod public string Documentation { get; set; } public MethodInfo MethodInfo { get; set; } public ParameterInfo[] Parameters { get; set; } + public bool IsJsInvokable { get; set; } } } diff --git a/src/Cropper.Blazor/Client/Components/Docs/DocsApi.razor b/src/Cropper.Blazor/Client/Components/Docs/DocsApi.razor index fc7bde81..15f793a2 100644 --- a/src/Cropper.Blazor/Client/Components/Docs/DocsApi.razor +++ b/src/Cropper.Blazor/Client/Components/Docs/DocsApi.razor @@ -9,277 +9,327 @@ @using MudBlazor; @using Cropper.Blazor.Shared.Extensions; - + - @if (!IsContract) + @if (Type is not null) { - - - - - - } - else - { - - @if (Type.IsEnum) - { -
- Enum @TypeNameHelper.GetTypeDisplay(Type, false, true) -
- } - else - { -
- Contract @TypeNameHelper.GetTypeDisplay(Type, false, true) + @if (!IsContract && !IsHelper) + { + (string? Href, string? Desc) pageInfo = GetHrefPageWithDesc(); + + + + + - } + + } + else + { + + @if (Type.IsEnum) + { +
+ Enum @TypeNameHelper.GetTypeDisplay(Type, false, true) +
+ } + else if (Type.IsInterface) + { +
+ Interface @TypeNameHelper.GetTypeDisplay(Type, false, true) +
+ } + else + { +
+ Contract @TypeNameHelper.GetTypeDisplay(Type, false, true) +
+ } +
+ } + + Description - } - @{ + + @(new MarkupString(AnalyseMethodDocumentation(GetClassDescription(), "summary"))) + // save as lists to speed up displaying the page - var properties = GetProperties().ToList(); - var methods = GetMethods().ToList(); - var eventCallbacks = GetEventCallbacks().ToList(); - } + var properties = GetProperties()?.ToList(); + bool? isShowPropertiesDescription = properties?.All(p => !string.IsNullOrWhiteSpace(p.Description)); + var methods = GetMethods()?.ToList(); + var eventCallbacks = GetEventCallbacks()?.ToList(); - @if (properties.Count() > 0) - { - - - - - - Name - Type - Default - Description - - - @if (_propertiesGrouping == Grouping.Inheritance && (Type)context.Key != Type) - { - - @($"Inherited from {((Type)context.Key).GetTypeDisplayName()}") - - } - else if (_propertiesGrouping == Grouping.Categories) - { - - @context.Key - - } - - - - @context.Name - @if (_propertiesGrouping == Grouping.Inheritance && IsOverridden(context.PropertyInfo)) + @if (properties?.Count() > 0) + { + + + + + + Name + Type + Default + @if (isShowPropertiesDescription == true) { - - overridden - - } - - -
- - @if (context.IsTwoWay) - { - - - - } -
-
- - @{ - var def = context.Default.PresentDefaultValue(); + Description } - @if (def.Contains(" + + @if (_propertiesGrouping == Grouping.Inheritance && (Type)context.Key != Type) { - + + @($"Inherited from {((Type)context.Key).GetTypeDisplayName()}") + } - else + else if (_propertiesGrouping == Grouping.Categories) { - @def + + @context.Key + } - - - -
- @(new MarkupString(HttpUtility.HtmlDecode(AnalyseMethodDocumentation(context.Description, "summary")))) -
-
-
- - -
- @(new MarkupString(HttpUtility.HtmlDecode(AnalyseMethodDocumentation(context.Description, "summary")))) -
-
-
-
- -
-
-
- } - - @if (eventCallbacks.Count() > 0) - { - - - - - - Name - Type - Description - - - @context.Name - - -
@(new MarkupString(context.Type.GetFormattedReturnSignature()))
-
-
- - @(new MarkupString(context.Type.GetFormattedReturnSignature())) - - - @HttpUtility.HtmlDecode(AnalyseMethodDocumentation(context.Description, "summary")) - -
-
-
-
- } - - @if (methods.Count() > 0) - { - - - - - - Name - Parameters - Return - Description - - - - - @if (!string.IsNullOrWhiteSpace(context.WarningSignatureMessage)) + + + + @context.Name + @if (_propertiesGrouping == Grouping.Inheritance && IsOverridden(context.PropertyInfo)) { - -
@context.WarningSignatureMessage
- @context.Signature -
- - } - else - { - @context.Signature + + overridden + }
- - @if (context.Parameters != null) - { - foreach (var parameterInfo in context.Parameters) + +
+ + @if (context.IsTwoWay) { -
-
@(new MarkupString($"
{parameterInfo.ParameterType.GetTypeDisplayName()} {parameterInfo.Name}
{AnalyseMethodDocumentation(context.Documentation, "param", parameterInfo.Name)}"))
-
+ + + } - } +
- + @{ - string methodReturn = AnalyseMethodDocumentation(context.Documentation, "returns"); + var def = context.Default.PresentDefaultValue(context.PropertyInfo); } - @if (!string.IsNullOrEmpty(methodReturn)) + @if (def.Contains("@(new MarkupString($"{methodReturn}"))
+ } else { -
@TypeNameHelper.GetTypeDisplay(context.Return.ParameterType, false, true)
+ @def } - -
@(new MarkupString(HttpUtility.HtmlDecode(AnalyseMethodDocumentation(context.Documentation, "summary"))))
-
- - - - @context.Signature - @if (!string.IsNullOrWhiteSpace(context.WarningSignatureMessage)) - { - -
- @context.WarningSignatureMessage + @if (isShowPropertiesDescription == true) + { + + +
+ @(new MarkupString(HttpUtility.HtmlDecode(AnalyseMethodDocumentation(context.Description, "summary"))))
- - } +
+
+ + +
+ @(new MarkupString(HttpUtility.HtmlDecode(AnalyseMethodDocumentation(context.Description, "summary")))) +
+
+
+ } + + + + + + } + + @if (eventCallbacks?.Count() > 0) + { + + + + + + Name + Type + Description + + + @context.Name + + +
@(new MarkupString(context.Type.GetFormattedReturnSignature()))
+
+
+ + @(new MarkupString(context.Type.GetFormattedReturnSignature())) + + + @HttpUtility.HtmlDecode(AnalyseMethodDocumentation(context.Description, "summary")) - - @if (context.Parameters != null) - { - foreach (var parameterInfo in context.Parameters) +
+
+
+
+ } + + @if (methods?.Count() > 0) + { + + + + + + Name + Parameters + Return + Description + + + + + @if (!string.IsNullOrWhiteSpace(context.WarningSignatureMessage)) { -
-
@(new MarkupString($"
{parameterInfo.ParameterType.GetTypeDisplayName()} {parameterInfo.Name}
{AnalyseMethodDocumentation(context.Documentation, "param", parameterInfo.Name)}"))
-
-
+ + @if (context.IsJsInvokable) + { +
+ Internally invokable from JS +
+ } +
@context.WarningSignatureMessage
+ @context.Signature +
+ } - } -
- - @{ - string methodReturn = AnalyseMethodDocumentation(context.Documentation, "returns"); - } - @if (!string.IsNullOrEmpty(methodReturn)) - { -
@(new MarkupString($"{methodReturn}"))
- } - else - { -
@TypeNameHelper.GetTypeDisplay(context.Return.ParameterType, false, true)
- } -
- @(new MarkupString(HttpUtility.HtmlDecode(AnalyseMethodDocumentation(context.Documentation, "summary")))) -
-
- - - - - - - - -
-
-
+ else + { + + @if (context.IsJsInvokable) + { +
+ Internally invokable from JS +
+ } + @context.Signature +
+ } + + + + @if (context.Parameters != null) + { + foreach (var parameterInfo in context.Parameters) + { +
+
@(new MarkupString($"
{TypeNameHelper.GetTypeDisplay(parameterInfo.ParameterType, false, true, Cropper.Blazor.Shared.Extensions.MethodInfoExtensions.CreateLink)} {parameterInfo.Name}
{AnalyseMethodDocumentation(context.Documentation, "param", parameterInfo.Name)}"))
+
+ } + } +
+ + @{ + string methodReturn = AnalyseMethodDocumentation(context.Documentation, "returns"); + } + @if (!string.IsNullOrEmpty(methodReturn)) + { +
@(new MarkupString($"{methodReturn}"))
+ } + else + { +
@TypeNameHelper.GetTypeDisplay(context.Return.ParameterType, false, true)
+ } +
+ +
@(new MarkupString(HttpUtility.HtmlDecode(AnalyseMethodDocumentation(context.Documentation, "summary"))))
+
+ + + + @context.Signature + @if (context.IsJsInvokable) + { + +
+ Internally invokable from JS +
+
+ } + @if (!string.IsNullOrWhiteSpace(context.WarningSignatureMessage)) + { + +
+ @context.WarningSignatureMessage +
+
+ } +
+ + @if (context.Parameters != null) + { + foreach (var parameterInfo in context.Parameters) + { +
+
@(new MarkupString($"
{TypeNameHelper.GetTypeDisplay(parameterInfo.ParameterType, false, true, Cropper.Blazor.Shared.Extensions.MethodInfoExtensions.CreateLink)} {parameterInfo.Name}
{AnalyseMethodDocumentation(context.Documentation, "param", parameterInfo.Name)}"))
+
+
+ } + } +
+ + @{ + string methodReturn = AnalyseMethodDocumentation(context.Documentation, "returns"); + } + @if (!string.IsNullOrEmpty(methodReturn)) + { +
@(new MarkupString($"{methodReturn}"))
+ } + else + { +
@TypeNameHelper.GetTypeDisplay(context.Return.ParameterType, false, true)
+ } +
+ @(new MarkupString(HttpUtility.HtmlDecode(AnalyseMethodDocumentation(context.Documentation, "summary")))) +
+ + + + + + + + + + + + + } + } + else + { + + The requested contract or component does not exist. + } - -
- @RenderTheType() -
\ No newline at end of file diff --git a/src/Cropper.Blazor/Client/Components/Docs/DocsApi.razor.cs b/src/Cropper.Blazor/Client/Components/Docs/DocsApi.razor.cs index 13286ff9..8e9bf294 100644 --- a/src/Cropper.Blazor/Client/Components/Docs/DocsApi.razor.cs +++ b/src/Cropper.Blazor/Client/Components/Docs/DocsApi.razor.cs @@ -13,6 +13,16 @@ namespace Cropper.Blazor.Client.Components.Docs { public partial class DocsApi { + [Parameter] public Type Type { get; set; } + [Parameter] public bool IsContract { get; set; } = false; + [Parameter] public bool IsHelper { get; set; } = false; + [Parameter] public bool? IsComponentContract { get; set; } = null; + [Inject] NavigationManager NavigationManager { get; set; } = null!; + + public DocsPage DocsPage { get; set; } + + // used for default value getting + private object CompInstance; private readonly List _hiddenMethods = [ "ToString", @@ -23,17 +33,38 @@ public partial class DocsApi "ReferenceEquals" ]; - [Parameter] public Type Type { get; set; } - [Parameter] public bool IsContract { get; set; } = false; - [Inject] NavigationManager NavigationManager { get; set; } = null!; + protected override async Task OnParametersSetAsync() + { + CompInstance = !Type.IsAssignableTo(typeof(IComponent)) ? null : Activator.CreateInstance(Type); - // used for default value getting - private object CompInstance; + await base.OnParametersSetAsync(); + } - public DocsPage DocsPage { get; set; } + private (string? Href, string? Desc) GetHrefPageWithDesc() + { + if (Type == typeof(CropperComponent)) + { + return ("examples/cropperusage", ""); + } + else if (Type == typeof(CroppedCanvasReceiver)) + { + return ("examples/cropping#crop-a-polygon-image-in-background", "See 'Crop in Background' example."); + } + else if (Type == typeof(ImageReceiver)) + { + return ("examples/cropping#crop-a-round-image-in-background", "See 'Crop a polygon image in Background' or 'Crop a round image in Background' examples."); + } + + return (null, null); + } private IEnumerable GetEventCallbacks() { + if (Type == null) + { + yield break; + } + string saveTypename = DocStrings.GetSaveTypename(Type); if (IsContract) @@ -42,7 +73,10 @@ private IEnumerable GetEventCallbacks() } else { - foreach (var info in Type.GetPropertyInfosWithAttribute().OrderBy(x => x.Name)) + IEnumerable? propertyInfos = IsComponentContract == true + ? Type.GetPropertyInfos() + : Type.GetPropertyInfosWithAttribute(); + foreach (var info in propertyInfos.OrderBy(x => x.Name)) { if (IsEventCallback(info)) { @@ -51,7 +85,7 @@ private IEnumerable GetEventCallbacks() Name = info.Name, PropertyInfo = info, Default = string.Empty, - Description = DocStrings.GetMemberDescription(saveTypename, info), + Description = DocStrings.GetMemberDescription(saveTypename, info, IsContract, IsComponentContract), IsTwoWay = CheckIsTwoWayEventCallback(info), Type = info.PropertyType, }; @@ -60,8 +94,35 @@ private IEnumerable GetEventCallbacks() } } + private string GetClassDescription() + { + if (Type.IsClass) + { + string saveTypename = DocStrings.GetSaveTypename(Type); + + return DocStrings.GetClassDescription(saveTypename); + } + else if (Type.IsInterface) + { + string saveTypename = DocStrings.GetSaveTypename(Type); + + return DocStrings.GetInterfaceDescription(saveTypename); + } + else if (Type.IsEnum) + { + return DocStrings.GetEnumDescription(Type.Name); + } + + return string.Empty; + } + private IEnumerable GetMethods() { + if (Type == null) + { + yield break; + } + string saveTypename = DocStrings.GetSaveTypename(Type); if (IsContract) @@ -74,29 +135,29 @@ private IEnumerable GetMethods() { if (!_hiddenMethods.Any(x => x.Contains(info.Name)) && !info.Name.StartsWith("get_") && !info.Name.StartsWith("set_")) { - if (info.GetCustomAttributes(typeof(JSInvokableAttribute), true).Length == 0) + bool hasNoJsInvokableAttribute = info.GetCustomAttributes(typeof(JSInvokableAttribute), true).Length == 0; + + Attribute? attribute = info + .GetCustomAttribute(typeof(ObsoleteAttribute), true); + string? warningSignatureMessage = null; + + if (attribute != null) { - Attribute? attribute = info - .GetCustomAttribute(typeof(ObsoleteAttribute), true); - string? warningSignatureMessage = null; - - if (attribute != null) - { - ObsoleteAttribute obsoleteAttr = (ObsoleteAttribute)attribute; - - warningSignatureMessage = obsoleteAttr.Message; - } - - yield return new ApiMethod() - { - MethodInfo = info, - WarningSignatureMessage = warningSignatureMessage, - Return = info.ReturnParameter, - Signature = info.GetSignature(), - Parameters = info.GetParameters(), - Documentation = DocStrings.GetMemberDescription(saveTypename, info) - }; + ObsoleteAttribute obsoleteAttr = (ObsoleteAttribute)attribute; + + warningSignatureMessage = obsoleteAttr.Message; } + + yield return new ApiMethod() + { + MethodInfo = info, + IsJsInvokable = !hasNoJsInvokableAttribute, + WarningSignatureMessage = warningSignatureMessage, + Return = info.ReturnParameter, + Signature = info.GetSignature(), + Parameters = info.GetParameters(), + Documentation = DocStrings.GetMemberDescription(saveTypename, info, IsContract, IsComponentContract) + }; } } } @@ -111,10 +172,15 @@ private static bool IsEventCallback(PropertyInfo? propertyInfo) private IEnumerable GetProperties() { + if (Type == null) + { + yield break; + } + string saveTypename = DocStrings.GetSaveTypename(Type); IEnumerable types = null!; - if (IsContract) + if (IsContract || IsComponentContract == true) { types = Type .GetPropertyInfos(); @@ -148,13 +214,15 @@ private IEnumerable GetProperties() private ApiProperty ToApiProperty(PropertyInfo info, string saveTypename) { + object defaultValue = GetDefaultValue(info); + return new ApiProperty { Name = info.Name, PropertyInfo = info, - Default = GetDefaultValue(info), + Default = defaultValue, IsTwoWay = CheckIsTwoWayProperty(info), - Description = DocStrings.GetMemberDescription(saveTypename, info, IsContract), + Description = DocStrings.GetMemberDescription(saveTypename, info, IsContract, IsComponentContract), Type = info.PropertyType }; } @@ -166,7 +234,7 @@ private static ApiProperty ToApiProperty(Type type, string? enumDisplayStatus, s Name = enumDisplayStatus, PropertyInfo = null, Default = value, - Description = DocStrings.GetEnumDescription(type.Name, enumDisplayStatus), + Description = DocStrings.GetEnumValueDescription(type.Name, enumDisplayStatus), Type = type }; } @@ -220,18 +288,6 @@ private bool CheckIsTwoWayProperty(PropertyInfo propertyInfo) eventCallbackInfo.GetCustomAttribute() == null; } - RenderFragment RenderTheType() - { - if (!Type.IsAssignableTo(typeof(IComponent))) - return null; - return new RenderFragment(builder => - { - builder.OpenComponent(0, Type); - builder.AddComponentReferenceCapture(1, inst => { CompInstance = inst; }); - builder.CloseComponent(); - }); - } - private async Task OnPageChanged(int newPage) { await DocsPage.ContentNavigation.ScrollToSection(new Uri(NavigationManager.BaseUri + "/api#methods")); @@ -306,19 +362,6 @@ private enum Grouping { Categories, Inheritance, None } private static bool IsOverridden(PropertyInfo p) => IsOverridden(p.GetMethod ?? p.SetMethod); // used for the "overridden" chip - // used for ordering groups of properties - private static int NumberOfAncestorClasses(Type type) - { - int n = 0; - - while ((type = type.BaseType) != null) - { - n++; - } - - return n; - } - #endregion } } diff --git a/src/Cropper.Blazor/Client/Cropper.Blazor.Client.csproj b/src/Cropper.Blazor/Client/Cropper.Blazor.Client.csproj index d70cbf89..a31600de 100644 --- a/src/Cropper.Blazor/Client/Cropper.Blazor.Client.csproj +++ b/src/Cropper.Blazor/Client/Cropper.Blazor.Client.csproj @@ -19,9 +19,9 @@ - - - + + + diff --git a/src/Cropper.Blazor/Client/Models/DocStrings.cs b/src/Cropper.Blazor/Client/Models/DocStrings.cs index ac11fa91..cb799ebe 100644 --- a/src/Cropper.Blazor/Client/Models/DocStrings.cs +++ b/src/Cropper.Blazor/Client/Models/DocStrings.cs @@ -11,13 +11,13 @@ public static partial class DocStrings * string saveTypename = DocStrings.GetSaveTypename(type); // calculate it only once * DocStrings.GetMemberDescription(saveTypename, member); */ - public static string GetMemberDescription(string saveTypename, MemberInfo member, bool isContract = false) + public static string GetMemberDescription(string saveTypename, MemberInfo member, bool isContract, bool? isComponentContract) { string name; if (member is PropertyInfo property) { - if (isContract) + if (isContract || isComponentContract == true) { name = saveTypename.Replace("<>", string.Empty) + "_property_" + property.Name; } @@ -38,13 +38,34 @@ public static string GetMemberDescription(string saveTypename, MemberInfo member return GetDocStrings(name); } - public static string GetEnumDescription(string enumName, string? enumValue) + public static string GetEnumValueDescription(string enumName, string? enumValue) { string name = $"{enumName}_enum_{enumValue}"; return GetDocStrings(name); } + public static string GetEnumDescription(string enumName) + { + string name = $"{enumName}_enum"; + + return GetDocStrings(name); + } + + public static string GetClassDescription(string className) + { + string name = $"{className}_class"; + + return GetDocStrings(name); + } + + public static string GetInterfaceDescription(string interfaceName) + { + string name = $"{interfaceName}_interface"; + + return GetDocStrings(name); + } + private static string GetDocStrings(string name) { var field = typeof(DocStrings).GetField(name, BindingFlags.Public | BindingFlags.Static | BindingFlags.GetField); diff --git a/src/Cropper.Blazor/Client/Pages/Api.razor b/src/Cropper.Blazor/Client/Pages/Api.razor deleted file mode 100644 index 50b543c7..00000000 --- a/src/Cropper.Blazor/Client/Pages/Api.razor +++ /dev/null @@ -1,39 +0,0 @@ -@page "/api" -@attribute [SitemapUrl(changeFreq: ChangeFreq.Daily, priority: 0.8)] - -@using Cropper.Blazor.Client.Components.Docs -@using Cropper.Blazor.Components; -@using Cropper.Blazor.Shared.Attributes -@using Cropper.Blazor.Shared.Models - - - -@if (ComponentType == null) -{ - -} -else -{ - -} - - -@code { - public Type ComponentType { get; set; } = null!; - - protected override void OnParametersSet() - { - ComponentType = typeof(CropperComponent); - StateHasChanged(); - } -} diff --git a/src/Cropper.Blazor/Client/Pages/CropperDemo.razor.cs b/src/Cropper.Blazor/Client/Pages/CropperDemo.razor.cs index 950369e4..7a18abf7 100644 --- a/src/Cropper.Blazor/Client/Pages/CropperDemo.razor.cs +++ b/src/Cropper.Blazor/Client/Pages/CropperDemo.razor.cs @@ -309,7 +309,7 @@ public async void OnCropEvent(JSEventData cropJSEvent) { await InvokeAsync(() => { - //JSRuntime!.InvokeVoidAsync("console.log", $"CropJSEvent {JsonSerializer.Serialize(cropJSEvent)}"); + JSRuntime!.InvokeVoidAsync("console.log", $"CropJSEvent {JsonSerializer.Serialize(cropJSEvent)}"); CropperDataPreview?.OnCropEvent(cropJSEvent.Detail); }); } @@ -408,7 +408,8 @@ public async void OnZoomEvent(JSEventData zoomJSEvent) { await InvokeAsync(() => { - //JSRuntime!.InvokeVoidAsync("console.log", $"ZoomEvent {JsonSerializer.Serialize(zoomJSEvent)}"); + JSRuntime!.InvokeVoidAsync("console.log", $"ZoomEvent {JsonSerializer.Serialize(zoomJSEvent)}"); + GetSetCropperData!.OnZoomEvent(zoomJSEvent.Detail); }); diff --git a/src/Cropper.Blazor/Client/Pages/DataContract.razor b/src/Cropper.Blazor/Client/Pages/DataContract.razor index dea85a0a..61b6f7e7 100644 --- a/src/Cropper.Blazor/Client/Pages/DataContract.razor +++ b/src/Cropper.Blazor/Client/Pages/DataContract.razor @@ -1,4 +1,7 @@ -@page "/api/{name}" +@page "/api" +@page "/api/{name}" +@attribute [SitemapUrl(changeFreq: ChangeFreq.Daily, priority: 0.8, url: "/api")] +@attribute [SitemapUrl(changeFreq: ChangeFreq.Daily, priority: 0.8, url: "/api/CropperComponent")] @attribute [SitemapUrl(changeFreq: ChangeFreq.Daily, priority: 0.7, url: "/api/CanvasData")] @attribute [SitemapUrl(changeFreq: ChangeFreq.Daily, priority: 0.7, url: "/api/ContainerData")] @attribute [SitemapUrl(changeFreq: ChangeFreq.Daily, priority: 0.7, url: "/api/CropBoxData")] @@ -22,29 +25,27 @@ @attribute [SitemapUrl(changeFreq: ChangeFreq.Daily, priority: 0.7, url: "/api/CroppedCanvasReceiver")] @attribute [SitemapUrl(changeFreq: ChangeFreq.Daily, priority: 0.7, url: "/api/ImageReceiver")] @attribute [SitemapUrl(changeFreq: ChangeFreq.Daily, priority: 0.7, url: "/api/ImageProcessingException")] +@attribute [SitemapUrl(changeFreq: ChangeFreq.Daily, priority: 0.7, url: "/api/CropEvent")] +@attribute [SitemapUrl(changeFreq: ChangeFreq.Daily, priority: 0.7, url: "/api/ActionEvent")] +@attribute [SitemapUrl(changeFreq: ChangeFreq.Daily, priority: 0.7, url: "/api/ImageSmoothingQuality")] @using Cropper.Blazor.Client.Components.Docs @using Cropper.Blazor.Shared.Attributes @using Cropper.Blazor.Shared.Models -@if (ComponentType is not null) +@if (ComponentType is not null && !HasName) { - - - - - + new[] { "API", "Cropper component API", "Cropper API", "Cropper.Blazor component API", "Cropper.Blazor API" })" /> } -else +else if (ComponentType is not null && HasName) { - - @($"Contract '{Name}' does not exist"); - + } + + diff --git a/src/Cropper.Blazor/Client/Pages/DataContract.razor.cs b/src/Cropper.Blazor/Client/Pages/DataContract.razor.cs index 4da4e8bf..168c5af7 100644 --- a/src/Cropper.Blazor/Client/Pages/DataContract.razor.cs +++ b/src/Cropper.Blazor/Client/Pages/DataContract.razor.cs @@ -1,4 +1,5 @@ using Cropper.Blazor.Client.Components.Docs; +using Cropper.Blazor.Components; using Microsoft.AspNetCore.Components; namespace Cropper.Blazor.Client.Pages @@ -10,10 +11,41 @@ public partial class DataContract public Type? ComponentType { get; set; } + private bool IsHelper = false; + private bool IsContract = true; + private bool? IsComponentContract = null; + + private bool HasName => !string.IsNullOrWhiteSpace(Name); + protected override void OnParametersSet() { - ComponentType = ApiLink.GetTypeFromComponentLink(Name); - StateHasChanged(); + // RESET state derived from parameters + IsHelper = false; + IsContract = true; + IsComponentContract = null; + + ComponentType = HasName + ? ApiLink.GetTypeFromComponentLink(Name) + : typeof(CropperComponent); + + if (ComponentType is not null && ComponentType.IsInterface) + { + IsHelper = true; + IsContract = false; + IsComponentContract = false; + } + else if (ComponentType == typeof(CropperComponent)) + { + IsContract = false; + IsComponentContract = false; + } + else if (ComponentType == typeof(ImageReceiver) || ComponentType == typeof(CroppedCanvasReceiver)) + { + IsContract = false; + IsComponentContract = true; + } + + base.OnParametersSet(); } } } diff --git a/src/Cropper.Blazor/Client/Services/MenuService.cs b/src/Cropper.Blazor/Client/Services/MenuService.cs index b3190fa8..654c1d22 100644 --- a/src/Cropper.Blazor/Client/Services/MenuService.cs +++ b/src/Cropper.Blazor/Client/Services/MenuService.cs @@ -36,25 +36,33 @@ public class MenuService : IMenuService public IEnumerable DocsLinkApi => Api ??= new List { new() {Title = "CropperComponent", Href = "api"}, - new() {Title = "ViewMode", Href = "api/ViewMode"}, - new() {Title = "DragMode", Href = "api/DragMode"}, - new() {Title = "CropperComponentType", Href = "api/CropperComponentType"}, - new() {Group = "Options", Title = "Options", Href = "api/Options"}, - new() {Group = "Options", Title = "GetCroppedCanvasOptions", Href = "api/GetCroppedCanvasOptions"}, - new() {Group = "Options", Title = "SetCropBoxDataOptions", Href = "api/SetCropBoxDataOptions"}, - new() {Group = "Options", Title = "SetCanvasDataOptions", Href = "api/SetCanvasDataOptions"}, - new() {Group = "Options", Title = "SetDataOptions", Href = "api/SetDataOptions"}, - new() {Group = "Event", Title = "CropEndEvent", Href = "api/CropEndEvent"}, - new() {Group = "Event", Title = "CropMoveEvent", Href = "api/CropMoveEvent"}, - new() {Group = "Event", Title = "CropStartEvent", Href = "api/CropStartEvent"}, - new() {Group = "Event", Title = "CropReadyEvent", Href = "api/CropReadyEvent"}, - new() {Group = "Event", Title = "ZoomEvent", Href = "api/ZoomEvent"}, + new() {Title = "CroppedCanvasReceiver", Href = "api/CroppedCanvasReceiver"}, + new() {Title = "ImageReceiver", Href = "api/ImageReceiver"}, + new() {Group = "Data", Title = "ViewMode", Href = "api/ViewMode"}, + new() {Group = "Data", Title = "DragMode", Href = "api/DragMode"}, + new() {Group = "Data", Title = "CropperComponentType", Href = "api/CropperComponentType"}, new() {Group = "Data", Title = "CropperData", Href = "api/CropperData"}, new() {Group = "Data", Title = "ImageData", Href = "api/ImageData"}, new() {Group = "Data", Title = "ContainerData", Href = "api/ContainerData"}, new() {Group = "Data", Title = "CanvasData", Href = "api/CanvasData"}, new() {Group = "Data", Title = "CropBoxData", Href = "api/CropBoxData"}, - new() {Group = "Data", Title = "JSEventData", Href = "api/JSEventData"} + new() {Group = "Data", Title = "JSEventData", Href = "api/JSEventData"}, + new() {Group = "Data", Title = "CroppedCanvas", Href = "api/CroppedCanvas"}, + new() {Group = "Data", Title = "ActionEvent", Href = "api/ActionEvent"}, + new() {Group = "Data", Title = "ImageSmoothingQuality", Href = "api/ImageSmoothingQuality"}, + new() {Group = "Options", Title = "Options", Href = "api/Options"}, + new() {Group = "Options", Title = "GetCroppedCanvasOptions", Href = "api/GetCroppedCanvasOptions"}, + new() {Group = "Options", Title = "SetCropBoxDataOptions", Href = "api/SetCropBoxDataOptions"}, + new() {Group = "Options", Title = "SetCanvasDataOptions", Href = "api/SetCanvasDataOptions"}, + new() {Group = "Options", Title = "SetDataOptions", Href = "api/SetDataOptions"}, + new() {Group = "Events", Title = "CropEndEvent", Href = "api/CropEndEvent"}, + new() {Group = "Events", Title = "CropMoveEvent", Href = "api/CropMoveEvent"}, + new() {Group = "Events", Title = "CropStartEvent", Href = "api/CropStartEvent"}, + new() {Group = "Events", Title = "CropReadyEvent", Href = "api/CropReadyEvent"}, + new() {Group = "Events", Title = "ZoomEvent", Href = "api/ZoomEvent"}, + new() {Group = "Events", Title = "CropEvent", Href = "api/CropEvent"}, + new() {Group = "Exceptions", Title = "ImageProcessingException", Href = "api/ImageProcessingException"}, + new() {Group = "Helpers", Title = "IUrlImageInterop", Href = "api/IUrlImageInterop"}, }; } } diff --git a/src/Cropper.Blazor/Client/Services/UserPreferences/UserPreferences.cs b/src/Cropper.Blazor/Client/Services/UserPreferences/UserPreferences.cs index 5e9ddea4..dc142429 100644 --- a/src/Cropper.Blazor/Client/Services/UserPreferences/UserPreferences.cs +++ b/src/Cropper.Blazor/Client/Services/UserPreferences/UserPreferences.cs @@ -1,6 +1,7 @@ using Cropper.Blazor.Client.Enums; namespace Cropper.Blazor.Client.Services.UserPreferences; + public class UserPreferences { /// diff --git a/src/Cropper.Blazor/Client/Services/UserPreferences/UserPreferencesService.cs b/src/Cropper.Blazor/Client/Services/UserPreferences/UserPreferencesService.cs index b69c77b3..d149462c 100644 --- a/src/Cropper.Blazor/Client/Services/UserPreferences/UserPreferencesService.cs +++ b/src/Cropper.Blazor/Client/Services/UserPreferences/UserPreferencesService.cs @@ -1,6 +1,7 @@ using Blazored.LocalStorage; namespace Cropper.Blazor.Client.Services.UserPreferences; + public interface IUserPreferencesService { /// diff --git a/src/Cropper.Blazor/Client/Styles/components/docssection.scss b/src/Cropper.Blazor/Client/Styles/components/docssection.scss index 28eecf90..b88d6ad3 100644 --- a/src/Cropper.Blazor/Client/Styles/components/docssection.scss +++ b/src/Cropper.Blazor/Client/Styles/components/docssection.scss @@ -1,7 +1,7 @@ .docs-page-section { .docs-section-header { .mud-typography-h5 { - margin-top: 60px; + margin-top: 40px; margin-bottom: 20px; } diff --git a/src/Cropper.Blazor/Client/wwwroot/service-worker.published.js b/src/Cropper.Blazor/Client/wwwroot/service-worker.published.js index 9d2f75c1..2c958ad6 100644 --- a/src/Cropper.Blazor/Client/wwwroot/service-worker.published.js +++ b/src/Cropper.Blazor/Client/wwwroot/service-worker.published.js @@ -3,28 +3,10 @@ self.importScripts('./service-worker-assets.js') self.addEventListener('install', event => { - event.waitUntil( - Promise.all([ - onInstall(), - self.skipWaiting() - ]) - ) + event.waitUntil(onInstall()); }) self.addEventListener('activate', event => { - event.waitUntil( - Promise.all( - [ - onActivate(), - self.clients.claim(), - self.skipWaiting() - ] - ) - .catch( - (err) => { // eslint-disable-line - event.skipWaiting() - } - ) - ) + event.waitUntil(onActivate()) }) self.addEventListener('fetch', event => event.respondWith(onFetch(event))) diff --git a/src/Cropper.Blazor/Client/wwwroot/sw-registrator.js b/src/Cropper.Blazor/Client/wwwroot/sw-registrator.js index 7dc64239..041e8326 100644 --- a/src/Cropper.Blazor/Client/wwwroot/sw-registrator.js +++ b/src/Cropper.Blazor/Client/wwwroot/sw-registrator.js @@ -15,6 +15,7 @@ window.updateAvailable = new Promise((resolve, reject) => { }, 60 * 1000) // 60000ms -> check each minute registration.onupdatefound = () => { + console.info(`Service worker onupdatefound event`) const installingServiceWorker = registration.installing installingServiceWorker.onstatechange = () => { if (installingServiceWorker.state === 'installed') { @@ -27,4 +28,10 @@ window.updateAvailable = new Promise((resolve, reject) => { console.error('Service worker registration failed with error:', error) reject(error) }) + + navigator.serviceWorker.addEventListener('controllerchange', () => { + console.log('Service worker controller changed, reloading page') + window.location.reload() + resolve(true) + }); }) diff --git a/src/Cropper.Blazor/Cropper.Blazor.Client.Compiler/DocStrings.cs b/src/Cropper.Blazor/Cropper.Blazor.Client.Compiler/DocStrings.cs index de0020ea..ed40919a 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.Client.Compiler/DocStrings.cs +++ b/src/Cropper.Blazor/Cropper.Blazor.Client.Compiler/DocStrings.cs @@ -34,7 +34,10 @@ public bool Execute() cb.IndentLevel++; Assembly assembly = typeof(CropperComponent).Assembly; - IOrderedEnumerable types = assembly.GetTypes().OrderBy(t => GetSaveTypename(t)); + IOrderedEnumerable types = assembly + .GetTypes() + .Where(x => x.FullName.StartsWith("Cropper.Blazor.")) + .OrderBy(t => GetSaveTypename(t)); foreach (var type in types) { @@ -86,18 +89,44 @@ public bool Execute() if (type.IsEnum) { string[] enumNames = type.GetEnumNames(); + string docEnum = type.GetDocumentation(); + string description = EscapeDescription(docEnum); - foreach (string enumName in enumNames) + cb.AddLine($"public const string {GetSaveTypename(type)}_enum = @\"{description}\";\n"); + + foreach (string enumItemName in enumNames) { - Enum enumValue = (Enum)Enum.Parse(type, enumName); + Enum enumValue = (Enum)Enum.Parse(type, enumItemName); string doc = enumValue.GetDocumentation(); doc = NormalizeWord(doc); doc = ConvertCrefToHTML(doc); doc = ConvertMarkdownToHTML(doc); + string descriptionItemValue = EscapeDescription(doc); + + cb.AddLine($"public const string {GetSaveTypename(type)}_enum_{enumItemName} = @\"{descriptionItemValue}\";\n"); + } + } + else if (type.IsClass) + { + string doc = type.GetDocumentation(); + + if (doc is not null) + { + string description = EscapeDescription(doc); + + cb.AddLine($"public const string {GetSaveTypename(type)}_class = @\"{description}\";\n"); + } + } + else if (type.IsInterface) + { + string doc = type.GetDocumentation(); + + if (doc is not null) + { string description = EscapeDescription(doc); - cb.AddLine($"public const string {GetSaveTypename(type)}_enum_{enumName} = @\"{description}\";\n"); + cb.AddLine($"public const string {GetSaveTypename(type)}_interface = @\"{description}\";\n"); } } } @@ -193,13 +222,17 @@ private static string ConvertSeeTagsForMethod(string doc, string formattedReturn string result = doc .Replace("
", "") .Replace("", "scaleX") + .Replace("", "IJSObjectReference") .Replace("", "ElementReference") .Replace("", "IBrowserFile") + .Replace("", "DotNetObjectReference") .Replace("", "DotNetStreamReference") .Replace("", "ValueTask") .Replace("", $"{formattedReturnSignature}") + .Replace("", "Task") + .Replace("", $"{formattedReturnSignature}") .Replace("", $"{formattedReturnSignature}") - .Replace("", "JSEventData<>") + .Replace("", "JSEventData") .Replace("", "CancellationToken"); return result; diff --git a/src/Cropper.Blazor/Cropper.Blazor.Shared/Extensions/MethodInfoExtensions.cs b/src/Cropper.Blazor/Cropper.Blazor.Shared/Extensions/MethodInfoExtensions.cs index efbe0cfa..a695436d 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.Shared/Extensions/MethodInfoExtensions.cs +++ b/src/Cropper.Blazor/Cropper.Blazor.Shared/Extensions/MethodInfoExtensions.cs @@ -125,7 +125,7 @@ public static string GetSignature(this MethodInfo method, bool callable = false) Culture = CultureInfo.InvariantCulture }; - public static string PresentDefaultValue(this object value) + public static string PresentDefaultValue(this object value, PropertyInfo? propertyInfo = null) { if (value is null) { @@ -158,7 +158,21 @@ public static string PresentDefaultValue(this object value) if (type.IsGenericType) // for instance event callbacks { - return ""; + if (propertyInfo == null) + { + return ""; + } + else + { + var propertyType = propertyInfo.PropertyType; + + // Default value for the property type + object? defaultValue = propertyType.IsValueType + ? Activator.CreateInstance(propertyType) + : null; + + return PresentDefaultValue(defaultValue); + } } if (type.IsValueType) @@ -214,7 +228,7 @@ public static string GetAliases(string value, Type type = null) ///
/// Type. May be generic or nullable /// Full type name, fully qualified namespaces - public static string TypeName(this Type type, Func? GenericArgumentFormatter = null) + public static string TypeName(this Type type, bool isShowGenericPart = true, Func? GenericArgumentFormatter = null) { var first = true; var nullableType = Nullable.GetUnderlyingType(type); @@ -229,37 +243,40 @@ public static string TypeName(this Type type, Func? GenericArgum return GetAliases(type.Name.ToUpperInvariant(), type); } - var stringBuilder = new StringBuilder(type.Name.Substring(0, type.Name.IndexOf('`'))); + StringBuilder stringBuilder = new(type.Name.Substring(0, type.Name.IndexOf('`'))); - if (GenericArgumentFormatter is not null) - { - stringBuilder.Append("<"); - } - else + if (isShowGenericPart) { - stringBuilder.Append('<'); - } - - foreach (var t in type.GetGenericArguments()) - { - if (!first) + if (GenericArgumentFormatter is not null) { - stringBuilder.Append(','); + stringBuilder.Append("<"); } - - string typeName = t.TypeName(); - - if (GenericArgumentFormatter is not null) + else { - typeName = GenericArgumentFormatter(typeName); + stringBuilder.Append('<'); } - stringBuilder.Append(typeName); + foreach (var t in type.GetGenericArguments()) + { + if (!first) + { + stringBuilder.Append(','); + } + + string typeName = t.TypeName(); - first = false; - } + if (GenericArgumentFormatter is not null) + { + typeName = GenericArgumentFormatter(typeName); + } - stringBuilder.Append('>'); + stringBuilder.Append(typeName); + + first = false; + } + + stringBuilder.Append('>'); + } // Return result return stringBuilder.ToString(); @@ -294,7 +311,7 @@ public static string GetFormattedReturnSignature(this Type type, bool callable = if (callable == false) { // Append return type - stringBuilder.Append(type.TypeName(CreateLink)); + stringBuilder.Append(type.TypeName(GenericArgumentFormatter: CreateLink)); stringBuilder.Append(' '); } @@ -312,6 +329,22 @@ public static string CreateLink(this string name) { return $"{name}"; } + else if (name == "IBrowserFile") + { + return $"{name}"; + } + else if (name == "ElementReference") + { + return $"{name}"; + } + else if (name == "DotNetStreamReference") + { + return $"{name}"; + } + else if (name == "DotNetObjectReference") + { + return $"{name}"; + } else if (name == "ErrorEventArgs") { return $"{name}"; @@ -320,6 +353,10 @@ public static string CreateLink(this string name) { return $"{name}"; } + else if (name == "MemoryStream") + { + return $"{name}"; + } else if (name == "IJSObjectReference") { return $"{name}"; diff --git a/src/Cropper.Blazor/Cropper.Blazor.Shared/Extensions/TypeNameHelper.cs b/src/Cropper.Blazor/Cropper.Blazor.Shared/Extensions/TypeNameHelper.cs index 1477b9bd..67b16c70 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.Shared/Extensions/TypeNameHelper.cs +++ b/src/Cropper.Blazor/Cropper.Blazor.Shared/Extensions/TypeNameHelper.cs @@ -43,10 +43,10 @@ public static class TypeNameHelper /// true to print a fully qualified name. /// true to include generic parameter names. /// The pretty printed type name. - public static string GetTypeDisplay(Type type, bool fullName = true, bool includeGenericParameterNames = false) + public static string GetTypeDisplay(Type type, bool fullName = true, bool includeGenericParameterNames = false, Func? genericArgumentFormatter = null) { var builder = new StringBuilder(); - ProcessType(builder, type, new DisplayNameOptions(fullName, includeGenericParameterNames)); + ProcessType(builder, type, new DisplayNameOptions(fullName, includeGenericParameterNames, genericArgumentFormatter)); return builder.ToString(); } @@ -113,7 +113,15 @@ private static void ProcessType(StringBuilder builder, Type type, DisplayNameOpt } else { - builder.Append(options.FullName ? type.FullName ?? type.Name : type.Name); + if (options.GenericArgumentFormatter is not null) + { + string typeName = type.TypeName(); + builder.Append(options.GenericArgumentFormatter(typeName)); + } + else + { + builder.Append(options.FullName ? type.FullName ?? type.Name : type.Name); + } } } @@ -177,12 +185,24 @@ private static void ProcessGenericType(StringBuilder builder, Type type, Type[] { builder.Append(builtInName); } + else if (options.GenericArgumentFormatter is not null) + { + builder.Append(options.GenericArgumentFormatter(type.Name.Substring(0, type.Name.IndexOf('`')))); + } else { builder.Append(type.Name, 0, genericPartIndex); } - builder.Append('<'); + if (options.GenericArgumentFormatter is not null) + { + builder.Append("<"); + } + else + { + builder.Append('<'); + } + for (var i = offset; i < length; i++) { ProcessType(builder, genericArguments[i], options); @@ -202,15 +222,18 @@ private static void ProcessGenericType(StringBuilder builder, Type type, Type[] private struct DisplayNameOptions { - public DisplayNameOptions(bool fullName, bool includeGenericParameterNames) + public DisplayNameOptions(bool fullName, bool includeGenericParameterNames, Func? genericArgumentFormatter = null) { FullName = fullName; IncludeGenericParameterNames = includeGenericParameterNames; + GenericArgumentFormatter = genericArgumentFormatter; } public bool FullName { get; } public bool IncludeGenericParameterNames { get; } + + public Func? GenericArgumentFormatter { get; } } } } diff --git a/src/Cropper.Blazor/Cropper.Blazor.Testing/BunitContextExtensions.cs b/src/Cropper.Blazor/Cropper.Blazor.Testing/BunitContextExtensions.cs index 65aa61bf..33d3a975 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.Testing/BunitContextExtensions.cs +++ b/src/Cropper.Blazor/Cropper.Blazor.Testing/BunitContextExtensions.cs @@ -31,5 +31,14 @@ public static IRenderedComponent GetIRenderedComponent(this TestContext te .RenderComponent(actionParameters); #endif } + + public static void DisposeTestContext(this TestContext testContext) + { +#if NET6_0 || NET7_0 + testContext.DisposeComponents(); +#endif + + testContext.Dispose(); + } } } diff --git a/src/Cropper.Blazor/Cropper.Blazor.Testing/Cropper.Blazor.Testing.csproj b/src/Cropper.Blazor/Cropper.Blazor.Testing/Cropper.Blazor.Testing.csproj index 135da976..9b8d1e8a 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.Testing/Cropper.Blazor.Testing.csproj +++ b/src/Cropper.Blazor/Cropper.Blazor.Testing/Cropper.Blazor.Testing.csproj @@ -22,18 +22,18 @@ - - + + - - + + - - + + diff --git a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Components/CropperComponent_Dispose_Should.cs b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Components/CropperComponent_Dispose_Should.cs new file mode 100644 index 00000000..2dfc74a3 --- /dev/null +++ b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Components/CropperComponent_Dispose_Should.cs @@ -0,0 +1,149 @@ +using System; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Bogus; +using Bunit; +using Cropper.Blazor.Components; +using Cropper.Blazor.Services; +using Cropper.Blazor.Testing; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; + +#if NET8_0_OR_GREATER +using TestContext = Bunit.BunitContext; +#endif + +namespace Cropper.Blazor.UnitTests.Components +{ + public class CropperComponent_Dispose_Should : IDisposable + { + private readonly TestContext _testContext; + private readonly Mock _mockCropperJsInterop; + + public CropperComponent_Dispose_Should() + { + _testContext = new Faker() + .Generate(); + + _mockCropperJsInterop = new Mock(); + + _testContext.Services.AddSingleton(_mockCropperJsInterop.Object); + } + + [Fact] + public void Should_Dispose_CropperComponent_After_Render() + { + // arrange + CancellationToken cancellationToken = new(); + + // act + IRenderedComponent cropperComponent = _testContext + .GetIRenderedComponent(); + + cropperComponent.Instance.Dispose(); + + // assert + Guid cropperComponentId = (Guid)cropperComponent.Instance + .GetInstanceField("CropperComponentId"); + + _mockCropperJsInterop.Verify(c => c.TryLoadModuleAsync(cancellationToken), Times.Once()); + _mockCropperJsInterop.Verify(c => c.DisposeAsync(), Times.Never()); + _mockCropperJsInterop.Verify(c => c.DestroyAsync(cropperComponentId, cancellationToken), Times.Once()); + _mockCropperJsInterop.VerifyNoOtherCalls(); + } + + [Fact] + public void Should_Dispose_CropperComponent_When_BlazorServer_And_Prerender() + { + // arrange + CancellationToken cancellationToken = new(); + + _mockCropperJsInterop + .Setup(c => c.IsBlazorServer) + .Returns(true); + + // act + IRenderedComponent cropperComponent = _testContext + .GetIRenderedComponent(); + + FieldInfo? isRenderedField = cropperComponent + .Instance + .GetType() + .GetField("IsRendered", BindingFlags.NonPublic | BindingFlags.Instance); + isRenderedField!.SetValue(cropperComponent.Instance, false); + + cropperComponent.Instance.Dispose(); + + // assert + Guid cropperComponentId = (Guid)cropperComponent.Instance + .GetInstanceField("CropperComponentId"); + + _mockCropperJsInterop.Verify(c => c.IsBlazorServer, Times.Once()); + _mockCropperJsInterop.Verify(c => c.TryLoadModuleAsync(cancellationToken), Times.Once()); + _mockCropperJsInterop.Verify(c => c.DisposeAsync(), Times.Never()); + _mockCropperJsInterop.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Should_DisposeAsync_CropperComponent_After_Render() + { + // arrange + CancellationToken cancellationToken = new(); + + // act + IRenderedComponent cropperComponent = _testContext + .GetIRenderedComponent(); + + await cropperComponent.Instance.DisposeAsync(); + + // assert + Guid cropperComponentId = (Guid)cropperComponent.Instance + .GetInstanceField("CropperComponentId"); + + _mockCropperJsInterop.Verify(c => c.TryLoadModuleAsync(cancellationToken), Times.Once()); + _mockCropperJsInterop.Verify(c => c.DisposeAsync(), Times.Never()); + _mockCropperJsInterop.Verify(c => c.DestroyAsync(cropperComponentId, cancellationToken), Times.Once()); + _mockCropperJsInterop.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Should_DisposeAsync_CropperComponent_When_BlazorServer_And_Prerender() + { + // arrange + CancellationToken cancellationToken = new(); + + _mockCropperJsInterop + .Setup(c => c.IsBlazorServer) + .Returns(true); + + // act + IRenderedComponent cropperComponent = _testContext + .GetIRenderedComponent(); + + FieldInfo? isRenderedField = cropperComponent + .Instance + .GetType() + .GetField("IsRendered", BindingFlags.NonPublic | BindingFlags.Instance); + isRenderedField!.SetValue(cropperComponent.Instance, false); + + await cropperComponent.Instance.DisposeAsync(); + + // assert + Guid cropperComponentId = (Guid)cropperComponent.Instance + .GetInstanceField("CropperComponentId"); + + _mockCropperJsInterop.Verify(c => c.IsBlazorServer, Times.Once()); + _mockCropperJsInterop.Verify(c => c.TryLoadModuleAsync(cancellationToken), Times.Once()); + _mockCropperJsInterop.Verify(c => c.DisposeAsync(), Times.Never()); + _mockCropperJsInterop.VerifyNoOtherCalls(); + } + + public void Dispose() + { + _testContext.DisposeTestContext(); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Components/CropperComponent_Should.cs b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Components/CropperComponent_Should.cs index c99e2663..64e35442 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Components/CropperComponent_Should.cs +++ b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Components/CropperComponent_Should.cs @@ -185,46 +185,6 @@ await func }); } - [Fact] - private void Should_Dispose_CropperComponent_After_Render() - { - // arrange - CancellationToken cancellationToken = new(); - - // act - IRenderedComponent cropperComponent = _testContext - .GetIRenderedComponent(); - - // assert - Guid cropperComponentId = (Guid)cropperComponent.Instance - .GetInstanceField("CropperComponentId"); - - cropperComponent.Instance.Dispose(); - - _mockCropperJsInterop.Verify(c => c.DisposeAsync(), Times.Never()); - _mockCropperJsInterop.Verify(c => c.DestroyAsync(cropperComponentId, cancellationToken), Times.Once()); - } - - [Fact] - private async Task Should_DisposeAsync_CropperComponent_After_Render_Async() - { - // arrange - CancellationToken cancellationToken = new(); - - // act - IRenderedComponent cropperComponent = _testContext - .GetIRenderedComponent(); - - // assert - Guid cropperComponentId = (Guid)cropperComponent.Instance - .GetInstanceField("CropperComponentId"); - - await cropperComponent.Instance.DisposeAsync(); - - _mockCropperJsInterop.Verify(c => c.DisposeAsync(), Times.Never()); - _mockCropperJsInterop.Verify(c => c.DestroyAsync(cropperComponentId, cancellationToken), Times.Once()); - } - [Fact] public async Task Should_Render_CropperComponent_From_Image_SuccessfulAsync() { @@ -1460,11 +1420,7 @@ private bool VerifyOptions(Options options) => public void Dispose() { -#if NET6_0 || NET7_0 - _testContext.DisposeComponents(); -#endif - - _testContext.Dispose(); + _testContext.DisposeTestContext(); GC.SuppressFinalize(this); } } diff --git a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Cropper.Blazor.UnitTests.csproj b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Cropper.Blazor.UnitTests.csproj index e2b7ef47..364f585d 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Cropper.Blazor.UnitTests.csproj +++ b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Cropper.Blazor.UnitTests.csproj @@ -39,7 +39,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -48,7 +48,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -57,7 +57,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Services/BaseJsInteropService.cs b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Services/BaseJsInteropService.cs new file mode 100644 index 00000000..3e7e2d7c --- /dev/null +++ b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Services/BaseJsInteropService.cs @@ -0,0 +1,27 @@ +using Bogus; +using Bunit; + +#if NET8_0_OR_GREATER +using TestContext = Bunit.BunitContext; +#endif + +namespace Cropper.Blazor.UnitTests.Services +{ + public abstract class BaseJsInteropService + { + protected readonly TestContext _testContext; + + public BaseJsInteropService() + { + _testContext = new Faker() + .Generate(); + } + + protected void VerifyLoadCropperModule( + string pathToCropperModule) + { + _testContext.JSInterop + .SetupModule(pathToCropperModule); + } + } +} diff --git a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Services/BaseJsInteropService_Should.cs b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Services/BaseJsInteropService_Should.cs index 273188a5..d3a846d9 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Services/BaseJsInteropService_Should.cs +++ b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Services/BaseJsInteropService_Should.cs @@ -1,13 +1,25 @@ -using Bogus; -using Bunit; +using System.Threading; +using System.Threading.Tasks; +using System; +using Bogus; +using Cropper.Blazor.ModuleOptions; +using Cropper.Blazor.Services; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.JSInterop; +using Xunit; +using FluentAssertions; +using Cropper.Blazor.Testing; #if NET8_0_OR_GREATER using TestContext = Bunit.BunitContext; +#else +using Bunit; #endif namespace Cropper.Blazor.UnitTests.Services { - public abstract class BaseJsInteropService_Should + public class BaseJsInteropService_Should : IDisposable { protected readonly TestContext _testContext; @@ -17,11 +29,61 @@ public BaseJsInteropService_Should() .Generate(); } - protected void VerifyLoadCropperModule( - string pathToCropperModule) + [Fact] + public void IsBlazorServer_Should_Return_True_For_RemoteJSRuntime() + { + // Arrange + RemoteJSRuntime jsRuntime = new RemoteJSRuntime(); + NavigationManager navigation = _testContext.Services.GetRequiredService(); + ICropperJsInteropOptions options = new Faker().Generate(); + TestBaseJsInterop service = new TestBaseJsInterop(jsRuntime, navigation, options); + + // Act + bool result = service.IsBlazorServer; + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public void IsBlazorServer_Should_Return_False_For_OtherJSRuntime() + { + // Arrange + NavigationManager navigation = _testContext.Services.GetRequiredService(); + ICropperJsInteropOptions options = new Faker().Generate(); + TestBaseJsInterop service = new TestBaseJsInterop(_testContext.JSInterop.JSRuntime, navigation, options); + + // Act + bool result = service.IsBlazorServer; + + // Assert + result.Should().BeFalse(); + } + + public void Dispose() + { + _testContext.DisposeTestContext(); + GC.SuppressFinalize(this); + } + + private class TestBaseJsInterop : BaseJsInterop { - _testContext.JSInterop - .SetupModule(pathToCropperModule); + public TestBaseJsInterop( + IJSRuntime jsRuntime, + NavigationManager navigationManager, + ICropperJsInteropOptions cropperJsInteropOptions) : base(jsRuntime, navigationManager, cropperJsInteropOptions) + { + + } + } + + private class RemoteJSRuntime : IJSRuntime + { + public ValueTask InvokeAsync(string identifier, object?[]? args) => + throw new NotImplementedException(); + + public ValueTask InvokeAsync(string identifier, CancellationToken cancellationToken, object?[]? args) => + throw new NotImplementedException(); } } } diff --git a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Services/CropperJsInterop_Should.cs b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Services/CropperJsInterop_Should.cs index acda6a21..3a58b629 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Services/CropperJsInterop_Should.cs +++ b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Services/CropperJsInterop_Should.cs @@ -26,7 +26,7 @@ namespace Cropper.Blazor.UnitTests.Services { - public class CropperJsInterop_Should : BaseJsInteropService_Should, IDisposable + public class CropperJsInterop_Should : BaseJsInteropService, IDisposable { private readonly Faker _faker; private readonly ICropperJsInterop _cropperJsInterop; diff --git a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Services/UrlImageInterop_Should.cs b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Services/UrlImageInterop_Should.cs index bb91713b..17f3c90f 100644 --- a/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Services/UrlImageInterop_Should.cs +++ b/src/Cropper.Blazor/Cropper.Blazor.UnitTests/Services/UrlImageInterop_Should.cs @@ -22,7 +22,7 @@ namespace Cropper.Blazor.UnitTests.Services { - public class UrlImageInterop_Should : BaseJsInteropService_Should, IDisposable + public class UrlImageInterop_Should : BaseJsInteropService, IDisposable { private readonly Faker _faker; private readonly IUrlImageInterop _urlImageInterop; diff --git a/src/Cropper.Blazor/Cropper.Blazor/Components/CroppedCanvasReceiver.cs b/src/Cropper.Blazor/Cropper.Blazor/Components/CroppedCanvasReceiver.cs index 58e4f70c..2f2dbc4a 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Components/CroppedCanvasReceiver.cs +++ b/src/Cropper.Blazor/Cropper.Blazor/Components/CroppedCanvasReceiver.cs @@ -36,7 +36,7 @@ public CroppedCanvasReceiver( /// /// Receives a cropped canvas reference from JavaScript. /// - /// The cropped canvas reference. + /// The used to reference the cropped canvas in JavaScript. [JSInvokable("ReceiveCanvasReference")] public void ReceiveCanvasReference(IJSObjectReference jsRuntimeObjectRef) { diff --git a/src/Cropper.Blazor/Cropper.Blazor/Components/CropperComponent.razor.Commands.cs b/src/Cropper.Blazor/Cropper.Blazor/Components/CropperComponent.razor.Commands.cs index 75c144ae..400b3432 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Components/CropperComponent.razor.Commands.cs +++ b/src/Cropper.Blazor/Cropper.Blazor/Components/CropperComponent.razor.Commands.cs @@ -8,9 +8,6 @@ namespace Cropper.Blazor.Components { - /// - /// The cropper component. - /// public partial class CropperComponent { /// diff --git a/src/Cropper.Blazor/Cropper.Blazor/Components/CropperComponent.razor.Events.cs b/src/Cropper.Blazor/Cropper.Blazor/Components/CropperComponent.razor.Events.cs index a2be4f2b..3da38a16 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Components/CropperComponent.razor.Events.cs +++ b/src/Cropper.Blazor/Cropper.Blazor/Components/CropperComponent.razor.Events.cs @@ -12,9 +12,6 @@ namespace Cropper.Blazor.Components { - /// - /// The cropper component. - /// public partial class CropperComponent { /// @@ -96,7 +93,10 @@ public void OnErrorLoadImage(ErrorEventArgs errorEventArgs) /// /// This event fires when the canvas (image wrapper) or the crop box changes. /// - /// The . + /// + /// The containing data of the underlying + /// JavaScript crop event. + /// [JSInvokable("CropperIsCroped")] public void CropperIsCroped(JSEventData jSEventData) { @@ -106,7 +106,10 @@ public void CropperIsCroped(JSEventData jSEventData) /// /// This event fires when the canvas (image wrapper) or the crop box stops changing. /// - /// The . + /// + /// The containing data of the underlying + /// JavaScript cropend event. + /// [JSInvokable("CropperIsEnded")] public void CropperIsEnded(JSEventData jSEventData) { @@ -116,7 +119,10 @@ public void CropperIsEnded(JSEventData jSEventData) /// /// This event fires when the canvas (image wrapper) or the crop box is changing. /// - /// The . + /// + /// The containing data of the underlying + /// JavaScript cropmove event. + /// [JSInvokable("CropperIsMoved")] public void CropperIsMoved(JSEventData jSEventData) { @@ -126,7 +132,10 @@ public void CropperIsMoved(JSEventData jSEventData) /// /// This event fires when the canvas (image wrapper) or the crop box starts to change. /// - /// The . + /// + /// The containing data of the underlying + /// JavaScript cropstart event. + /// [JSInvokable("CropperIsStarted")] public void CropperIsStarted(JSEventData jSEventData) { @@ -136,7 +145,10 @@ public void CropperIsStarted(JSEventData jSEventData) /// /// This event fires when a cropper instance starts to zoom in or zoom out its canvas (image wrapper). /// - /// The . + /// + /// The containing data of the underlying + /// JavaScript zoom event. + /// [JSInvokable("CropperIsZoomed")] public void CropperIsZoomed(JSEventData jSEventData) { @@ -146,7 +158,10 @@ public void CropperIsZoomed(JSEventData jSEventData) /// /// This event fires when the target image has been loaded and the cropper instance is ready for operating. /// - /// The . + /// + /// The containing the data of the + /// underlying JavaScript ready event. + /// [JSInvokable] public void IsReady(JSEventData jSEventData) { diff --git a/src/Cropper.Blazor/Cropper.Blazor/Components/CropperComponent.razor.Queries.cs b/src/Cropper.Blazor/Cropper.Blazor/Components/CropperComponent.razor.Queries.cs index f6bf0b60..826444e1 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Components/CropperComponent.razor.Queries.cs +++ b/src/Cropper.Blazor/Cropper.Blazor/Components/CropperComponent.razor.Queries.cs @@ -8,9 +8,6 @@ namespace Cropper.Blazor.Components { - /// - /// The cropper component. - /// public partial class CropperComponent { /// diff --git a/src/Cropper.Blazor/Cropper.Blazor/Components/CropperComponent.razor.cs b/src/Cropper.Blazor/Cropper.Blazor/Components/CropperComponent.razor.cs index 6d8b9385..cdd076c9 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Components/CropperComponent.razor.cs +++ b/src/Cropper.Blazor/Cropper.Blazor/Components/CropperComponent.razor.cs @@ -9,7 +9,8 @@ namespace Cropper.Blazor.Components { /// - /// The cropper component. + /// A Blazor component that provides image and canvas cropping functionality + /// via JavaScript interop, wrapping the underlying Cropper.js behavior. /// public partial class CropperComponent : ICropperComponentBase, IAsyncDisposable, IDisposable { @@ -96,6 +97,8 @@ public partial class CropperComponent : ICropperComponentBase, IAsyncDisposable, [Parameter(CaptureUnmatchedValues = true)] public Dictionary InputAttributes { get; set; } = null!; + private bool IsRendered = false; + /// /// Method invoked after each time the component has been rendered. Note that the component does /// not automatically re-render after the completion of any returned , because @@ -116,6 +119,8 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { + IsRendered = true; + await CropperJsIntertop!.TryLoadModuleAsync(); } @@ -136,6 +141,11 @@ protected override void OnInitialized() /// A representing any asynchronous operation. public async ValueTask DisposeAsync() { + if (!IsRendered && CropperJsIntertop.IsBlazorServer) + { + return; + } + ElementReference? cropperElementReference = GetCropperElementReference(); if (cropperElementReference.HasValue) @@ -149,6 +159,11 @@ public async ValueTask DisposeAsync() /// public void Dispose() { + if (!IsRendered && CropperJsIntertop.IsBlazorServer) + { + return; + } + ElementReference? cropperElementReference = GetCropperElementReference(); if (cropperElementReference.HasValue) diff --git a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj index a3e3d544..e71f18cc 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj +++ b/src/Cropper.Blazor/Cropper.Blazor/Cropper.Blazor.csproj @@ -180,15 +180,15 @@ - + - + - + diff --git a/src/Cropper.Blazor/Cropper.Blazor/Exceptions/ImageProcessingException.cs b/src/Cropper.Blazor/Cropper.Blazor/Exceptions/ImageProcessingException.cs index 1522e467..6901b0cb 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Exceptions/ImageProcessingException.cs +++ b/src/Cropper.Blazor/Cropper.Blazor/Exceptions/ImageProcessingException.cs @@ -3,7 +3,7 @@ namespace Cropper.Blazor.Exceptions { /// - /// Represents an exception that is thrown when an error occurs during image processing. + /// Represents an exception that is thrown when an error occurs during background image processing. /// public class ImageProcessingException : Exception { diff --git a/src/Cropper.Blazor/Cropper.Blazor/Services/BaseJsInterop.cs b/src/Cropper.Blazor/Cropper.Blazor/Services/BaseJsInterop.cs index 2b139f78..ef4b0b44 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Services/BaseJsInterop.cs +++ b/src/Cropper.Blazor/Cropper.Blazor/Services/BaseJsInterop.cs @@ -75,6 +75,16 @@ private async Task LoadModuleAsync(CancellationToken cancellationToken = default "import", cancellationToken, globalPathToCropperModule); } + /// + /// Determines whether the application is running in a Blazor Server environment + /// by checking if the JavaScript runtime type is RemoteJSRuntime. + /// + /// + /// True if running on Blazor Server; otherwise, false. + /// + public bool IsBlazorServer => + string.Equals(_jsRuntime.GetType().Name, "RemoteJSRuntime", StringComparison.OrdinalIgnoreCase); + /// /// Finds path to the cropper module. /// diff --git a/src/Cropper.Blazor/Cropper.Blazor/Services/IBaseJsInterop.cs b/src/Cropper.Blazor/Cropper.Blazor/Services/IBaseJsInterop.cs index 44bbb0da..9b37a205 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/Services/IBaseJsInterop.cs +++ b/src/Cropper.Blazor/Cropper.Blazor/Services/IBaseJsInterop.cs @@ -9,6 +9,15 @@ namespace Cropper.Blazor.Services /// public interface IBaseJsInterop : IAsyncDisposable { + /// + /// Determines whether the application is running in a Blazor Server environment + /// by checking if the JavaScript runtime type is RemoteJSRuntime. + /// + /// + /// True if running on Blazor Server; otherwise, false. + /// + bool IsBlazorServer { get; } + /// /// Try load JavaScript object into .NET when module empty. /// diff --git a/src/Cropper.Blazor/Cropper.Blazor/package.json b/src/Cropper.Blazor/Cropper.Blazor/package.json index 641975e3..60c66421 100644 --- a/src/Cropper.Blazor/Cropper.Blazor/package.json +++ b/src/Cropper.Blazor/Cropper.Blazor/package.json @@ -16,21 +16,21 @@ "author": "@MaxymGorn (Maksym Hornytskiy), @ColdForeign (George Radchuk)", "license": "MIT", "devDependencies": { - "@typescript-eslint/eslint-plugin": "8.52.0", - "@typescript-eslint/parser": "8.52.0", - "@vitest/coverage-istanbul": "4.0.17", - "@vitest/ui": "4.0.17", + "@typescript-eslint/eslint-plugin": "8.54.0", + "@typescript-eslint/parser": "8.54.0", + "@vitest/coverage-istanbul": "4.0.18", + "@vitest/ui": "4.0.18", "clean-css": "5.3.3", "copy-webpack-plugin": "13.0.1", "dts-bundle-generator": "^9.5.1", "eslint": "9.39.2", - "eslint-plugin-n": "17.23.1", - "eslint-plugin-prettier": "5.5.4", - "prettier": "^3.7.4", + "eslint-plugin-n": "17.23.2", + "eslint-plugin-prettier": "5.5.5", + "prettier": "3.8.1", "terser-webpack-plugin": "5.3.16", "ts-loader": "9.5.4", "typescript": "5.9.3", - "vitest": "4.0.17", + "vitest": "4.0.18", "webpack": "5.104.1", "webpack-cli": "6.0.1" }, diff --git a/src/Cropper.Blazor/Server/Cropper.Blazor.Server.csproj b/src/Cropper.Blazor/Server/Cropper.Blazor.Server.csproj index 5acc3f99..f898d670 100644 --- a/src/Cropper.Blazor/Server/Cropper.Blazor.Server.csproj +++ b/src/Cropper.Blazor/Server/Cropper.Blazor.Server.csproj @@ -7,7 +7,7 @@ - +