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:nhancepolicy/presentation/email_template/email_template_models.dart'; import 'package:universal_html/html.dart' as html; const _caretSelector = '[data-nhance-caret="1"]'; class EmailHtmlPreviewController { _EmailHtmlPreviewState? _state; void _bind(_EmailHtmlPreviewState state) => _state = state; void _unbind(_EmailHtmlPreviewState state) { if (_state == state) _state = null; } bool get isReady => _state?.isReady ?? false; bool get hasCaretMarker => _state?.hasCaretMarker ?? false; String get currentHtml => _state?.currentHtml ?? ''; void removeCaretMarker() => _state?.removeCaretMarker(); bool insertSnippetAtCursor(String snippet) => _state?.insertSnippetAtCursor(snippet) ?? false; } class EmailHtmlPreview extends StatefulWidget { final String html; final double? height; final bool editable; final EmailHtmlPreviewController? controller; const EmailHtmlPreview({ super.key, required this.html, this.height, this.editable = false, this.controller, }); @override State createState() => _EmailHtmlPreviewState(); } class _EmailHtmlPreviewState extends State { late final String _viewType; html.IFrameElement? _iframe; late String _activeHtml; bool _iframeReady = false; bool _selectionListenersAttached = false; bool get isReady => _iframeReady; bool get hasCaretMarker { final doc = _iframeDocument; if (doc == null) return false; return doc.querySelector(_caretSelector) != null; } html.Document? get _iframeDocument { final iframe = _iframe; if (iframe == null) return null; try { return (iframe as dynamic).contentDocument as html.Document?; } catch (_) { return null; } } html.Element? get _iframeBody { final doc = _iframeDocument; if (doc == null) return null; return (doc as dynamic).body as html.Element?; } dynamic get _iframeWindow { final iframe = _iframe; if (iframe == null) return null; return (iframe as dynamic).contentWindow; } String get currentHtml { removeCaretMarker(); try { final root = _iframeDocument?.documentElement; if (root != null) { return '\n${root.outerHtml}'; } } catch (_) {} return _stripCaretMarkers(_activeHtml); } @override void initState() { super.initState(); _activeHtml = _stripCaretMarkers(widget.html); widget.controller?._bind(this); _viewType = 'email-preview-${identityHashCode(this)}-${DateTime.now().microsecondsSinceEpoch}'; if (kIsWeb) { _registerView(); } } @override void dispose() { widget.controller?._unbind(this); super.dispose(); } String _srcForIframe(String htmlContent) { final clean = _stripCaretMarkers(htmlContent); if (!widget.editable) return clean; return _injectEditableBody(clean); } String _injectEditableBody(String htmlContent) { final bodyTag = RegExp(r']*)>', caseSensitive: false); if (!bodyTag.hasMatch(htmlContent)) return htmlContent; return htmlContent.replaceFirstMapped(bodyTag, (match) { final attrs = match.group(1) ?? ''; if (RegExp('contenteditable', caseSensitive: false).hasMatch(attrs)) { return match.group(0)!; } return ''; }); } String _stripCaretMarkers(String htmlContent) { return htmlContent.replaceAll( RegExp( r']*data-nhance-caret="1"[^>]*>.*?', caseSensitive: false, dotAll: true, ), '', ); } void _registerView() { ui.platformViewRegistry.registerViewFactory(_viewType, (int viewId) { final iframe = html.IFrameElement() ..style.border = 'none' ..style.width = '100%' ..style.height = '100%' ..style.display = 'block' ..style.pointerEvents = 'auto' ..srcdoc = _srcForIframe(_activeHtml); if (!widget.editable) { iframe.setAttribute('sandbox', 'allow-same-origin'); } iframe.onLoad.listen((_) { _iframeReady = true; _selectionListenersAttached = false; _setupEditableIframe(); }); _iframe = iframe; return iframe; }); } void _setupEditableIframe() { if (!widget.editable || _selectionListenersAttached) return; final body = _iframeBody; if (body == null) return; body.setAttribute('contenteditable', 'true'); body.style.setProperty('outline', 'none'); body.style.setProperty('cursor', 'text'); // Place an invisible DOM marker at the click position so inserts survive // focus moving to the sidebar. body.onMouseUp.listen((_) => _placeCaretMarker()); body.onKeyUp.listen((_) => _placeCaretMarker()); body.onClick.listen((_) => _placeCaretMarker()); _selectionListenersAttached = true; } html.Element _createCaretMarker(html.Document doc) { final marker = doc.createElement('span'); marker.setAttribute('data-nhance-caret', '1'); marker.text = '\u200B'; marker.style.setProperty('font-size', '0'); marker.style.setProperty('line-height', '0'); marker.style.setProperty('display', 'inline'); return marker; } void _placeCaretMarker() { if (!widget.editable || !_iframeReady) return; final doc = _iframeDocument; final body = _iframeBody; if (doc == null || body == null) return; removeCaretMarker(); final range = _readSelectionRange(body); if (range == null) return; try { final marker = _createCaretMarker(doc); range.insertNode(marker); (range as dynamic).setStartAfter(marker); (range as dynamic).collapse(true); final win = _iframeWindow; final sel = win?.getSelection() as html.Selection?; sel?.removeAllRanges(); sel?.addRange(range); } catch (_) {} } dynamic _readSelectionRange(html.Element body) { try { final win = _iframeWindow; final sel = win?.getSelection() as html.Selection?; if (sel == null || (sel.rangeCount ?? 0) == 0) return null; final range = sel.getRangeAt(0); if (!_nodeInside(body, range.startContainer)) return null; return (range as dynamic).cloneRange(); } catch (_) { return null; } } void removeCaretMarker() { final doc = _iframeDocument; if (doc == null) return; final markers = doc.querySelectorAll(_caretSelector).toList(); for (final marker in markers) { marker.remove(); } } bool insertSnippetAtCursor(String snippet) { if (!widget.editable || !_iframeReady || snippet.trim().isEmpty) { return false; } final doc = _iframeDocument; if (doc == null) return false; final marker = doc.querySelector(_caretSelector); if (marker == null || marker.parent == null) return false; try { final parent = marker.parent!; final tempRange = doc.createRange(); final fragment = (tempRange as dynamic).createContextualFragment(snippet); if (fragment == null) return false; parent.insertBefore(fragment, marker); marker.remove(); _activeHtml = currentHtml; return true; } catch (_) { return false; } } bool _nodeInside(html.Node ancestor, html.Node node) { var current = node; while (true) { if (current == ancestor) return true; final parent = current.parentNode; if (parent == null) return false; current = parent; } } @override Widget build(BuildContext context) { final preview = kIsWeb ? ClipRect( clipBehavior: Clip.hardEdge, child: SizedBox.expand( child: HtmlElementView(viewType: _viewType), ), ) : Center( child: Padding( padding: const EdgeInsets.all(16), child: Text( widget.editable ? 'Editable email preview is available on web.' : 'Email preview is available on web.', style: GoogleFonts.poppins(fontSize: 13, color: Colors.black54), textAlign: TextAlign.center, ), ), ); final decorated = Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(4), border: Border.all(color: const Color(0xFFE5E7EB)), ), clipBehavior: Clip.hardEdge, child: preview, ); if (widget.height != null) { return SizedBox(height: widget.height, child: decorated); } return decorated; } } String appendHtmlSnippet(String html, String snippet) { if (html.trim().isEmpty) { return EmailTemplateData.wrapPlainTextAsHtml(snippet); } final lower = html.toLowerCase(); const footerMarkers = [ '