Skip to content

Instantly share code, notes, and snippets.

@slightfoot
Last active February 1, 2021 21:04
Show Gist options
  • Save slightfoot/166d98aed7eb252079661da711966cd3 to your computer and use it in GitHub Desktop.
Save slightfoot/166d98aed7eb252079661da711966cd3 to your computer and use it in GitHub Desktop.
HTML Widgets for Flutter Web (IFrame and TextArea)- by Simon Lightfoot - 21/04/2020
// MIT License
//
// Copyright (c) 2020 Simon Lightfoot
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import 'dart:html' as html;
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart' show OneSequenceGestureRecognizer;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'
show PlatformViewController, SystemChannels;
final _widgetRef = <int, HtmlElementWidgetState>{};
abstract class HtmlElementWidget extends StatefulWidget {
const HtmlElementWidget({
Key key,
}) : super(key: key);
html.HtmlElement createHtmlElement(BuildContext context) {
throw UnimplementedError('createHtmlElement implementation required');
}
@override
HtmlElementWidgetState createState() => HtmlElementWidgetState();
}
class HtmlElementWidgetState<T extends HtmlElementWidget> extends State<T> {
static bool _registered = false;
@override
void initState() {
super.initState();
if (!_registered) {
registerPlatformView();
_registered = true;
}
}
html.HtmlElement createHtmlElement(BuildContext context) {
return widget.createHtmlElement(context);
}
void registerPlatformView() {
if (kIsWeb) {
// ignore: undefined_prefixed_name
ui.platformViewRegistry.registerViewFactory(
'HtmlElementWidget',
(int viewId) {
final state = _widgetRef[viewId];
assert(state != null);
return state.createHtmlElement(context);
},
);
}
}
@override
Widget build(BuildContext context) {
return PlatformViewLink(
viewType: 'HtmlElementWidget',
onCreatePlatformView: (PlatformViewCreationParams params) {
_widgetRef[params.id] = this;
final controller = _HtmlElementViewController(params);
controller._initialize().then((_) {
params.onPlatformViewCreated(params.id);
});
return controller;
},
surfaceFactory: (_, PlatformViewController controller) {
return PlatformViewSurface(
controller: controller,
gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
);
},
);
}
}
class _HtmlElementViewController extends PlatformViewController {
_HtmlElementViewController(this.params);
final PlatformViewCreationParams params;
@override
int get viewId => params.id;
bool _initialized = false;
Future<void> _initialize() async {
await SystemChannels.platform_views.invokeMethod<void>(
'create',
<String, dynamic>{'id': viewId, 'viewType': params.viewType},
);
_initialized = true;
}
@override
Future<void> clearFocus() async {
// Currently this does nothing on Flutter Web.
// TODO(het): Implement this. See https://github.com/flutter/flutter/issues/39496
}
@override
Future<void> dispatchPointerEvent(PointerEvent event) async {
// We do not dispatch pointer events to HTML views because they may contain
// cross-origin iframes, which only accept user-generated events.
}
@override
Future<void> dispose() async {
if (_initialized) {
// Asynchronously dispose this view.
SystemChannels.platform_views.invokeMethod<void>('dispose', viewId);
}
}
}
// MIT License
//
// Copyright (c) 2020 Simon Lightfoot
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import 'dart:html';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'html_element_widget.dart';
class IFrameWidget extends HtmlElementWidget {
const IFrameWidget({
Key key,
this.width,
this.height,
this.src,
this.style,
this.allow,
this.allowFullscreen,
}) : super(key: key);
final int width;
final int height;
final String src;
final String style;
final String allow;
final bool allowFullscreen;
@override
HtmlElement createHtmlElement(BuildContext context) {
return IFrameElement()
..width = width.toString()
..height = height.toString()
..src = src
..style.cssText = style
..allow = allow
..allowFullscreen = allowFullscreen;
}
}
// MIT License
//
// Copyright (c) 2020 Simon Lightfoot
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import 'package:flutter/gestures.dart' show PointerSignalEvent, PointerScrollEvent;
import 'dart:async';
import 'dart:html' as html;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'html_element_widget.dart';
class TextAreaWidget extends HtmlElementWidget {
const TextAreaWidget({
Key key,
this.initialValue = '',
this.wrap = 'soft',
this.style = 'border: 0; outline: none; resize: none;',
this.autofocus,
this.onChanged,
}) : super(key: key);
final String initialValue;
final String wrap;
final String style;
final bool autofocus;
final ValueChanged<String> onChanged;
@override
HtmlElementWidgetState<HtmlElementWidget> createState() {
return TextAreaWidgetState();
}
}
class TextAreaWidgetState extends HtmlElementWidgetState<TextAreaWidget> {
html.TextAreaElement _element;
StreamSubscription<html.Event> _onInputSub;
@override
html.HtmlElement createHtmlElement(BuildContext context) => _element;
String get text => _element?.text;
set text(String text) => _element?.text = text;
@override
void initState() {
super.initState();
_element = html.TextAreaElement()
..wrap = widget.wrap
..value = widget.initialValue
..autofocus = widget.autofocus
..style.cssText = widget.style;
_onInputSub = _element.onInput.listen(_onInput);
}
@override
void didUpdateWidget(TextAreaWidget oldWidget) {
super.didUpdateWidget(oldWidget);
_element
..wrap = widget.wrap
..style.cssText = widget.style;
}
void _onInput(html.Event event) => widget.onChanged?.call(_element.value);
void _onPointerSignal(PointerSignalEvent event) {
if(event is PointerScrollEvent) {
final delta = event.scrollDelta;
_element.scrollBy(delta.dx, delta.dy);
}
}
@override
Widget build(BuildContext context) {
return Listener(
onPointerSignal: _onPointerSignal,
child: super.build(context),
);
}
@override
void dispose() {
_onInputSub.cancel();
_element = null;
super.dispose();
}
}
@slightfoot
Copy link
Author

slightfoot commented Jun 22, 2020

Example:

const IFrameWidget(
	width: 640,
	height: 360,
	src: 'https://www.youtube.com/embed/2zwEdDoPvnc',
	style: 'border: 0;',
	allow: 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture',
	allowFullscreen: true,
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment