Skip to content

Instantly share code, notes, and snippets.

@PlugFox
Last active February 5, 2024 14:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save PlugFox/a88aa91ce1020d00a1028de7a02c56f7 to your computer and use it in GitHub Desktop.
Save PlugFox/a88aa91ce1020d00a1028de7a02c56f7 to your computer and use it in GitHub Desktop.
Flutter Web | Inline HTML | Shadow root | Resume builder
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:playground/src/resume_preview.dart';
void main() => runZonedGuarded<Future<void>>(
() async {
runApp(const App());
},
(error, stackTrace) =>
// ignore: avoid_print
print('Top level exception: $error\n$stackTrace'),
);
/// {@template app}
/// App widget.
/// {@endtemplate}
class App extends StatelessWidget {
/// {@macro app}
const App({super.key});
@override
Widget build(BuildContext context) => MaterialApp(
title: 'Resume Builder',
theme: ThemeData.dark(),
home: const WebViewScreen(),
);
}
/// {@template webview_app}
/// WebViewScreen widget.
/// {@endtemplate}
class WebViewScreen extends StatefulWidget {
/// {@macro webview_app}
const WebViewScreen({
super.key, // ignore: unused_element
});
/// Returns the [TextEditingController] from the closest [WebViewScreen] ancestor.
static TextEditingController of(BuildContext context, {bool listen = true}) =>
context.findAncestorStateOfType<_WebViewScreenState>()!.controller;
@override
State<WebViewScreen> createState() => _WebViewScreenState();
}
/// State for widget WebViewScreen.
class _WebViewScreenState extends State<WebViewScreen> {
bool _show = true;
double _size = 75;
bool get _isSizeMax => _size >= 100;
bool get _isSizeMin => _size <= 25;
final FocusNode focusNode = FocusNode();
final TextEditingController controller =
TextEditingController(text: 'John Doe');
void _updateSize(double value) => setState(() => _size = value);
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text(
'Resume Builder',
style: TextStyle(
color: Colors.white,
fontSize: 24,
),
),
backgroundColor: Colors.blueGrey,
centerTitle: true,
),
floatingActionButton: Padding(
padding: const EdgeInsets.only(bottom: 64),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
FloatingActionButton(
onPressed: () => setState(() => _show = !_show),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
transitionBuilder: (child, animation) => FadeTransition(
opacity: animation,
child: ScaleTransition(
scale: animation,
child: child,
),
),
child: _show
? const Icon(Icons.visibility_off, key: ValueKey(true))
: const Icon(Icons.visibility, key: ValueKey(false)),
),
),
const SizedBox(height: 8),
AnimatedOpacity(
duration: const Duration(milliseconds: 500),
opacity: _isSizeMax ? 0.25 : 1,
child: FloatingActionButton(
onPressed: _isSizeMax
? null
: () => _updateSize((_size + 25).clamp(25, 100)),
child: const Icon(Icons.add),
),
),
const SizedBox(height: 8),
AnimatedOpacity(
duration: const Duration(milliseconds: 500),
opacity: _isSizeMin ? 0.25 : 1,
child: FloatingActionButton(
onPressed: _isSizeMin
? null
: () => _updateSize((_size - 25).clamp(25, 100)),
child: const Icon(Icons.remove),
),
),
],
),
),
body: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16),
child: SizedBox(
height: 64,
width: 420,
child: TextFormField(
decoration: InputDecoration(
isCollapsed: false,
isDense: false,
floatingLabelBehavior: FloatingLabelBehavior.auto,
border: OutlineInputBorder(
borderRadius:
const BorderRadius.all(Radius.circular(12)),
borderSide: BorderSide(
width: 1,
color: Colors.blueGrey.shade200,
),
),
hoverColor: Colors.blueGrey.shade100,
labelText: 'Name',
hintText: 'Enter your name',
helperText: null,
suffixIcon: const Icon(Icons.search),
counter: const SizedBox.shrink(),
errorText: null,
helperMaxLines: 0,
errorMaxLines: 0,
),
controller: controller,
focusNode: focusNode,
maxLines: 1,
expands: false,
keyboardType: TextInputType.text,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
),
),
),
),
Expanded(
child: Theme(
data: ThemeData.light(),
child: Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: _show
? LayoutBuilder(
builder: (context, constraints) => SizedBox(
width: constraints.maxWidth * _size / 100,
height: constraints.maxHeight * _size / 100,
child: const Center(
child: AspectRatio(
aspectRatio: 1 / 1.414,
child: Card(
child: Padding(
padding: EdgeInsets.all(8.0),
child: ResumePreview(),
),
),
),
),
),
)
: const SizedBox.shrink(),
),
),
),
),
SizedBox(
height: 64,
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.blueGrey,
border: Border.all(color: Colors.black),
),
child: Center(
child: Text(
'Size: ${_size.toStringAsFixed(0)}%',
style: const TextStyle(
color: Colors.white,
fontSize: 24,
),
),
),
),
),
],
),
),
);
}
import 'package:flutter/material.dart';
import 'platform/resume_preview_js.dart'
// ignore: uri_does_not_exist
if (dart.library.io) 'platform/resume_preview_vm.dart';
/// {@template resume_preview}
/// ResumePreview widget.
/// {@endtemplate}
class ResumePreview extends StatelessWidget {
/// {@macro resume_preview}
const ResumePreview({
super.key, // ignore: unused_element
});
@override
Widget build(BuildContext context) => $buildResumePreview(context);
}
String get $cvInlineStyle => r'''
div.a4-wrapper {
display: flex;
width: 100%;
height: 100%;
border: none; /* 1px solid #ccc; */
position: relative;
box-shadow: none; /* 0 4px 6px rgba(0,0,0,0.1); */
justify-content: center;
transform-origin: top left;
transition: transform 0.3s ease;
overflow-y: auto;
overflow-x: hidden;
padding: 0px;
box-sizing: border-box;
font-family: Arial, sans-serif;
font-size: 16px;
line-height: 1.5;
color: #333;
/* scrollbar-width: thin;
scrollbar-color: #363636 #e7e7e7; */
}
div.a4-wrapper::-webkit-scrollbar {
width: 12px;
}
div.a4-wrapper::-webkit-scrollbar-track {
border-radius: 8px;
background-color: #e7e7e7;
border: 1px solid #cacaca;
box-shadow: inset 0 0 6px rgba(0, 0, 0, .3);
}
div.a4-wrapper::-webkit-scrollbar-thumb {
border-radius: 8px;
background-color: #363636;
}
div.a4-wrapper::-webkit-scrollbar-thumb:hover {
background: #555;
}
div.a4-content {
font-family: Arial, sans-serif;
width: 794px;
min-height: 1122px; /* A4 size */
box-sizing: border-box;
padding: 20px;
}
.header { text-align: center; }
.section { margin-top: 20px; }
.section-title { font-size: 24px; color: #333; }
.list { margin-top: 10px; }
.item { margin-bottom: 10px; }
''';
String get $cvInlineContent => r'''
<div class="header">
<h1 class="cv-template-name"> </h1>
<p>Email: alexey@designer.com | Phone: +7 999 123 45 67 | City: Moscow</p>
</div>
<div class="section">
<h2 class="section-title">Work Experience</h2>
<ul class="list">
<li class="item">
<strong>Senior Designer</strong> - Design Studio Alpha, Moscow (2020-2023)
<p>Developing concepts, creating visual solutions for clients, teamwork on projects.</p>
</li>
<li class="item">
<strong>Junior Designer</strong> - Creative Agency Beta, Saint Petersburg (2017-2020)
<p>Supporting design projects, creating layouts and prototypes, working with clients.</p>
</li>
</ul>
</div>
<div class="section">
<h2 class="section-title">Education</h2>
<ul class="list">
<li class="item">
<strong>Master of Design</strong> - Moscow State University of Design and Technology (2013-2017)
</li>
<li class="item">
<strong>Additional Courses</strong> - UX/UI Design, Skillbox (2018)
</li>
</ul>
</div>
<div class="section">
<h2 class="section-title">Lorem ipsum</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
</div>
<div class="section">
<h2 class="section-title">Lorem ipsum</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
</div>
<div class="section">
<h2 class="section-title">Lorem ipsum</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
</div>
<div class="section">
<h2 class="section-title">Lorem ipsum</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
</div>
<div class="section">
<h2 class="section-title">Lorem ipsum</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
</div>
<div class="section">
<h2 class="section-title">Skills</h2>
<ul class="list">
<li class="item">Adobe Photoshop, Adobe Illustrator</li>
<li class="item">Sketch, Figma</li>
<li class="item">Understanding of composition and typography principles</li>
<li class="item">User Interface Design and Web Design</li>
</ul>
</div>
''';
String get $cvInlineScript => r'''
console.log('Embedded CV script loaded');
''';
// ignore_for_file: avoid_web_libraries_in_flutter
import 'dart:html' as html;
import 'dart:ui_web' as ui;
import 'package:flutter/material.dart';
import 'package:playground/src/platform/resume_preview_content.dart';
import 'package:playground/main.dart';
Widget $buildResumePreview(BuildContext context) => const ResumePreviewJS();
/// {@template resume_preview_js}
/// ResumePreviewJS widget.
/// {@endtemplate}
class ResumePreviewJS extends StatefulWidget {
/// {@macro resume_preview_js}
const ResumePreviewJS({
super.key, // ignore: unused_element
});
@override
State<ResumePreviewJS> createState() => _ResumePreviewJSState();
}
class _ResumePreviewJSState extends State<ResumePreviewJS> {
late final TextEditingController _controller;
html.ShadowRoot? _shadowRoot;
@override
void initState() {
super.initState();
ui.platformViewRegistry.registerViewFactory(
'embedded-cv-view',
(int viewId) => html.DivElement()
..style.display = 'block'
..style.width = '100%' // 100% of parent (Scaffold)
..style.height = '100%' // 100% of parent (Scaffold)
..style.overflowY = 'hidden'
..style.overflowX = 'hidden'
..style.border = 'none'
..classes.addAll(['embedded-cv-view']),
isVisible: true,
);
_controller = WebViewScreen.of(context)..addListener(_onNameChanged);
html.window.console.log('ResumePreviewJS view factory registered');
}
void _appendShadowContent() {
final view = html.document.querySelector('.embedded-cv-view');
if (view == null) {
html.window.console.error('Embedded CV view not found');
return;
}
final sanitizer = html.NodeTreeSanitizer(
html.NodeValidatorBuilder.common()
..allowInlineStyles()
..allowElement('style', attributes: ['type'])
..allowElement('script', attributes: ['src']),
);
final content = html.DivElement()
..classes.addAll(['a4-wrapper', 'cv-shadow-container'])
..append(html.DivElement()
..classes.add('a4-content')
..appendHtml($cvInlineContent, treeSanitizer: sanitizer));
final style = html.StyleElement()..text = $cvInlineStyle;
final script = html.ScriptElement()..text = $cvInlineScript;
_shadowRoot = (view.shadowRoot ?? view.attachShadow({'mode': 'open'}))
..append(style)
..append(content)
..append(script);
_shadowRoot?.addEventListener('wheel', _onScrollEvent);
_onNameChanged();
html.window.console.log('Shadow content appended');
}
void _onNameChanged() {
final name = _controller.text.trim();
final nameTemplate =
_shadowRoot?.querySelector('.header > h1.cv-template-name');
if (nameTemplate == null) {
html.window.console.error('Name template not found');
return;
}
nameTemplate.text = name.trim();
html.window.console.log('Name changed: $name');
}
void _onScrollEvent(html.Event event) {
if (event is! html.WheelEvent) return;
final wrapper = _shadowRoot?.querySelector('.a4-wrapper');
if (wrapper == null) return;
event.preventDefault();
wrapper.scrollTop += event.deltaY.toInt();
}
@override
void dispose() {
_controller.removeListener(_onNameChanged);
_shadowRoot?.removeEventListener('wheel', _onScrollEvent);
super.dispose();
}
@override
Widget build(BuildContext context) => FittedBox(
alignment: Alignment.center,
clipBehavior: Clip.hardEdge,
fit: BoxFit.contain,
child: SizedBox(
width: 794,
height: 1122,
child: HtmlElementView(
viewType: 'embedded-cv-view',
onPlatformViewCreated: (_) => _appendShadowContent(),
),
),
);
}
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:playground/src/platform/resume_preview_content.dart';
import 'package:playground/main.dart';
import 'package:webview_flutter/webview_flutter.dart' as wv;
Widget $buildResumePreview(BuildContext context) => const ResumePreviewVM();
/// {@template resume_preview_vm}
/// ResumePreviewVM widget.
/// {@endtemplate}
class ResumePreviewVM extends StatefulWidget {
/// {@macro resume_preview_vm}
const ResumePreviewVM({
super.key, // ignore: unused_element
});
@override
State<ResumePreviewVM> createState() => _ResumePreviewVMState();
}
class _ResumePreviewVMState extends State<ResumePreviewVM> {
bool _contentLoaded = false;
late final wv.WebViewController _wvController;
late final TextEditingController _nameController;
@override
void initState() {
final buffer = StringBuffer()
..writeln('<html>')
..writeln('<style>')
..writeln($cvInlineStyle)
..writeln('</style>')
..writeln('<body>')
..writeln('<div class="a4-wrapper">')
..writeln('<div class="a4-content">')
..writeln($cvInlineContent)
..writeln('</div>')
..writeln('<script>')
..writeln($cvInlineScript)
..writeln('</script>')
..writeln('</div>')
..writeln('</body>')
..writeln('</html>');
_wvController = wv.WebViewController()
..setJavaScriptMode(wv.JavaScriptMode.unrestricted)
..setBackgroundColor(const Color(0x00000000))
..setOnConsoleMessage(print) // ignore: avoid_print
..enableZoom(false)
..setNavigationDelegate(wv.NavigationDelegate(
onNavigationRequest: (_) => wv.NavigationDecision.prevent))
..loadHtmlString(buffer.toString()).whenComplete(() {
_contentLoaded = true;
_onNameChanged();
});
_nameController = WebViewScreen.of(context)..addListener(_onNameChanged);
WidgetsBinding.instance.addPostFrameCallback((_) => _onNameChanged());
super.initState();
}
void _onNameChanged() {
if (!_contentLoaded || !mounted) return;
final name = _nameController.text.trim();
_wvController.runJavaScript(
'document.querySelector(".cv-template-name").textContent = "$name";');
}
@override
void dispose() {
_nameController.removeListener(_onNameChanged);
_wvController
..clearCache()
..clearLocalStorage();
super.dispose();
}
@override
Widget build(BuildContext context) => FittedBox(
alignment: Alignment.center,
clipBehavior: Clip.hardEdge,
fit: BoxFit.contain,
child: SizedBox(
width: 794,
height: 1122,
child: RepaintBoundary(
child: wv.WebViewWidget(
controller: _wvController,
layoutDirection: TextDirection.ltr,
gestureRecognizers: const <Factory<
OneSequenceGestureRecognizer>>{},
),
),
),
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment