236 lines
6.1 KiB
Dart
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),
|
|
),
|
|
);
|
|
}
|
|
}
|