JavaScript

Dropzone sperren und freigeben

disableDropzone und enableDropzone blenden den Ablagebereich aus und wieder ein – die bereits hochgeladenen Dokumente bleiben sichtbar.

@model DropzoneStatusBeispielViewModel

@* Die beiden Schaltflächen tragen den Feldnamen als data-Attribut; welche Funktion daraus wird,
   entscheidet der Listener im Tab „JavaScript". Die alte Schreibweise mit
   onclick="disableDropzone('Kursbelege')" tut dasselbe, verlangt aber eine gelockerte
   Content-Security-Policy – deshalb hier die Fassung mit externem Skript. *@

<form asp-area="FileUpload" asp-controller="DropzoneStatusBeispiel" asp-action="Index" method="post">

    @Html.EditorFor(m => m.Kursbelege)

    <div class="btn-group btn-group-sm mt-2">
        @Html.Fmh(Fmh.Button()
            .Text("Sperren")
            .Type(ButtonType.Button)
            .Color(ButtonColor.Secondary)
            .Outline()
            .Attributes(new { data_dropzone = "sperren", data_dropzone_feld = "Kursbelege" }))
        @Html.Fmh(Fmh.Button()
            .Text("Freigeben")
            .Type(ButtonType.Button)
            .Color(ButtonColor.Secondary)
            .Outline()
            .Attributes(new { data_dropzone = "freigeben", data_dropzone_feld = "Kursbelege" }))
    </div>

    @Html.Fmh(Fmh.Button()
        .Text("Absenden")
        .Size(ButtonSize.Sm)
        .Attributes(new { @class = "mt-3 d-block" }))
</form>

@Html.PostScript(@<script src="~/js/demos/dropzone-status.js" asp-append-version="true"></script>)
using System.ComponentModel.DataAnnotations;
using FMH.Komponente.Basis.Ui.Validation.DataAnnotation;
using FMH.Komponente.FileUpload.Models;
using FMH.Komponente.Infrastruktur;

namespace FMH.Komponente.WebUI.Areas.FileUpload.Models
{
    /// <summary>
    /// Ausgangslage für «Dropzone sperren und freigeben». Die Schaltflächen des Beispiels sprechen
    /// das Feld über seinen Namen an, deshalb bleibt es ohne <c>[UniqueId]</c>: Der
    /// Eigenschaftsname entspricht damit 1:1 der Html-Id und ist im Skript vorhersagbar.
    /// </summary>
    public class DropzoneStatusBeispielViewModel
    {
        /// <summary>
        /// Die Dokumentliste, deren Ablagebereich das Beispiel schaltet.
        /// </summary>
        [UIHint(UiEditor.FileUpload.DokumentListe)]
        [Display(Name = "Kursbelege")]
        [AdditionalMetadata(UiMetadata.FileUpload.UploadUrl, "/FileUpload/DropzoneStatusBeispiel/Upload")]
        [AdditionalMetadata(UiMetadata.FileUpload.MaxFileSizeInMb, "1")]
        [AdditionalMetadata(UiMetadata.FileUpload.MaxFiles, "5")]
        [AdditionalMetadata(UiMetadata.FileUpload.DownloadActionName, "Download")]
        [AdditionalMetadata(UiMetadata.FileUpload.DownloadControllerName, "DropzoneStatusBeispiel")]
        public DokumentListeViewModel Kursbelege { get; set; } = new DokumentListeViewModel();
    }
}
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using FMH.Komponente.Basis.Erweiterungen;
using FMH.Komponente.Basis.Ui.Feedback.Alert;
using FMH.Komponente.FileUpload.Models;
using FMH.Komponente.WebUI.Areas.FileUpload.Models;
using Microsoft.AspNetCore.Mvc;

namespace FMH.Komponente.WebUI.Areas.FileUpload.Controllers.Beispiele
{
    /// <summary>
    /// Serverseitig unterscheidet sich dieses Beispiel nicht vom Grundfall, der Lerninhalt steht
    /// im Skript des Partials, das die Dropzone zur Laufzeit sperrt und wieder freigibt. Der
    /// Controller liefert nur die Ausgangslage samt Endpunkten, damit sich die freigegebene
    /// Dropzone auch benutzen lässt.
    /// </summary>
    [Area("FileUpload")]
    public class DropzoneStatusBeispielController : Controller
    {
        private static readonly ConcurrentDictionary<int, string> Dokumente =
            new ConcurrentDictionary<int, string>(
                new Dictionary<int, string> { [11] = "Kursbestaetigung.pdf" });

        private static int letzteId = 300;

        /// <summary>
        /// Auch die Doku-Seite zeigt die gefüllte Liste, deshalb öffentlich.
        /// </summary>
        /// <returns>Das Modell mit der Ausgangslage des Beispiels.</returns>
        public static DropzoneStatusBeispielViewModel ErstelleModell()
        {
            var modell = new DropzoneStatusBeispielViewModel();

            modell.Kursbelege.Dokumente = Dokumente
                .OrderBy(eintrag => eintrag.Key)
                .Select(eintrag => new DokumentEintrag
                                   {
                                       DokumentEintragId = eintrag.Key,
                                       Name = eintrag.Value,
                                       Groesse = 40000 + (eintrag.Key * 100),
                                   })
                .ToList();

            return modell;
        }

        /// <summary>
        /// Zeigt das Beispiel.
        /// </summary>
        /// <returns>Die Ansicht mit der Ausgangslage.</returns>
        [HttpGet]
        public IActionResult Index()
        {
            return this.View(ErstelleModell());
        }

        /// <summary>
        /// Nimmt das Formular entgegen und meldet zurück, wie viele Dokumente dazugekommen sind.
        /// </summary>
        /// <param name="modell">Das abgesendete Modell.</param>
        /// <returns>Weiterleitung auf die Ansicht (PRG).</returns>
        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult Index(DropzoneStatusBeispielViewModel modell)
        {
            var anzahlNeu = modell.Kursbelege.DokumentIds?.Length ?? 0;

            this.AddAlert(
                $"Formular übertragen – {anzahlNeu} neue(s) Dokument(e).",
                AlertType.Success,
                autoHide: true);

            return this.RedirectToAction(nameof(this.Index));
        }

        /// <summary>
        /// Der Upload-Endpunkt dieses Beispiels.
        /// </summary>
        /// <returns>Json mit der vergebenen Id.</returns>
        [HttpPost]
        public IActionResult Upload()
        {
            if (this.Request.Headers.ContainsKey("IsDropzoneRequest") == false || this.Request.Form.Files.Count == 0)
            {
                return this.Json(new { id = 0 });
            }

            var datei = this.Request.Form.Files[0];
            var bezeichnung = this.Request.Form["DateiBezeichnung"].ToString();
            var name = string.IsNullOrWhiteSpace(bezeichnung) ? datei.FileName : bezeichnung;

            var id = Interlocked.Increment(ref letzteId);
            Dokumente[id] = name;

            return this.Json(new { id });
        }

        /// <summary>
        /// Der Download-Endpunkt dieses Beispiels.
        /// </summary>
        /// <param name="id">Die Id des Dokuments aus der Tabellenzeile.</param>
        /// <returns>Die Datei zum Herunterladen.</returns>
        [HttpGet]
        public IActionResult Download(int id)
        {
            if (Dokumente.TryGetValue(id, out var name) == false)
            {
                return this.NotFound();
            }

            var inhalt = Encoding.UTF8.GetBytes($"Platzhalter-Inhalt für «{name}» (Id {id}).");

            return this.File(inhalt, "text/plain", name);
        }
    }
}
// Beispiel «Dropzone sperren und freigeben» (FileUpload).
//
// Die beiden Funktionen sind global und kommen aus jquery-fmh-fileupload.js der Komponente
// FileUpload. Sie erwarten den **Eigenschaftsnamen** des Feldes, nicht einen Selektor, ein
// führendes '#' wird zwar toleriert, gehört aber nicht hin.
//
// Gesperrt wird der Ablagebereich #dz-content-<Feldname>; an seine Stelle tritt der Hinweis
// #dz-content-inaktiv-<Feldname>, und die Schaltfläche für schmale Bildschirme wird deaktiviert.
var dropzoneFunktionen = {
    'sperren': disableDropzone,
    'freigeben': enableDropzone,
};

// Delegation an das document: Der Zeitpunkt des Renderns spielt damit keine Rolle.
document.addEventListener('click', function (ereignis) {
    var knopf = ereignis.target.closest('[data-dropzone]');

    if (knopf === null) {
        return;
    }

    var funktion = dropzoneFunktionen[knopf.dataset.dropzone];

    if (funktion) {
        funktion(knopf.dataset.dropzoneFeld);
    }
});
// Dieselbe Umsetzung mit jQuery. Diese Datei wird NICHT geladen, sie steht nur im Code-Tab
// «jQuery».
//
// Unterschied zur JavaScript-Fassung: die delegierte Bindung über $(document).on(...) statt
// addEventListener plus closest. Die beiden aufgerufenen Funktionen sind dieselben.
var dropzoneFunktionen = {
    'sperren': disableDropzone,
    'freigeben': enableDropzone,
};

$(document).on('click', '[data-dropzone]', function () {
    var funktion = dropzoneFunktionen[$(this).data('dropzone')];

    if (funktion) {
        funktion($(this).data('dropzone-feld'));
    }
});

Darstellung

Description Size Actions
Kursbestaetigung.pdf 40.14 KB
attach_file

Drag files here or click here to upload.

report_problem

You can not upload any more files.

Die Dropzone lässt sich zur Laufzeit sperren und wieder freigeben, etwa, wenn eine Auswahl weiter oben im Formular bestimmt, ob überhaupt Dokumente erlaubt sind.

Funktion Wirkung
disableDropzone(feldname) Blendet den Ablagebereich #dz-content-<feldname> aus, blendet den Hinweis #dz-content-inaktiv-<feldname> ein und deaktiviert die Schaltfläche .fileinput-button für schmale Bildschirme.
enableDropzone(feldname) Macht dasselbe rückgängig.
checkMaxUploadedFiles(feldname, id, maxDateien) Zählt die vorhandenen Dokumente und sperrt die Dropzone, sobald maxDateien erreicht ist. Ruft dabei die beiden oberen Funktionen auf und löst documentsuploaded aus. Das EditorTemplate DokumentListe ruft sie selbst auf; ein eigener Aufruf ist nur nötig, wenn die Anzahl an der Vorlage vorbei verändert wird.

Erwartet wird der Eigenschaftsname des Feldes, nicht ein Selektor. Ein führendes # oder . wird toleriert, gehört aber nicht hin. Trägt das Feld ein [UniqueId], ist der Name zur Laufzeit nicht mehr vorhersagbar, dann die tatsächlich gerenderte Id verwenden.

Die Funktionen liefert diese Komponente aus (jquery-fmh-fileupload.js, Bundle fmh-scripts). Eine Anwendung ohne FMH.Komponente.FileUpload hat sie nicht, sie hätte aber auch keine Dropzone, an der sie etwas ausrichten könnten.

Fehlerdarstellung

Die Fehlerdarstellung läuft nicht über eine eigene Funktion, sondern über das Element-Register der Basis. Die Komponente meldet dort einen Adapter dropzone an, der weiss, dass nicht das Feld selbst markiert wird, dort steht nur ein verstecktes Eingabefeld ,, sondern der Ablagebereich und seine Beschriftung:

fmh.elemente.registriere({
    name: "dropzone",
    trifft: function (element) { … },
    markiereFehler: function (element, hatFehler) { … }
});

Der bisherige Aufruf validationDropzoneCSS(feldname, isValid) bleibt unverändert gültig; er reicht seit dem Umzug nur noch ans Register weiter. Ohne geladene Komponente FileUpload ist kein Adapter angemeldet und der Aufruf bleibt wirkungslos, statt an fremdem Markup zu arbeiten.