Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -225,3 +225,4 @@ sample.db
/ci/scripts/npm/dotvvm-types/index.d.ts

signconfig.json
/src/Samples/Common/Temp/Upload
57 changes: 44 additions & 13 deletions src/Framework/Framework/Controls/FileUpload.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public bool AllowMultipleFiles
get { return (bool)GetValue(AllowMultipleFilesProperty)!; }
set { SetValue(AllowMultipleFilesProperty, value); }
}

[AttachedProperty(typeof(bool))]
public static readonly DotvvmProperty AllowMultipleFilesProperty
= DotvvmProperty.Register<bool, FileUpload>(p => p.AllowMultipleFiles, true);

Expand All @@ -58,6 +58,7 @@ public string? AllowedFileTypes
set { SetValue(AllowedFileTypesProperty, value); }
}

[AttachedProperty(typeof(string))]
public static readonly DotvvmProperty AllowedFileTypesProperty
= DotvvmProperty.Register<string?, FileUpload>(p => p.AllowedFileTypes);

Expand Down Expand Up @@ -86,6 +87,7 @@ public int? MaxFileSize
set { SetValue(MaxFileSizeProperty, value); }
}

[AttachedProperty(typeof(int?))]
public static readonly DotvvmProperty MaxFileSizeProperty
= DotvvmProperty.Register<int?, FileUpload>(c => c.MaxFileSize);

Expand Down Expand Up @@ -147,7 +149,7 @@ public Command? UploadCompleted
get { return (Command?)GetValue(UploadCompletedProperty); }
set { SetValue(UploadCompletedProperty, value); }
}

[AttachedProperty(typeof(Command))]
public static readonly DotvvmProperty UploadCompletedProperty
= DotvvmProperty.Register<Command?, FileUpload>(p => p.UploadCompleted);

Expand All @@ -171,13 +173,18 @@ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequest
writer.AddKnockoutDataBind("with", uploadedFiles, this);
writer.AddAttribute("class", "dotvvm-upload", true);

var uploadCompletedBinding = GetCommandBinding(UploadCompletedProperty);
RenderUploadCompletedBinding(this, writer);

base.AddAttributesToRender(writer, context);
}

private static void RenderUploadCompletedBinding(DotvvmControl control, IHtmlWriter writer)
{
var uploadCompletedBinding = control.GetCommandBinding(UploadCompletedProperty);
if (uploadCompletedBinding != null)
{
writer.AddAttribute("data-dotvvm-upload-completed", KnockoutHelper.GenerateClientPostBackScript(nameof(UploadCompleted), uploadCompletedBinding, this, useWindowSetTimeout: true, returnValue: null));
writer.AddAttribute("data-dotvvm-upload-completed", KnockoutHelper.GenerateClientPostBackScript(nameof(UploadCompleted), uploadCompletedBinding, control, useWindowSetTimeout: true, returnValue: null));
}

base.AddAttributesToRender(writer, context);
}

protected override void RenderContents(IHtmlWriter writer, IDotvvmRequestContext context)
Expand Down Expand Up @@ -258,34 +265,58 @@ private void RenderInputControl(IHtmlWriter writer, IDotvvmRequestContext contex
writer.AddAttribute("capture", Capture);
}

writer.AddKnockoutDataBind("dotvvm-FileUpload", JsonSerializer.Serialize(new { url = context.TranslateVirtualPath(GetFileUploadHandlerUrl()) }, DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe));
writer.AddKnockoutDataBind("dotvvm-FileUpload", JsonSerializer.Serialize(new { url = context.TranslateVirtualPath(GetFileUploadHandlerUrl(this)) }, DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe));
writer.RenderSelfClosingTag("input");
}

private string GetFileUploadHandlerUrl()
private static string GetFileUploadHandlerUrl(DotvvmControl control)
{
var builder = new StringBuilder("~/");
builder.Append(HostingConstants.FileUploadHandlerMatchUrl);
var delimiter = "?";

if (AllowMultipleFiles)
if (control.GetValue(AllowMultipleFilesProperty) as bool? == true)
{
builder.AppendFormat("{0}multiple=true", delimiter);
delimiter = "&";
}

if (!string.IsNullOrWhiteSpace(AllowedFileTypes))
var allowedFileTypes = control.GetValue(AllowedFileTypesProperty) as string;
if (!string.IsNullOrWhiteSpace(allowedFileTypes))
{
builder.AppendFormat("{0}fileTypes={1}", delimiter, WebUtility.UrlEncode(AllowedFileTypes));
builder.AppendFormat("{0}fileTypes={1}", delimiter, WebUtility.UrlEncode(allowedFileTypes));
delimiter = "&";
}

if (MaxFileSize != null)
var maxFileSize = control.GetValue(MaxFileSizeProperty) as int?;
if (maxFileSize != null)
{
builder.AppendFormat("{0}maxSize={1}", delimiter, MaxFileSize);
builder.AppendFormat("{0}maxSize={1}", delimiter, maxFileSize);
}

return builder.ToString();
}

/// <summary>
/// Gets or sets the UploadedFilesCollection to which files will be uploaded when pasted or dropped on a control (typically TextBox).
/// This is an attached property.
/// </summary>
[MarkupOptions(AllowHardCodedValue = false)]
[AttachedProperty(typeof(UploadedFilesCollection))]
public static readonly DotvvmProperty UploadOnPasteOrDropProperty
= DelegateActionProperty<UploadedFilesCollection>.Register<FileUpload>("UploadOnPasteOrDrop", RenderUploadOnPasteOrDropProperty);

private static void RenderUploadOnPasteOrDropProperty(IHtmlWriter writer, IDotvvmRequestContext context, DotvvmProperty property, DotvvmControl control)
{
RenderUploadCompletedBinding(control, writer);

var group = new KnockoutBindingGroup()
{
{ "url", KnockoutHelper.MakeStringLiteral(context.TranslateVirtualPath(GetFileUploadHandlerUrl(control))) },
{ "collection", control, property },
{ "multiple", control, AllowMultipleFilesProperty }
};
writer.AddKnockoutDataBind("dotvvm-FileUpload-UploadOnPasteOrDrop", group);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import updateProgress from './update-progress'
import gridviewdataset from './gridviewdataset'
import namedCommand from './named-command'
import fileUpload from './file-upload'
import fileUploadPasteDrop from './file-upload-paste-drop'
import jsComponents from './js-component'
import modalDialog from './modal-dialog'
import appendableDataPager from './appendable-data-pager'
Expand All @@ -29,6 +30,7 @@ const allHandlers: KnockoutHandlerDictionary = {
...gridviewdataset,
...namedCommand,
...fileUpload,
...fileUploadPasteDrop,
...jsComponents,
...modalDialog,
...appendableDataPager,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { uploadFiles } from "../controls/fileUpload";

export default {
"dotvvm-FileUpload-UploadOnPasteOrDrop": {
init: function (element: HTMLElement, valueAccessor: () => any, allBindings?: any, viewModel?: any, bindingContext?: KnockoutBindingContext) {
const options = ko.unwrap(valueAccessor());
const uploadUrl = ko.unwrap(options.url);
const collectionObservable = options.collection;

function callUploadFiles(files: FileList) {
uploadFiles(collectionObservable, options.multiple, uploadUrl, files, () => {
if (element.hasAttribute("data-dotvvm-upload-completed")) {
new Function(element.getAttribute("data-dotvvm-upload-completed")!).call(element);
}
});
}

// Handle paste events
element.addEventListener("paste", function(e: ClipboardEvent) {
e.preventDefault();

if (e.clipboardData && e.clipboardData.files && e.clipboardData.files.length > 0) {
callUploadFiles(e.clipboardData.files);
}
});

// Handle drop events
element.addEventListener("dragover", function(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
element.classList.add("dotvvm-upload-dragover");
});

element.addEventListener("dragleave", function(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
element.classList.remove("dotvvm-upload-dragover");
});

element.addEventListener("drop", function(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
element.classList.remove("dotvvm-upload-dragover");

if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length > 0) {
callUploadFiles(e.dataTransfer.files);
}
});
}
}
}
Original file line number Diff line number Diff line change
@@ -1,44 +1,20 @@
import { uploadFiles } from '../controls/fileUpload';

export default {
"dotvvm-FileUpload": {
init: function (element: HTMLInputElement, valueAccessor: () => any, allBindings?: any, viewModel?: any, bindingContext?: KnockoutBindingContext) {

var args = ko.unwrap(valueAccessor());

function reportProgress(isBusy: boolean, percent: number, resultOrError: string | DotvvmStaticCommandResponse<DotvvmFileUploadData[]>) {
dotvvm.fileUpload.reportProgress(<HTMLInputElement> element, isBusy, percent, resultOrError);
}

element.addEventListener("change", function() {
if (!element.files || !element.files.length) return;

var xhr = XMLHttpRequest ? new XMLHttpRequest() : new ((window as any)["ActiveXObject"])("Microsoft.XMLHTTP");
xhr.open("POST", args.url, true);
xhr.setRequestHeader("X-DotVVM-AsyncUpload", "true");
xhr.upload.onprogress = function (e: ProgressEvent) {
if (e.lengthComputable) {
reportProgress(true, Math.round(e.loaded * 100 / e.total), '');
}
};
xhr.onload = function () {
if (xhr.status == 200) {
reportProgress(false, 100, JSON.parse(xhr.responseText));
element.value = "";
} else {
reportProgress(false, 0, "Upload failed.");
uploadFiles(ko.contextFor(element).$rawData, element.multiple, args.url, element.files, () => {
if (element.parentElement!.hasAttribute("data-dotvvm-upload-completed")) {
new Function(element.parentElement!.getAttribute("data-dotvvm-upload-completed")!).call(element);
}
};

var formData = new FormData();
if (element.files.length > 1) {
for (var i = 0; i < element.files.length; i++) {
formData.append("upload[]", element.files[i]);
}
} else if (element.files.length > 0) {
formData.append("upload", element.files[0]);
}
xhr.send(formData);
element.value = "";
});
});

}
}
}
73 changes: 46 additions & 27 deletions src/Framework/Framework/Resources/Scripts/controls/fileUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,52 @@ export function showUploadDialog(sender: HTMLElement) {
fileUpload!.click();
}

export function reportProgress(inputControl: HTMLInputElement, isBusy: boolean, progress: number, result: DotvvmStaticCommandResponse<DotvvmFileUploadData[]> | string): void {
// find target control viewmodel
const targetControl = <HTMLDivElement> inputControl.parentElement!;
const viewModel = <DotvvmFileUploadCollection> ko.dataFor(targetControl.firstChild!);

// determine the status
if (typeof result === "string") {
// error during upload
viewModel.Error(result);
} else if ("result" in result) {
// files were uploaded successfully
viewModel.Error("");
updateTypeInfo(result.typeMetadata);

// if multiple files are allowed, we append to the collection
// if it's not, we replace the collection with the one new file
const allowMultiple = inputControl.multiple
const filesObservable = viewModel.Files as DotvvmObservable<any>;
const newFiles = allowMultiple ? [...filesObservable.state, ...result.result] : result.result;
filesObservable.setState!(newFiles)

// call the handler
if (((<any> targetControl.attributes)["data-dotvvm-upload-completed"] || { value: null }).value) {
new Function((<any> targetControl.attributes)["data-dotvvm-upload-completed"].value).call(targetControl);
export function uploadFiles(viewModel: DotvvmObservable<DotvvmFileUploadCollection>, allowMultiple: boolean, url: string, files: FileList, onCompleted: () => void) {
var xhr = XMLHttpRequest ? new XMLHttpRequest() : new ((window as any)["ActiveXObject"])("Microsoft.XMLHTTP");
xhr.open("POST", url, true);
xhr.setRequestHeader("X-DotVVM-AsyncUpload", "true");
xhr.upload.onprogress = function (e: ProgressEvent) {
if (e.lengthComputable) {
(viewModel as any).patchState({ Error: null, IsBusy: true, Progress: Math.round(e.loaded * 100 / e.total) });
}
};
xhr.onerror = function () {
(viewModel as any).patchState({ Error: "Upload failed.", IsBusy: false, Progress: 0 });
};
xhr.onload = function () {
if (xhr.status == 200) {
(viewModel as any).patchState({ Error: null, IsBusy: true, Progress: 100 });

const result = JSON.parse(xhr.responseText) as DotvvmStaticCommandResponse<DotvvmFileUploadData[]>;
if ("typeMetadata" in result) {
updateTypeInfo(result.typeMetadata);
}
if (!("result" in result)) {
throw new Error("FileUpload result is empty!");
}

// if multiple files are allowed, we append to the collection
// if it's not, we replace the collection with the one new file
const newFiles = allowMultiple ? [...viewModel.state.Files as any, ...result.result] : result.result;
(viewModel as any).patchState({ Files: newFiles });

// call the handler
onCompleted();

(viewModel as any).patchState({ IsBusy: false });

} else {
(viewModel as any).patchState({ Error: "Upload failed.", IsBusy: false, Progress: 0 });
}
};

var formData = new FormData();
if (files.length > 1) {
for (var i = 0; i < files.length; i++) {
formData.append("upload[]", files[i]);
}
} else if (files.length > 0) {
formData.append("upload", files[0]);
}
viewModel.Progress(progress);
viewModel.IsBusy(isBusy);
xhr.send(formData);
}

1 change: 0 additions & 1 deletion src/Framework/Framework/Resources/Scripts/dotvvm-root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ const dotvvmExports = {
wrapObservable: evaluator.wrapObservable
},
fileUpload: {
reportProgress: fileUpload.reportProgress,
showUploadDialog: fileUpload.showUploadDialog
},
api: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,10 +297,10 @@ type CoerceErrorType = {
type CoerceResult = CoerceErrorType | { value: any, wasCoerced?: boolean, isError?: false };

type DotvvmFileUploadCollection = {
Files: KnockoutObservableArray<KnockoutObservable<DotvvmFileUploadData>>;
Progress: KnockoutObservable<number>;
Error: KnockoutObservable<string>;
IsBusy: KnockoutObservable<boolean>;
Files: DotvvmObservable<DotvvmObservable<DotvvmFileUploadData>[]>;
Progress: DotvvmObservable<number>;
Error: DotvvmObservable<string>;
IsBusy: DotvvmObservable<boolean>;
}
type DotvvmFileUploadData = {
FileId: KnockoutObservable<string>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DotVVM.Framework.Controls;
using DotVVM.Framework.Hosting;
using DotVVM.Framework.ViewModel;

namespace DotVVM.Samples.Common.ViewModels.ControlSamples.FileUpload
{
public class PasteDropViewModel : DotvvmViewModelBase
{
public string Text { get; set; }

public int FilesCount { get; set; }

public UploadedFilesCollection Files { get; set; } = new UploadedFilesCollection();


public void OnUploadCompleted()
{
FilesCount++;
}
}
}

Loading
Loading