From e393456e5663f430337ed6594226cd9fd896c505 Mon Sep 17 00:00:00 2001 From: TheNoumanDev Date: Mon, 14 Oct 2024 17:48:51 +0500 Subject: [PATCH 1/3] added onTap feature for chartJS --- .../lib/widget/visualization/chart_js.dart | 48 ++++++++++++++----- .../js_widget/lib/src/mobile/js_widget.dart | 16 ++++++- modules/js_widget/lib/src/web/js_widget.dart | 10 +++- 3 files changed, 61 insertions(+), 13 deletions(-) diff --git a/modules/ensemble/lib/widget/visualization/chart_js.dart b/modules/ensemble/lib/widget/visualization/chart_js.dart index f760c97e..49eee266 100644 --- a/modules/ensemble/lib/widget/visualization/chart_js.dart +++ b/modules/ensemble/lib/widget/visualization/chart_js.dart @@ -1,14 +1,15 @@ -import 'dart:io'; +import 'dart:convert'; import 'dart:math'; - +import 'package:ensemble/framework/action.dart'; +import 'package:ensemble/framework/event.dart'; +import 'package:ensemble/screen_controller.dart'; +import 'package:ensemble_ts_interpreter/parser/newjs_interpreter.dart'; +import 'package:flutter/material.dart'; +import 'package:js_widget/js_widget.dart'; import 'package:ensemble/framework/widget/widget.dart'; import 'package:ensemble/util/utils.dart'; import 'package:ensemble/widget/helpers/controllers.dart'; import 'package:ensemble_ts_interpreter/invokables/invokable.dart'; -import 'package:ensemble_ts_interpreter/parser/newjs_interpreter.dart'; -import 'package:flutter/material.dart'; -import 'package:js_widget/js_widget.dart'; -import 'dart:convert'; class ChartJsController extends WidgetController { ChartJsController() { @@ -21,6 +22,7 @@ class ChartJsController extends WidgetController { String get chartId => id!; dynamic config = ''; Function? evalScript; + EnsembleAction? onTap; } class ChartJs extends StatefulWidget @@ -125,7 +127,9 @@ class ChartJs extends StatefulWidget } else { _controller.config = value; } - } + }, + 'onTap': (funcDefinition) => _controller.onTap = + EnsembleAction.from(funcDefinition, initiator: this), }; } } @@ -161,15 +165,37 @@ class ChartJsState extends EWidgetState { id: widget.controller.id!, createHtmlTag: () => '
', - scriptToInstantiate: (String c) { - return 'if (typeof ${widget.controller.chartVar} !== "undefined") ${widget.controller.chartVar}.destroy();${widget.controller.chartVar} = new Chart(document.getElementById("${widget.controller.chartId}"), $c);${widget.controller.chartVar}.update();'; + scriptToInstantiate: (String config) { + return ''' + if (typeof ${widget.controller.chartVar} !== "undefined") { + ${widget.controller.chartVar}.destroy(); + } + ${widget.controller.chartVar} = new Chart(document.getElementById("${widget.controller.chartId}"), $config); + + // Add click event listener to the chart + document.getElementById("${widget.controller.chartId}").onclick = function(event) { + // Notify Flutter about the click event (no need to send data) + if (window.dispatchEvent) { + var eventDetail = new CustomEvent('callFlutter', { detail: 'onTap' }); + window.dispatchEvent(eventDetail); + } else { + console.log("Flutter handler not available"); + } + }; + ${widget.controller.chartVar}.update(); + '''; }, - size: Size(widget.controller.width.toDouble(), - widget.controller.height.toDouble()), + size: Size(widget.controller.width.toDouble(), widget.controller.height.toDouble()), data: widget.controller.config, scripts: const [ "https://cdn.jsdelivr.net/npm/chart.js", ], + listener: (msg) { + if (msg == 'onTap' && widget.controller.onTap != null) { + ScreenController().executeAction(context, widget.controller.onTap!, + event: EnsembleEvent(widget)); + } + }, ); return jsWidget!; } diff --git a/modules/js_widget/lib/src/mobile/js_widget.dart b/modules/js_widget/lib/src/mobile/js_widget.dart index a80bf3f5..fdf35b8c 100644 --- a/modules/js_widget/lib/src/mobile/js_widget.dart +++ b/modules/js_widget/lib/src/mobile/js_widget.dart @@ -1,3 +1,5 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher_string.dart'; // Import for Android features. @@ -138,7 +140,19 @@ class JsWidgetState extends State { fit: StackFit.expand, children: [ !_isLoaded ? widget.loader : const SizedBox.shrink(), - WebViewWidget(controller: controller), + WebViewWidget( + controller: controller, + gestureRecognizers: >{ + Factory( + () => TapGestureRecognizer() + ..onTapDown = (TapDownDetails details) { + if (widget.listener != null) { + widget.listener!('onTap'); // Notify on tap + } + }, + ), + }, + ), ], ), ); diff --git a/modules/js_widget/lib/src/web/js_widget.dart b/modules/js_widget/lib/src/web/js_widget.dart index 74f361e3..34cc56c3 100644 --- a/modules/js_widget/lib/src/web/js_widget.dart +++ b/modules/js_widget/lib/src/web/js_widget.dart @@ -100,7 +100,7 @@ class JsWidgetState extends State { void initState() { if (widget.listener != null) { addListener(widget.id, widget.listener!); - init(globalListener); + _setupFlutterWebCommunication(); } if (widget.preCreateScript != null) { eval(widget.preCreateScript!()); @@ -114,6 +114,14 @@ class JsWidgetState extends State { }); super.initState(); } + // Setup Flutter web communication for JS interactions + void _setupFlutterWebCommunication() { + html.window.addEventListener('callFlutter', (event) { + final customEvent = event as html.CustomEvent; + final msg = customEvent.detail; + globalListener(widget.id, msg); + }); + } @override void dispose() { From e00bff5d18aec4124fd4f25fefdef6e5cfad21b6 Mon Sep 17 00:00:00 2001 From: TheNoumanDev Date: Thu, 17 Oct 2024 19:18:15 +0500 Subject: [PATCH 2/3] added data to retrive on OnTap and removed customEvent --- .../lib/widget/visualization/chart_js.dart | 53 +++++++++++++++---- .../js_widget/lib/src/mobile/js_widget.dart | 43 ++++++++++----- modules/js_widget/lib/src/web/js_widget.dart | 43 +++++++-------- 3 files changed, 96 insertions(+), 43 deletions(-) diff --git a/modules/ensemble/lib/widget/visualization/chart_js.dart b/modules/ensemble/lib/widget/visualization/chart_js.dart index 49eee266..b2f9f3a7 100644 --- a/modules/ensemble/lib/widget/visualization/chart_js.dart +++ b/modules/ensemble/lib/widget/visualization/chart_js.dart @@ -171,17 +171,51 @@ class ChartJsState extends EWidgetState { ${widget.controller.chartVar}.destroy(); } ${widget.controller.chartVar} = new Chart(document.getElementById("${widget.controller.chartId}"), $config); - + // Add click event listener to the chart document.getElementById("${widget.controller.chartId}").onclick = function(event) { - // Notify Flutter about the click event (no need to send data) - if (window.dispatchEvent) { - var eventDetail = new CustomEvent('callFlutter', { detail: 'onTap' }); - window.dispatchEvent(eventDetail); - } else { - console.log("Flutter handler not available"); + var activePoints = ${widget.controller.chartVar}.getElementsAtEventForMode(event, 'nearest', { intersect: true }, true); + if (activePoints.length > 0) { + var firstPoint = activePoints[0]; + var datasetIndex = firstPoint.datasetIndex; + var index = firstPoint.index; + var dataset = ${widget.controller.chartVar}.data.datasets[datasetIndex] || {}; + var label = ${widget.controller.chartVar}.data.labels[index] || ''; + var value = dataset.data ? dataset.data[index] : ''; + var datasetLabel = dataset.label || ''; + var backgroundColor = dataset.backgroundColor || ''; + var borderColor = dataset.borderColor || ''; + var x = firstPoint.element.x || 0; + var y = firstPoint.element.y || 0; + var chartType = ${widget.controller.chartVar}.config.type || ''; + // Serialize options safely + var options = JSON.parse(JSON.stringify(${widget.controller.chartVar}.options, function(key, value) { + if (typeof value === 'function') { + return value.toString(); + } + return value; + })) || {}; + var data = { + label: label, + value: value, + datasetLabel: datasetLabel, + datasetIndex: datasetIndex, + index: index, + backgroundColor: backgroundColor, + borderColor: borderColor, + x: x, + y: y, + chartType: chartType, + options: options + }; + if (window.sendMessageToFlutter) { + window.sendMessageToFlutter(JSON.stringify(data)); + } else { + console.log("Flutter handler not available"); + } } }; + ${widget.controller.chartVar}.update(); '''; }, @@ -191,9 +225,10 @@ class ChartJsState extends EWidgetState { "https://cdn.jsdelivr.net/npm/chart.js", ], listener: (msg) { - if (msg == 'onTap' && widget.controller.onTap != null) { + if (widget.controller.onTap != null) { + Map data = jsonDecode(msg); ScreenController().executeAction(context, widget.controller.onTap!, - event: EnsembleEvent(widget)); + event: EnsembleEvent(widget, data: data)); } }, ); diff --git a/modules/js_widget/lib/src/mobile/js_widget.dart b/modules/js_widget/lib/src/mobile/js_widget.dart index fdf35b8c..abd07e24 100644 --- a/modules/js_widget/lib/src/mobile/js_widget.dart +++ b/modules/js_widget/lib/src/mobile/js_widget.dart @@ -77,6 +77,14 @@ class JsWidgetState extends State { controller = WebViewController.fromPlatformCreationParams(params) ..setBackgroundColor(Colors.transparent) ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..addJavaScriptChannel( + 'JsBridge', + onMessageReceived: (JavaScriptMessage message) { + if (widget.listener != null) { + widget.listener!(message.message); + } + }, + ) ..setNavigationDelegate( NavigationDelegate( onProgress: (int progress) { @@ -140,19 +148,19 @@ class JsWidgetState extends State { fit: StackFit.expand, children: [ !_isLoaded ? widget.loader : const SizedBox.shrink(), - WebViewWidget( - controller: controller, - gestureRecognizers: >{ - Factory( - () => TapGestureRecognizer() - ..onTapDown = (TapDownDetails details) { - if (widget.listener != null) { - widget.listener!('onTap'); // Notify on tap - } + widget.listener != null + ? WebViewWidget( + controller: controller, + gestureRecognizers: >{ + Factory( + () => TapGestureRecognizer() + ..onTapDown = (TapDownDetails details) { + // No need to manually handle tap here; JavaScript will send messages via JsBridge + }, + ), }, - ), - }, - ), + ) + : WebViewWidget(controller: controller), ], ), ); @@ -165,6 +173,14 @@ class JsWidgetState extends State { for (String src in widget.scripts) { html += ''; } + html += ''' + + '''; html += ''; return html; } @@ -174,7 +190,8 @@ class JsWidgetState extends State { _isLoaded = true; }); controller.runJavaScript(''' + ${widget.preCreateScript != null ? widget.preCreateScript!() : ''} ${widget.scriptToInstantiate(widget.data)} - '''); + '''); } } diff --git a/modules/js_widget/lib/src/web/js_widget.dart b/modules/js_widget/lib/src/web/js_widget.dart index 34cc56c3..43f21dd6 100644 --- a/modules/js_widget/lib/src/web/js_widget.dart +++ b/modules/js_widget/lib/src/web/js_widget.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:html' as html; +import 'dart:js' as js; // Import dart:js for interop import 'dart:math'; import 'dart:ui' as ui; @@ -27,12 +28,12 @@ class JsWidget extends StatefulWidget { ///Custom `loader` widget, until script is loaded /// - ///Has no effect on Web + /// Has no effect on Web /// - ///Defaults to `CircularProgressIndicator` + /// Defaults to `CircularProgressIndicator` final Widget loader; - ///Widget data + /// Widget data final String id; final Function scriptToInstantiate; final Function createHtmlTag; @@ -40,16 +41,16 @@ class JsWidget extends StatefulWidget { final String data; Function(String msg)? listener; - ///Widget size + /// Widget size /// - ///Height and width of the widget is required + /// Height and width of the widget is required /// - ///```dart - ///Size size = Size(400, 300); - ///``` + /// ```dart + /// Size size = Size(400, 300); + /// ``` final Size size; - ///Scripts to be loaded + /// Scripts to be loaded final List scripts; @override JsWidgetState createState() => JsWidgetState(); @@ -85,6 +86,13 @@ class JsWidgetState extends State { } } + void init(Function(String id, String msg) globalListener) { + // Expose the 'sendMessageToFlutter' function to JavaScript + js.context['sendMessageToFlutter'] = (dynamic msg) { + globalListener(widget.id, msg as String); + }; + } + @override void didUpdateWidget(covariant JsWidget oldWidget) { if (oldWidget.data != widget.data || @@ -98,9 +106,9 @@ class JsWidgetState extends State { @override void initState() { + init(globalListener); if (widget.listener != null) { addListener(widget.id, widget.listener!); - _setupFlutterWebCommunication(); } if (widget.preCreateScript != null) { eval(widget.preCreateScript!()); @@ -114,14 +122,6 @@ class JsWidgetState extends State { }); super.initState(); } - // Setup Flutter web communication for JS interactions - void _setupFlutterWebCommunication() { - html.window.addEventListener('callFlutter', (event) { - final customEvent = event as html.CustomEvent; - final msg = customEvent.detail; - globalListener(widget.id, msg); - }); - } @override void dispose() { @@ -132,9 +132,10 @@ class JsWidgetState extends State { @override Widget build(BuildContext context) { return SizedBox( - height: widget.size.height, - width: widget.size.width, - child: HtmlElementView(viewType: widget.id)); + height: widget.size.height, + width: widget.size.width, + child: HtmlElementView(viewType: widget.id), + ); } Future _load() { From 41161ebd3b1e21d8bdddd468aefea5f31f2a7ffc Mon Sep 17 00:00:00 2001 From: TheNoumanDev Date: Mon, 21 Oct 2024 18:02:38 +0500 Subject: [PATCH 3/3] remoded redundant code --- modules/js_widget/lib/src/mobile/js_widget.dart | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/modules/js_widget/lib/src/mobile/js_widget.dart b/modules/js_widget/lib/src/mobile/js_widget.dart index abd07e24..250e8258 100644 --- a/modules/js_widget/lib/src/mobile/js_widget.dart +++ b/modules/js_widget/lib/src/mobile/js_widget.dart @@ -148,19 +148,7 @@ class JsWidgetState extends State { fit: StackFit.expand, children: [ !_isLoaded ? widget.loader : const SizedBox.shrink(), - widget.listener != null - ? WebViewWidget( - controller: controller, - gestureRecognizers: >{ - Factory( - () => TapGestureRecognizer() - ..onTapDown = (TapDownDetails details) { - // No need to manually handle tap here; JavaScript will send messages via JsBridge - }, - ), - }, - ) - : WebViewWidget(controller: controller), + WebViewWidget(controller: controller), ], ), );