FileMaker WebViewer als Portal-Ersatz — mit eingebetteten Bildern und einer Base64-Falle
ProduktX-Karten mit Bildstreifen, Lightbox und Aktiv-Markierung — komplett in Formelfeldern. Und warum ¶ allein nicht reicht.
Wer kennt das nicht: Ein schoen aufgebautes Layout, ein Portal mit Produktdaten und dann kommt die Anforderung, pro Portal-Zeile ein Bild anzuzeigen. Ja das ist ja kein Problem, aber in der neuen Datenbank gibt es die Möglichkeit unendlich viele Bilder pro Produkt zu hinterlegen. Nehme ich dann nur das erste Bild das über die Referenz gefunden wird? Nein, dafür betreiben wir doch den Aufwand nicht. Spatestens dann stoesst man an die Grenze: Ein WebViewer laesst sich nicht in einen Ausschnitt (Portal) einbetten. Punkt.
Was tun? Wir haben den WebViewer aus dem Portal herausgeloest und das Portal komplett im WebViewer nachgebaut. Das Ergebnis ist ein vollwertiger Portal-Ersatz mit ProduktX-Karten, Bildstreifen, Lightbox und Klick-Rueckkanal zu FileMaker. Und das Ganze laeuft ohne Script-Trigger, komplett ueber Formelfelder.
Das Problem: Portal kann keinen WebViewer enthalten
In der Datenbank des Kunden gibt es eine Tabelle _Produkte_X — das sind konkrete Produktausführungen eines Angebots, z.B. “Jägerzaun aus Aluminium” oder “Balkongelaender mit Glasfuellung”. Jedes dieser ProduktX-Objekte gehoert zu einem Angebot und soll mit Fotos aus einer verknuepften _Documents-Tabelle angezeigt werden.
Das bestehende Layout hatte ein Portal T05_angebot_PRODUKTE_X||fk_angebot_id, das die Produkte auflistete. Ein WebViewer fuer die Bilder passte da einfach nicht rein, FileMaker erlaubt das nicht.
Die Loesung: Den WebViewer ausserhalb des Portals platzieren und den gesamten Portalinhalt als HTML darin nachbauen.
Architektur: Zwei Ebenen, zwei Formelfelder
Das Muster ist das gleiche wie beim bereits vorhandenen wv_html_artikel_teile im Projekt:
T05_ANGEBOT::wv_html_produkt_x
└── Liste ( T05_angebot_PRODUKTE_X||fk_angebot_id::_c_wv_zeile )
└── jede Zeile: While() ueber
Liste ( T05_angebot_PRODUKTE_X_DOCUMENTS||fk_produkt_id::c_base64_clean )
Formelfeld 1: _Produkte_X::_c_wv_zeile
Ungespeichert. Baut fuer einen ProduktX-Datensatz eine komplette HTML-Karte zusammen — Kopfzeile mit Produktname, Bildstreifen aus verknuepften _Documents, JS-Daten fuer die Lightbox. Das Ergebnis ist eine einzige, lange Zeile ohne ¶ (dazu gleich mehr).
Formelfeld 2: T05_ANGEBOT::wv_html_produkt_x
Ungespeichert. Holt per Liste() alle _c_wv_zeile-Werte der verknuepften ProduktX-Datensaetze, fuegt CSS-Header und JavaScript-Block davor, markiert den aktiven Datensatz per Substitute() und gibt fertiges HTML zurueck. Der WebViewer zeigt dieses Feld direkt an.
Die Karte — was eine Zeile enthaelt
Jede Produktkarte besteht aus:
- Header-Zeile: Pfeil-Icon, Produktname, Badge “aktiv” oder inaktiv — komplett klickbar
- Bildstreifen: Horizontale Reihe von Thumbnails (70×52 px,
object-fit: cover) - Fallback: “Noch keine Bilder hinterlegt” wenn kein Bild vorhanden
- Script-Block: JS-Objekt mit allen Bild-URLs fuer die Lightbox
SetzeVar ( [
~id = _pk_produkt_x_id ;
~name = Substitute ( t_produktbeschreibung ; [ "<" ; "<" ] ; ... ) ;
~b64s = Liste ( DOCUMENTS_TO::c_base64_clean ) ;
~caps = Liste ( DOCUMENTS_TO::t_bildkennung ) ;
~anz = ElementeAnzahl ( ~b64s ) ;
~thumbs = While (
[ ~i = 1 ; ~h = "" ] ;
~i <= ~anz ;
[
~b64 = HoleWert ( ~b64s ; ~i ) ;
~h = ~h & "<div class='thumb'>
<img src='data:image/jpeg;base64," & ~b64 & "' />
</div>" ;
~i = ~i + 1
] ;
~h
)
] ;
"<div class='produkt inaktiv' data-id='" & ~id & "'>"
& "<div class='produkt-header' onclick='wahle(""" & ~id & """)'>"
& ~name & "</div>"
& "<div class='bild-streifen'>" & ~thumbs & "</div>"
& "</div>"
)
Wichtig: Das Ergebnis darf kein ¶ enthalten. Liste() im uebergeordneten Formelfeld trennt seine Werte mit ¶. Ein ¶ innerhalb einer Karte wuerde von HoleWert() als Zeilenende interpretiert und die Karte zerschneiden.
Die Aktiv-Markierung per Substitute
Das uebergeordnete Feld wv_html_produkt_x weiss, welches Produkt gerade aktiv ist aus dem Systemfeld T05_ANGEBOT::_sys_Produkt_ID_X. Statt die Klasse bereits in _c_wv_zeile zu setzen (was kompliziert waere, da das Feld keinen Kontext zum uebergeordneten Datensatz hat), tauschen wir die Klasse erst im uebergeordneten Feld per Substitute():
~zeile = Substitute ( ~zeile ;
"class='produkt inaktiv' data-id='" & ~aktivID & "'" ;
"class='produkt aktiv' data-id='" & ~aktivID & "'"
) ;
Der Trick: _c_wv_zeile schreibt immer inaktiv. Das uebergeordnete Feld sucht nach der Kombination aus Klasse und der aktiven UUID und tauscht genau diese eine Stelle aus. CSS erledigt den Rest — Gold fuer aktiv, Grau fuer inaktiv.
Der Klick-Rueckkanal: FileMaker.PerformScript()
Wenn der Benutzer eine Produktkarte anklickt, soll FileMaker den entsprechenden Datensatz als aktiv setzen. Das erledigt `FileMaker.PerformScript(), seit FileMaker 19 aufrufbar direkt aus JavaScript im WebViewer:
function wahle(id) {
if (typeof FileMaker !== 'undefined') {
FileMaker.PerformScript('ProduktX wahlen', id);
}
}
Das Script setzt _sys_Produkt_ID_X auf die uebergebene UUID, FileMaker berechnet wv_html_produkt_x neu, der WebViewer zeigt den aktualisierten Zustand, mit der neuen Karte in Gold.
Hinweis zu Umlauten in Script-Namen: Scripte, die aus einem WebViewer heraus per FileMaker.PerformScript() aufgerufen werden, sollten keine Umlaute im Namen enthalten. Ein separater WebViewer-Script-Ordner hilft dabei, diese Scripts organisiert zu halten.
Die Base64-Falle: Char(10) ist nicht Char(13)
Das war die interessanteste Debugging-Session des Projekts.
Das Formelfeld c_base64_clean in _Documents soll das Container-Bild als sauberen Base64-String zurueckgeben, bereinigt von allen Zeilenumbruechen, damit Liste() und HoleWert() sauber funktionieren. Erste Version der Formel:
Substitute ( Base64Encode ( cnt_image ) ; "¶" ; "" )
Das sah richtig aus. Und trotzdem: Im WebViewer erschienen statt 2 Bildern 309 Thumbnails , alle defekt, alle als Fragezeichen dargestellt. Der Zaehler in der Lightbox zeigte “247 / 309”.
Fehlersuche
Das WebViewer-HTML wurde als Textdatei exportiert und analysiert. Befund: Fuer das Produkt “Gartenzaun aus Aluminium” waren tatsaechlich 309 <div class='thumb'> im HTML vorhanden obwohl nur 2 Dokumente mit der richtigen FK verknuepft waren.
Die Ursache lag in ElementeAnzahl(~b64s). Dieser Aufruf zaehlt die Werte in ~b64s, das per Liste() aus den verknuepften _Documents zusammengebaut wurde. Die Anzahl haette 2 sein muessen — war aber 309.
Das konnte nur bedeuten: Der Base64-String enthielt noch Zeilenumbrueche, die Liste() als eigenstaendige Listenwerte interpretierte.
Warum "¶" nicht reichte
In FileMaker entspricht ¶ dem Zeichen Char(13) — dem klassischen Carriage Return. Liste() verwendet Char(13) als Trennzeichen zwischen Datensaetzen.
Base64Encode() erzeugt auf macOS jedoch Char(10) als Zeilenumbruch — den Unix Line Feed. Diese beiden Zeichen sehen im Formeleditor gleich aus, sind aber unterschiedlich.
Substitute ( Base64 ; "¶" ; "" ) entfernte also Char(13) — aber Char(10) blieb im String. Und da Liste() nun zwei Base64-Strings (einen pro Dokument) mit Char(13) verbindet und HoleWert() / ElementeAnzahl() ebenfalls Char(13) als Trennzeichen nutzen, wurden die eingebetteten Char(10) zwar nicht als Trennzeichen gezaehlt — aber die Problematik lag tiefer:
Der eigentliche Mechanismus war subtiler: Der String aus Liste() enthielt Char(10) innerhalb der Base64-Werte. ElementeAnzahl() zaehlt Char(13)-getrennte Werte. Wenn aber die urspruenglichen Base64-Strings noch Char(13) enthielten (nicht entfernt weil auf diesem System nur Char(10) erzeugt wird und man deshalb flschlicherweise nur Char(13) zu entfernen versucht hatte)… kurz: auf bestimmten Plattformen/Versionen erzeugt Base64Encode() CRLF-Umbrueche (Char(13) & Char(10)), auf anderen nur Char(10).
Die sichere, plattformunabhaengige Loesung:
Wenn (
IstLeer ( cnt_image ) ;
"" ;
Substitute (
Base64Encode ( cnt_image ) ;
[ Char(13) ; "" ] ;
[ Char(10) ; "" ]
)
)
Beide Zeichen explizit entfernen. Dazu den Leercontainer-Check: Base64Encode() auf ein leeres Container-Feld gibt keinen wirklich leeren String zurueck — Liste() wuerde sonst leere Werte mitzaehlen.
Nach dieser Korrektur und einem “Alle Datensaetze ersetzen → Feldinhalt neu berechnen”: 2 Thumbnails, sauber, korrekt.
Die Regel
¶=Char(13), aberBase64Encode()erzeugtChar(10). Immer BEIDE entfernen.
Das gilt ueberall im Projekt wo Base64-Strings per Liste() gebuendelt und per HoleWert() wieder aufgespalten werden.
SetzeVar mit einem Argument zu viel
Ein weiterer Stolperstein war die Struktur des SetzeVar-Aufrufs. SetzeVar (= Let()) akzeptiert genau zwei Argumente: die Variablenliste und den Ausdruck. Drei Argumente — also zwei While()-Schleifen als separate Argumente statt als verkettete Ausdruecke fuehren dazu, dass FileMaker die Formel nicht korrekt parst und Feldreferenzen nicht aufloesen kann. Symptom: irreführende Fehlermeldung “Tabelle ohne Beziehung”.
Die korrekte Struktur: alle Variablen in eine einzige Liste, die beiden While()-Schleifen per & im Ausdruck verkettet:
SetzeVar ( [
~var1 = ... ;
~var2 = ... ;
~loop1 = While ( ... ) ; // ← auch While() kann Variable sein
~loop2 = While ( ... )
] ;
~loop1 & ~loop2 // ← Ausdruck
)
Ergebnis
Der fertige WebViewer ersetzt das Portal vollstaendig:
- ProduktX-Karten mit Name und Bildstreifen
- Aktiv-Karte in Gold, inaktive Karten in Grau
- Thumbnails klickbar → Lightbox mit Vor/Zurueck und Tastaturnavigation
- Klick auf Karte →
FileMaker.PerformScript()→ Systemfeld wird gesetzt - Kein Script-Trigger noetig, kein manuelles HTML-Zusammenbauen per Script
- Vollstaendig offline, kein CDN, kein externes Framework
Die Loesung skaliert direkt mit den Daten: Neue ProduktX-Eintraege erscheinen automatisch, neue Bilder ebenfalls — sobald c_base64_clean berechnet ist.