enrollment-app/lib/presentation/email_template/grapes_email_editor.dart
2026-07-02 11:06:41 +05:30

236 lines
6.1 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:ui_web' as ui;
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:universal_html/html.dart' as html;
class GrapesEmailEditorController {
_GrapesEmailEditorState? _state;
void _bind(_GrapesEmailEditorState state) => _state = state;
void _unbind(_GrapesEmailEditorState state) {
if (_state == state) _state = null;
}
bool get isReady => _state?.isReady ?? false;
String get currentHtml => _state?.currentHtml ?? '';
Future<String> exportHtml() => _state?.exportHtml() ?? Future.value('');
void loadHtml(String html) => _state?.loadHtml(html);
void insertToken(String token) => _state?.insertToken(token);
}
class GrapesEmailEditor extends StatefulWidget {
final String html;
final GrapesEmailEditorController? controller;
final ValueChanged<String>? onHtmlChanged;
const GrapesEmailEditor({
super.key,
required this.html,
this.controller,
this.onHtmlChanged,
});
@override
State<GrapesEmailEditor> createState() => _GrapesEmailEditorState();
}
class _GrapesEmailEditorState extends State<GrapesEmailEditor> {
static int _instanceCounter = 0;
late final String _viewType;
html.IFrameElement? _iframe;
StreamSubscription<html.MessageEvent>? _messageSub;
bool _isReady = false;
bool _isDisposed = false;
String _currentHtml = '';
int _requestCounter = 0;
final Map<int, Completer<String>> _pendingExports = {};
bool get isReady => _isReady;
String get currentHtml => _currentHtml;
@override
void initState() {
super.initState();
_currentHtml = widget.html;
widget.controller?._bind(this);
_instanceCounter += 1;
_viewType = 'grapes-email-editor-$_instanceCounter';
if (kIsWeb) {
_registerView();
_messageSub = html.window.onMessage.listen(_onMessage);
}
}
@override
void dispose() {
_isDisposed = true;
_messageSub?.cancel();
_messageSub = null;
_pendingExports.clear();
try {
_postToEditor({'action': 'destroy'});
_iframe?.src = 'about:blank';
} catch (_) {}
widget.controller?._unbind(this);
super.dispose();
}
void _registerView() {
ui.platformViewRegistry.registerViewFactory(_viewType, (int viewId) {
final iframe = html.IFrameElement()
..src = 'email-editor.html'
..style.border = 'none'
..style.width = '100%'
..style.height = '100%'
..style.display = 'block';
_iframe = iframe;
return iframe;
});
}
void _postToEditor(Map<String, dynamic> payload) {
if (_isDisposed) return;
final win = _iframe?.contentWindow;
if (win == null) return;
try {
final message = jsonEncode({
'source': 'nhance-email-editor-host',
...payload,
});
(win as dynamic).postMessage(message, '*');
} catch (_) {}
}
Map<String, dynamic>? _readPayload(Object? data) {
if (data == null) return null;
if (data is String) {
try {
final decoded = jsonDecode(data);
if (decoded is Map) {
return Map<String, dynamic>.from(decoded);
}
} catch (_) {
return null;
}
}
if (data is Map) {
return Map<String, dynamic>.from(data);
}
try {
final dynamic raw = data;
final source = raw['source']?.toString();
if (source == null) return null;
return {
'source': source,
'action': raw['action']?.toString(),
'html': raw['html']?.toString(),
'requestId': raw['requestId'],
'message': raw['message']?.toString(),
};
} catch (_) {
return null;
}
}
void _onMessage(html.MessageEvent event) {
if (_isDisposed || !mounted) return;
Map<String, dynamic>? data;
try {
data = _readPayload(event.data);
} catch (_) {
return;
}
if (data == null || data['source'] != 'nhance-email-editor') return;
try {
switch (data['action']) {
case 'ready':
if (_isDisposed || !mounted) return;
_isReady = true;
loadHtml(_currentHtml);
break;
case 'change':
if (_isDisposed || !mounted) return;
final htmlContent = data['html']?.toString() ?? '';
if (htmlContent.isEmpty || htmlContent == _currentHtml) return;
_currentHtml = htmlContent;
widget.onHtmlChanged?.call(htmlContent);
break;
case 'html':
if (_isDisposed) return;
final requestId = int.tryParse(data['requestId']?.toString() ?? '');
if (requestId != null) {
_pendingExports.remove(requestId)?.complete(
data['html']?.toString() ?? '',
);
}
break;
}
} catch (_) {}
}
void loadHtml(String html) {
if (_isDisposed) return;
_currentHtml = html;
_postToEditor({'action': 'load', 'html': html});
}
Future<String> exportHtml() async {
if (_isDisposed) return _currentHtml;
if (!_isReady) return _currentHtml;
final requestId = ++_requestCounter;
final completer = Completer<String>();
_pendingExports[requestId] = completer;
_postToEditor({'action': 'export', 'requestId': requestId});
try {
final result = await completer.future.timeout(const Duration(seconds: 5));
if (!_isDisposed) _currentHtml = result;
return result;
} catch (_) {
return _currentHtml;
} finally {
_pendingExports.remove(requestId);
}
}
void insertToken(String token) {
_postToEditor({'action': 'insertToken', 'token': token});
}
@override
Widget build(BuildContext context) {
if (!kIsWeb) {
return Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(
'Drag-and-drop email editor is available on web.',
style: GoogleFonts.poppins(fontSize: 13, color: Colors.black54),
textAlign: TextAlign.center,
),
),
);
}
return ClipRect(
clipBehavior: Clip.hardEdge,
child: SizedBox.expand(
child: HtmlElementView(viewType: _viewType),
),
);
}
}