Merge pull request #2 from JideGuru/editor-settings-and-state

Added editor specific settings, started working on editor button state and added 'Add Video' Feature
This commit is contained in:
Festus Olusegun 2021-06-06 21:12:55 +01:00 committed by GitHub
commit f473048edb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1149 additions and 888 deletions

208
LICENSE
View File

@ -1,13 +1,201 @@
Copyright 2021 JideGuru
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
http://www.apache.org/licenses/LICENSE-2.0
1. Definitions.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2021 JideGuru
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,4 +1,5 @@
# ✨ rich_editor
[![pub package](https://img.shields.io/pub/v/rich_editor.svg)](https://pub.dartlang.org/packages/rich_editor)
[![pub points](https://badges.bar/rich_editor/pub%20points)](https://pub.dev/packages/rich_editor/score)
@ -7,6 +8,7 @@ WYSIWYG editor for Flutter with a rich set of supported formatting options.
Based on https://github.com/dankito/RichTextEditor, but for Flutter.
## ✨ Features
- [x] Bold, Italic, Underline, Strike through, Subscript, Superscript
- [x] Heading 1 - 6, Text body, Preformatted, Block quote
- [x] Font (reads all system fonts) (Android only)
@ -26,14 +28,28 @@ Based on https://github.com/dankito/RichTextEditor, but for Flutter.
- [ ] Icon indicators
## 📸 Screenshots
<img src="https://github.com/JideGuru/rich_editor/raw/master/res/1.png" width="400"> <img src="https://github.com/JideGuru/rich_editor/raw/master/res/2.png" width="400">
<img src="https://github.com/JideGuru/rich_editor/raw/master/res/1.png" width="400">
<img src="https://github.com/JideGuru/rich_editor/raw/master/res/2.png" width="400">
## Usage
```dart
// Insert widget into tree
RichEditor(
key: keyEditor,
value: 'initial html here',
editorOptions: RichEditorOptions(
placeholder: 'Start typing',
// backgroundColor: Colors.blueGrey, // Editor's bg color
// baseTextColor: Colors.white,
// editor padding
padding: EdgeInsets.symmetric(horizontal: 5.0),
// font name
baseFontFamily: 'sans-serif',
// Position of the editing bar (BarPosition.TOP or BarPosition.BOTTOM)
barPosition: BarPosition.TOP,
),
// You can return a Link (maybe you need to upload the image to your
// storage before displaying in the editor or you can also use base64
getImageUrl: (image) {
@ -46,22 +62,30 @@ Based on https://github.com/dankito/RichTextEditor, but for Flutter.
```
Get current HTML from editor
```dart
String? html = await keyEditor.currentState?.getHtml();
print(html);
```
Set Focus and Unfocus
```dart
await keyEditor.currentState?.focus();
await keyEditor.currentState?.unFocus();
```
Clear Editor content
```dart
await keyEditor.currentState?.clear();
```
### Custom Toolbar
If you're interested in creating your own toolbar check the
custom_toolbar_demo.dart in the example
## License
Copyright 2021 JideGuru
@ -76,4 +100,4 @@ await keyEditor.currentState?.clear();
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
limitations under the License.

View File

@ -474,6 +474,25 @@ var editor = {
}
},
insertVideo: function(url, width, height, fromDevice) {
console.log(url);
if (fromDevice) {
this._insertVideo(url, width, height);
} else {
this._insertYoutubeVideo(url, width, height);
}
},
_insertYoutubeVideo: function(url, width, height) {
var html = '<iframe width="'+ width +'" height="'+ height +'" src="' + url + '" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"></iframe>';
this._insertHtml(html);
},
_insertVideo: function(url, width, height) {
var html = '<video width="'+ width +'" height="'+ height +'" controls><source type="video/mp4" src="'+ url +'"></video>'
this._insertHtml(html);
},
insertCheckbox: function(text) {
var editor = this;

View File

@ -1,39 +1,38 @@
PODS:
- Flutter (1.0.0)
- flutter_inappwebview (0.0.1):
- Flutter
- flutter_inappwebview/Core (= 0.0.1)
- OrderedSet (~> 5.0)
- flutter_inappwebview/Core (0.0.1):
- Flutter
- OrderedSet (~> 5.0)
- image_picker (0.0.1):
- Flutter
- video_player (0.0.1):
- Flutter
- wakelock (0.0.1):
- Flutter
- webview_flutter (0.0.1):
- Flutter
- OrderedSet (5.0.0)
DEPENDENCIES:
- Flutter (from `Flutter`)
- flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`)
- image_picker (from `.symlinks/plugins/image_picker/ios`)
- video_player (from `.symlinks/plugins/video_player/ios`)
- wakelock (from `.symlinks/plugins/wakelock/ios`)
- webview_flutter (from `.symlinks/plugins/webview_flutter/ios`)
SPEC REPOS:
trunk:
- OrderedSet
EXTERNAL SOURCES:
Flutter:
:path: Flutter
flutter_inappwebview:
:path: ".symlinks/plugins/flutter_inappwebview/ios"
image_picker:
:path: ".symlinks/plugins/image_picker/ios"
video_player:
:path: ".symlinks/plugins/video_player/ios"
wakelock:
:path: ".symlinks/plugins/wakelock/ios"
webview_flutter:
:path: ".symlinks/plugins/webview_flutter/ios"
SPEC CHECKSUMS:
Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
image_picker: 50e7c7ff960e5f58faa4d1f4af84a771c671bc4a
video_player: 9cc823b1d9da7e8427ee591e8438bfbcde500e6e
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
webview_flutter: 9f491a9b5a66f2573946a389b2677987b0ff8c0b
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c

99
example/lib/basic.dart Normal file
View File

@ -0,0 +1,99 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:rich_editor/rich_editor.dart';
class BasicDemo extends StatelessWidget {
GlobalKey<RichEditorState> keyEditor = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Basic Demo'),
actions: [
PopupMenuButton(
child: IconButton(
icon: Icon(Icons.more_vert),
onPressed: null,
disabledColor: Colors.white,
),
itemBuilder: (context) {
return [
PopupMenuItem(
child: Text('Get HTML'),
value: 0,
),
PopupMenuItem(
child: Text('Clear content'),
value: 1,
),
PopupMenuItem(
child: Text('Hide keyboard'),
value: 2,
),
PopupMenuItem(
child: Text('Show Keyboard'),
value: 3,
),
];
},
onSelected: (val) async {
switch (val) {
case 0:
String? html = await keyEditor.currentState?.getHtml();
print(html);
break;
case 1:
await keyEditor.currentState?.clear();
break;
case 2:
await keyEditor.currentState?.unFocus();
break;
case 3:
await keyEditor.currentState?.focus();
break;
}
},
),
],
),
body: RichEditor(
key: keyEditor,
// value: '''
// <h1>Heading 1</h1>
// <h2>Heading 2</h2>
// <h3>Heading 3</h3>
// <h4>Heading 4</h4>
// <h5>Heading 5</h5>
// <h6>Heading 6</h6>
// ''', // initial HTML data
editorOptions: RichEditorOptions(
placeholder: 'Start typing',
// backgroundColor: Colors.blueGrey, // Editor's bg color
// baseTextColor: Colors.white,
// editor padding
padding: EdgeInsets.symmetric(horizontal: 5.0),
// font name
baseFontFamily: 'sans-serif',
// Position of the editing bar (BarPosition.TOP or BarPosition.BOTTOM)
barPosition: BarPosition.TOP,
),
// You can return a Link (maybe you need to upload the image to your
// storage before displaying in the editor or you can also use base64
getImageUrl: (image) {
String link = 'https://avatars.githubusercontent.com/u/24323581?v=4';
String base64 = base64Encode(image.readAsBytesSync());
String base64String = 'data:image/png;base64, $base64';
return base64String;
},
getVideoUrl: (video) {
String link = 'https://file-examples-com.github.io/uploads/2017/'
'04/file_example_MP4_480_1_5MG.mp4';
return link;
},
),
);
}
}

View File

@ -0,0 +1,107 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:rich_editor/rich_editor.dart';
class CustomToolbarDemo extends StatefulWidget {
@override
_CustomToolbarDemoState createState() => _CustomToolbarDemoState();
}
class _CustomToolbarDemoState extends State<CustomToolbarDemo> {
GlobalKey<RichEditorState> keyEditor = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Custom Toolbar Demo'),
actions: [
PopupMenuButton(
child: IconButton(
icon: Icon(Icons.more_vert),
onPressed: null,
disabledColor: Colors.white,
),
itemBuilder: (context) {
return [
PopupMenuItem(
child: Text('Get HTML'),
value: 0,
),
PopupMenuItem(
child: Text('Clear content'),
value: 1,
),
PopupMenuItem(
child: Text('Hide keyboard'),
value: 2,
),
PopupMenuItem(
child: Text('Show Keyboard'),
value: 3,
),
];
},
onSelected: (val) async {
switch (val) {
case 0:
String? html = await keyEditor.currentState?.getHtml();
print(html);
break;
case 1:
await keyEditor.currentState?.clear();
break;
case 2:
await keyEditor.currentState?.unFocus();
break;
case 3:
await keyEditor.currentState?.focus();
break;
}
},
),
],
),
body: Column(
children: [
Wrap(
children: [
IconButton(
icon: Icon(Icons.format_bold),
onPressed: () {
keyEditor.currentState!.javascriptExecutor.setBold();
},
),
],
),
Expanded(
child: RichEditor(
key: keyEditor,
// value: '', // initial HTML data
editorOptions: RichEditorOptions(
placeholder: 'Start typing',
// backgroundColor: Colors.blueGrey, // Editor's bg color
// baseTextColor: Colors.white,
// editor padding
padding: EdgeInsets.symmetric(horizontal: 5.0),
// font name
baseFontFamily: 'sans-serif',
// Position of the editing bar (BarPosition.TOP or BarPosition.BOTTOM)
barPosition: BarPosition.CUSTOM,
),
// You can return a Link (maybe you need to upload the image to your
// storage before displaying in the editor or you can also use base64
getImageUrl: (image) {
String link = 'https://avatars.githubusercontent.com/u/24323581?v=4';
String base64 = base64Encode(image.readAsBytesSync());
String base64String = 'data:image/png;base64, $base64';
return base64String;
},
),
),
],
),
);
}
}

View File

@ -1,5 +1,7 @@
import 'dart:convert';
import 'package:example/basic.dart';
import 'package:example/custom_toolbar_demo.dart';
import 'package:flutter/material.dart';
import 'package:rich_editor/rich_editor.dart';
@ -16,93 +18,8 @@ class MyApp extends StatelessWidget {
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Rich Editor Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
GlobalKey<RichEditorState> keyEditor = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
actions: [
PopupMenuButton(
child: IconButton(
icon: Icon(Icons.more_vert),
onPressed: null,
disabledColor: Colors.white,
),
itemBuilder: (context) {
return [
PopupMenuItem(
child: Text('Get HTML'),
value: 0,
),
PopupMenuItem(
child: Text('Clear content'),
value: 1,
),
PopupMenuItem(
child: Text('Hide keyboard'),
value: 2,
),
PopupMenuItem(
child: Text('Show Keyboard'),
value: 3,
),
];
},
onSelected: (val) async {
switch(val) {
case 0: {
String? html = await keyEditor.currentState?.getHtml();
print(html);
} break;
case 1: {
await keyEditor.currentState?.clear();
} break;
case 2: {
await keyEditor.currentState?.unFocus();
} break;
case 3: {
await keyEditor.currentState?.focus();
} break;
}
},
),
],
),
body: RichEditor(
key: keyEditor,
value: '''
<h1>Heading 1</h1>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h5>Heading 5</h5>
<h6>Heading 6</h6>
''',
// You can return a Link (maybe you need to upload the image to your
// storage before displaying in the editor or you can also use base64
getImageUrl: (image) {
String link = 'https://avatars.githubusercontent.com/u/24323581?v=4';
String base64 = base64Encode(image.readAsBytesSync());
String base64String = 'data:image/png;base64, $base64';
return base64String;
},
),
home: BasicDemo(),
// home: CustomToolbarDemo(),
);
}
}

View File

@ -5,8 +5,6 @@
import FlutterMacOS
import Foundation
import wakelock_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin"))
}

View File

@ -26,6 +26,7 @@
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
C9461C47681F0FF10BFACAC7 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1D0103636D5BC6143471898 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -54,7 +55,7 @@
/* Begin PBXFileReference section */
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "example.app"; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
@ -66,8 +67,12 @@
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
67E13F4865ADF40CF5FAA8FC /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
A1D0103636D5BC6143471898 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
A7593B64764F2BD906AA475F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
C4DAD072CA42D22C645EC022 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -75,6 +80,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
C9461C47681F0FF10BFACAC7 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -99,6 +105,7 @@
33CEB47122A05771004F2AC0 /* Flutter */,
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
42B7EB610A1F9C85B7C623A2 /* Pods */,
);
sourceTree = "<group>";
};
@ -145,9 +152,21 @@
path = Runner;
sourceTree = "<group>";
};
42B7EB610A1F9C85B7C623A2 /* Pods */ = {
isa = PBXGroup;
children = (
A7593B64764F2BD906AA475F /* Pods-Runner.debug.xcconfig */,
67E13F4865ADF40CF5FAA8FC /* Pods-Runner.release.xcconfig */,
C4DAD072CA42D22C645EC022 /* Pods-Runner.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
isa = PBXGroup;
children = (
A1D0103636D5BC6143471898 /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -159,6 +178,7 @@
isa = PBXNativeTarget;
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
B7620E0CE768F128AE1679D1 /* [CP] Check Pods Manifest.lock */,
33CC10E92044A3C60003C045 /* Sources */,
33CC10EA2044A3C60003C045 /* Frameworks */,
33CC10EB2044A3C60003C045 /* Resources */,
@ -270,6 +290,28 @@
shellPath = /bin/sh;
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
B7620E0CE768F128AE1679D1 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */

View File

@ -4,4 +4,7 @@
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@ -29,20 +29,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
chewie:
dependency: transitive
description:
name: chewie
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
chewie_audio:
dependency: transitive
description:
name: chewie_audio
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
clock:
dependency: transitive
description:
@ -78,13 +64,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
ffi:
dependency: transitive
description:
name: ffi
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
@ -97,27 +76,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.0"
flutter_html:
flutter_inappwebview:
dependency: transitive
description:
name: flutter_html
name: flutter_inappwebview
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
flutter_layout_grid:
dependency: transitive
description:
name: flutter_layout_grid
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
flutter_math_fork:
dependency: transitive
description:
name: flutter_math_fork
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.3"
version: "5.3.2"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@ -125,13 +90,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
flutter_svg:
dependency: transitive
description:
name: flutter_svg
url: "https://pub.dartlang.org"
source: hosted
version: "0.22.0"
flutter_test:
dependency: "direct dev"
description: flutter
@ -142,6 +100,13 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_widget_from_html_core:
dependency: transitive
description:
name: flutter_widget_from_html_core
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.1+1"
html:
dependency: transitive
description:
@ -212,13 +177,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
nested:
dependency: transitive
description:
name: nested
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
path:
dependency: transitive
description:
@ -226,20 +184,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
path_drawing:
dependency: transitive
description:
name: path_drawing
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.1"
path_parsing:
dependency: transitive
description:
name: path_parsing
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.1"
pedantic:
dependency: transitive
description:
@ -261,27 +205,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
provider:
dependency: transitive
description:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.0"
quiver:
dependency: transitive
description:
name: quiver
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
rich_editor:
dependency: "direct main"
description:
path: ".."
relative: true
source: path
version: "0.0.1"
version: "0.0.2"
sky_engine:
dependency: transitive
description: flutter
@ -329,13 +259,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
tuple:
dependency: transitive
description:
name: tuple
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
typed_data:
dependency: transitive
description:
@ -350,76 +273,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
video_player:
dependency: transitive
description:
name: video_player
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.4"
video_player_platform_interface:
dependency: transitive
description:
name: video_player_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.0"
video_player_web:
dependency: transitive
description:
name: video_player_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
wakelock:
dependency: transitive
description:
name: wakelock
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.2"
wakelock_macos:
dependency: transitive
description:
name: wakelock_macos
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.0+1"
wakelock_platform_interface:
dependency: transitive
description:
name: wakelock_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.1+1"
wakelock_web:
dependency: transitive
description:
name: wakelock_web
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0+1"
wakelock_windows:
dependency: transitive
description:
name: wakelock_windows
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.0"
webview_flutter:
dependency: transitive
description:
name: webview_flutter
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.7"
win32:
dependency: transitive
description:
name: win32
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
xml:
dependency: transitive
description:
@ -435,5 +288,5 @@ packages:
source: hosted
version: "5.2.0"
sdks:
dart: ">=2.13.0 <3.0.0"
dart: ">=2.12.0 <3.0.0"
flutter: ">=2.0.0"

View File

@ -1,5 +1,9 @@
library rich_editor;
export 'src/models/enum/bar_position.dart';
export 'src/models/rich_editor_options.dart';
export 'src/rendering/rich_editor.dart';
export 'src/utils/javascript_executor_base.dart';
export 'src/widgets/editor_tool_bar.dart';
export 'src/widgets/tab_button.dart';
export 'src/widgets/tab_button.dart';
export 'src/widgets/tab_button.dart';

View File

@ -1,5 +1,11 @@
import 'package:flutter/material.dart';
/// An extension that makes it easy to get Hex code
/// from [Color] and [MaterialColor]
extension ColorX on Color {
String toHexColorString() => '#${value.toRadixString(16).replaceAll('ff', '')}';
}
String toHexColorString() {
String hex = value.toRadixString(16).replaceAll('ff', '');
if (hex.isEmpty) hex = '000000';
return '#$hex';
}
}

View File

@ -18,4 +18,4 @@ class Alias {
data['weight'] = this.weight;
return data;
}
}
}

View File

@ -0,0 +1,3 @@
class DidHtmlChangeListener {
didHtmlChange(bool didHtmlChange) {}
}

View File

@ -0,0 +1,3 @@
class GetCurrentHtmlCallback {
htmlRetrieved(String html) {}
}

View File

@ -0,0 +1,3 @@
class HtmlChangedListener {
htmlChangedAsync(String html) {}
}

View File

@ -0,0 +1,3 @@
class LoadedListener {
editorLoaded() {}
}

View File

@ -1,6 +1,5 @@
import 'package:rich_editor/src/models/enum.dart';
import 'command_state.dart';
import 'enum/command_name.dart';
class EditorState {
bool? didHtmlChange;
@ -27,4 +26,3 @@ class EditorState {
return data;
}
}

View File

@ -0,0 +1,2 @@
/// Position the inbuilt Toolbar or use your custom toolbar
enum BarPosition { TOP, BOTTOM, CUSTOM }

View File

@ -37,8 +37,5 @@ enum CommandName {
INSERTIMAGE,
INSERTCHECKBOX,
// pseudo commands for toggling grouped command views
ENTER_VIEWING_MODE,
EXPANDING_SEARCH_VIEWING,
TOGGLE_GROUPED_TEXT_STYLES_COMMANDS_VIEW,
TOGGLE_GROUPED_INSERT_COMMANDS_COMMANDS_VIEW
}

View File

@ -12,12 +12,11 @@ class Family {
name = json['name'];
if (json['font'] != null) {
fonts = <Font>[];
if(json['font'] is List) {
if (json['font'] is List) {
json['font'].forEach((v) {
fonts!.add(new Font.fromJson(v));
});
}
}
lang = json['lang'];
variant = json['variant'];

View File

@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'enum/bar_position.dart';
class RichEditorOptions {
Color? backgroundColor;
Color? baseTextColor;
EdgeInsets? padding;
String? placeholder;
String? baseFontFamily;
BarPosition? barPosition;
bool? enableVideo;
RichEditorOptions({
Color? backgroundColor,
Color? baseTextColor,
EdgeInsets? padding,
String? placeholder,
String? baseFontFamily,
BarPosition? barPosition,
bool? enableVideo = true,
}) {
this.backgroundColor = backgroundColor;
this.baseTextColor = baseTextColor;
this.padding = padding;
this.placeholder = placeholder;
this.baseFontFamily = baseFontFamily;
this.barPosition = barPosition;
this.enableVideo = enableVideo;
}
}

View File

@ -4,49 +4,55 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:rich_editor/src/models/enum/bar_position.dart';
import 'package:rich_editor/src/models/rich_editor_options.dart';
import 'package:rich_editor/src/services/local_server.dart';
import 'package:rich_editor/src/utils/javascript_executor_base.dart';
import 'package:rich_editor/src/widgets/editor_tool_bar.dart';
import 'package:webview_flutter/webview_flutter.dart';
class RichEditor extends StatefulWidget {
final String? value;
final RichEditorOptions? editorOptions;
final Function(File image)? getImageUrl;
final Function(File video)? getVideoUrl;
RichEditor({Key? key, this.value, this.getImageUrl, this.getVideoUrl})
: super(key: key);
RichEditor({
Key? key,
this.value,
this.editorOptions,
this.getImageUrl,
this.getVideoUrl,
}) : super(key: key);
@override
RichEditorState createState() => RichEditorState();
}
class RichEditorState extends State<RichEditor> {
WebViewController? _controller;
String text = "";
InAppWebViewController? _controller;
final Key _mapKey = UniqueKey();
String assetPath = 'packages/rich_editor/assets/editor/editor.html';
int port = 5321;
String html = '';
LocalServer? localServer;
JavascriptExecutorBase javascriptExecutorBase = JavascriptExecutorBase();
JavascriptExecutorBase javascriptExecutor = JavascriptExecutorBase();
@override
void initState() {
super.initState();
if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView();
if (!Platform.isAndroid) {
initServer();
if (Platform.isIOS) {
_initServer();
}
}
initServer() async {
_initServer() async {
localServer = LocalServer(port);
await localServer!.start(handleRequest);
await localServer!.start(_handleRequest);
}
void handleRequest(HttpRequest request) {
void _handleRequest(HttpRequest request) {
try {
if (request.method == 'GET' &&
request.uri.queryParameters['query'] == "getRawTeXHTML") {
@ -69,79 +75,164 @@ class RichEditorState extends State<RichEditor> {
_loadHtmlFromAssets() async {
final filePath = assetPath;
_controller!.loadUrl("http://localhost:$port/$filePath");
_controller!.loadUrl(
urlRequest: URLRequest(
url: Uri.tryParse('http://localhost:$port/$filePath'),
),
);
}
@override
Widget build(BuildContext context) {
return Column(
children: [
EditorToolBar(
controller: _controller,
getImageUrl: widget.getImageUrl,
javascriptExecutorBase: javascriptExecutorBase,
Visibility(
visible: widget.editorOptions!.barPosition == BarPosition.TOP,
child: _buildToolBar(),
),
Expanded(
child: WebView(
child: InAppWebView(
key: _mapKey,
onWebViewCreated: (WebViewController controller) async {
onWebViewCreated: (controller) async {
_controller = controller;
print('WebView created');
setState(() {});
if (!Platform.isAndroid) {
print('Loading');
await _loadHtmlFromAssets();
} else {
await _controller!
.loadUrl('file:///android_asset/flutter_assets/$assetPath');
}
javascriptExecutorBase.init(_controller!);
if (widget.value != null) {
// wait 1 second before setting the html
Timer(Duration(seconds: 1), () async {
await javascriptExecutorBase.setHtml(widget.value!);
});
await _controller!.loadUrl(
urlRequest: URLRequest(
url: Uri.tryParse(
'file:///android_asset/flutter_assets/$assetPath'),
),
);
}
},
javascriptMode: JavascriptMode.unrestricted,
gestureNavigationEnabled: true,
onLoadStop: (controller, link) async {
if (link!.path != 'blank') {
javascriptExecutor.init(_controller!);
await _setInitialValues();
_addJSListener();
}
},
// javascriptMode: JavascriptMode.unrestricted,
// gestureNavigationEnabled: false,
gestureRecognizers: [
Factory(() => VerticalDragGestureRecognizer()..onUpdate = (_) {}),
].toSet(),
onWebResourceError: (e) {
print("error ${e.description}");
onLoadError: (controller, url, code, e) {
print("error $e $code");
},
onConsoleMessage: (controller, consoleMessage) async {
print(
'WebView Message: $consoleMessage',
);
},
),
)
),
Visibility(
visible: widget.editorOptions!.barPosition == BarPosition.BOTTOM,
child: _buildToolBar(),
),
],
);
}
_buildToolBar() {
return EditorToolBar(
getImageUrl: widget.getImageUrl,
getVideoUrl: widget.getVideoUrl,
javascriptExecutor: javascriptExecutor,
enableVideo: widget.editorOptions!.enableVideo,
);
}
_setInitialValues() async {
if (widget.value != null) await javascriptExecutor.setHtml(widget.value!);
if (widget.editorOptions!.padding != null)
await javascriptExecutor.setPadding(widget.editorOptions!.padding!);
if (widget.editorOptions!.backgroundColor != null)
await javascriptExecutor
.setBackgroundColor(widget.editorOptions!.backgroundColor!);
if (widget.editorOptions!.baseTextColor != null)
await javascriptExecutor
.setBaseTextColor(widget.editorOptions!.baseTextColor!);
if (widget.editorOptions!.placeholder != null)
await javascriptExecutor
.setPlaceholder(widget.editorOptions!.placeholder!);
if (widget.editorOptions!.baseFontFamily != null)
await javascriptExecutor
.setBaseFontFamily(widget.editorOptions!.baseFontFamily!);
}
_addJSListener() async {
_controller!.addJavaScriptHandler(
handlerName: 'editor-state-changed-callback://',
callback: (c) {
print('Callback $c');
});
}
/// Get current HTML from editor
Future<String?> getHtml() async {
try {
html = await javascriptExecutorBase.getCurrentHtml();
html = await javascriptExecutor.getCurrentHtml();
} catch (e) {}
return html;
}
/// Set your HTML to the editor
setHtml(String html) async {
return await javascriptExecutorBase.setHtml(html);
return await javascriptExecutor.setHtml(html);
}
// Hide the keyboard using JavaScript since it's being opened in a WebView
// https://stackoverflow.com/a/8263376/10835183
/// Hide the keyboard using JavaScript since it's being opened in a WebView
/// https://stackoverflow.com/a/8263376/10835183
unFocus() {
_controller!.evaluateJavascript('document.activeElement.blur();');
javascriptExecutor.unFocus();
}
// Clear editor content using Javascript
/// Clear editor content using Javascript
clear() {
_controller!.evaluateJavascript('document.getElementById(\'editor\').innerHTML = "";');
_controller!.evaluateJavascript(
source: 'document.getElementById(\'editor\').innerHTML = "";');
}
// Focus and Show the keyboard using JavaScript
// https://stackoverflow.com/a/6809236/10835183
/// Focus and Show the keyboard using JavaScript
/// https://stackoverflow.com/a/6809236/10835183
focus() {
_controller!.evaluateJavascript('document.getElementById(\'editor\').focus();');
javascriptExecutor.focus();
}
/// Add custom CSS code to Editor
loadCSS(String cssFile) {
var jsCSSImport = "(function() {" +
" var head = document.getElementsByTagName(\"head\")[0];" +
" var link = document.createElement(\"link\");" +
" link.rel = \"stylesheet\";" +
" link.type = \"text/css\";" +
" link.href = \"" +
cssFile +
"\";" +
" link.media = \"all\";" +
" head.appendChild(link);" +
"}) ();";
_controller!.evaluateJavascript(source: jsCSSImport);
}
/// if html is equal to html RichTextEditor sets by default at start
/// (<p></p>) so that RichTextEditor can be considered as 'empty'.
Future<bool> isEmpty() async {
html = await javascriptExecutor.getCurrentHtml();
return html == '<p></p>';
}
/// Enable Editing (If editing is disabled)
enableEditing() async {
await javascriptExecutor.setInputEnabled(true);
}
/// Disable Editing (Could be used for a 'view mode')
disableEditing() async {
await javascriptExecutor.setInputEnabled(false);
}
}

View File

@ -1,148 +1 @@
import 'package:flutter/material.dart';
import 'package:rich_editor/src/models/button.dart';
import 'javascript_executor_base.dart';
List<Button> buttons = [
Button(
icon: Icons.format_bold,
onTap: (JavascriptExecutorBase javascriptExecutorBase) async =>
await javascriptExecutorBase.setBold(),
),
Button(
icon: Icons.format_italic,
onTap: (JavascriptExecutorBase javascriptExecutorBase) async =>
await javascriptExecutorBase.setItalic(),
),
Button(
icon: Icons.link,
onTap: (JavascriptExecutorBase javascriptExecutorBase, String link, String title) async {
await javascriptExecutorBase.insertLink(
'https://github.com/JideGuru',
'Sample',
);
},
),
Button(
icon: Icons.image,
onTap: (JavascriptExecutorBase javascriptExecutorBase) async {
await javascriptExecutorBase.insertImage(
'https://avatars.githubusercontent.com/u/24323581?v=4',
alt: 'Jide',
height: 200,
width: 200,
);
},
),
Button(
icon: Icons.format_underline,
onTap: (JavascriptExecutorBase javascriptExecutorBase) async =>
await javascriptExecutorBase.setUnderline(),
),
Button(
icon: Icons.format_strikethrough,
onTap: (JavascriptExecutorBase javascriptExecutorBase) async =>
await javascriptExecutorBase.setStrikeThrough(),
),
Button(
icon: Icons.superscript,
onTap: (JavascriptExecutorBase javascriptExecutorBase) async =>
await javascriptExecutorBase.setSuperscript(),
),
Button(
icon: Icons.subscript,
onTap: (JavascriptExecutorBase javascriptExecutorBase) async =>
await javascriptExecutorBase.setSubscript(),
),
Button(
icon: Icons.format_clear,
onTap: (JavascriptExecutorBase javascriptExecutorBase) async =>
await javascriptExecutorBase.removeFormat(),
),
Button(
icon: Icons.undo,
onTap: (JavascriptExecutorBase javascriptExecutorBase) async =>
await javascriptExecutorBase.undo(),
),
Button(
icon: Icons.redo,
onTap: (JavascriptExecutorBase javascriptExecutorBase) async =>
await javascriptExecutorBase.redo(),
),
Button(
icon: Icons.format_quote,
onTap: (JavascriptExecutorBase javascriptExecutorBase) async =>
await javascriptExecutorBase.setBlockQuote(),
),
Button(
icon: Icons.text_format,
onTap: (JavascriptExecutorBase javascriptExecutorBase, String name) async =>
await javascriptExecutorBase.setPreformat(),
),
Button(
icon: Icons.font_download,
onTap: (JavascriptExecutorBase javascriptExecutorBase, String name) async =>
await javascriptExecutorBase.setFontName(name),
),
Button(
icon: Icons.format_size,
onTap: (JavascriptExecutorBase javascriptExecutorBase, int size) async =>
await javascriptExecutorBase.setFontSize(size),
),
Button(
icon: Icons.format_color_text,
onTap: (JavascriptExecutorBase javascriptExecutorBase, Color color) async =>
await javascriptExecutorBase.setTextColor(color),
),
Button(
icon: Icons.format_color_fill,
onTap: (JavascriptExecutorBase javascriptExecutorBase, Color color) async =>
await javascriptExecutorBase.setTextBackgroundColor(color),
),
Button(
icon: Icons.format_indent_increase,
onTap: (JavascriptExecutorBase javascriptExecutorBase, int size) async =>
await javascriptExecutorBase.setIndent(),
),
Button(
icon: Icons.format_indent_decrease,
onTap: (JavascriptExecutorBase javascriptExecutorBase) async =>
await javascriptExecutorBase.setOutdent(),
),
Button(
icon: Icons.format_align_left_outlined,
onTap: (JavascriptExecutorBase javascriptExecutorBase) async =>
await javascriptExecutorBase.setJustifyLeft(),
),
Button(
icon: Icons.format_align_center,
onTap: (JavascriptExecutorBase javascriptExecutorBase) async =>
await javascriptExecutorBase.setJustifyCenter(),
),
Button(
icon: Icons.format_align_right,
onTap: (JavascriptExecutorBase javascriptExecutorBase) async =>
await javascriptExecutorBase.setJustifyRight(),
),
Button(
icon: Icons.format_align_justify,
onTap: (JavascriptExecutorBase javascriptExecutorBase) async =>
await javascriptExecutorBase.setJustifyFull(),
),
Button(
icon: Icons.format_list_bulleted,
onTap: (JavascriptExecutorBase javascriptExecutorBase) async =>
await javascriptExecutorBase.insertBulletList(),
),
Button(
icon: Icons.format_list_numbered,
onTap: (JavascriptExecutorBase javascriptExecutorBase) async =>
await javascriptExecutorBase.insertNumberedList(),
),
Button(
icon: Icons.check_box_outlined,
onTap: (JavascriptExecutorBase javascriptExecutorBase, String text) async =>
await javascriptExecutorBase.insertCheckbox(text),
),
Button(icon: Icons.search),
];

View File

@ -8,12 +8,13 @@ import 'package:rich_editor/src/models/font.dart';
import 'package:rich_editor/src/models/system_font.dart';
import 'package:xml2json/xml2json.dart';
// A simple port of FontListParser from Java to Kotlin
// See https://stackoverflow.com/a/29533686/10835183
/// A simple port of FontListParser from Java to Dart
/// See https://stackoverflow.com/a/29533686/10835183
class FontListParser {
File androidFontsFile = File("/system/etc/fonts.xml");
File androidSystemFontsFile = File("/system/etc/system_fonts.xml");
/// Gets fonts from the fonts xml files in the android system
List<SystemFont> getSystemFonts() {
String fontsXml;
if (androidFontsFile.existsSync()) {
@ -38,7 +39,7 @@ class FontListParser {
break;
}
}
if( font.t != null) {
if (font.t != null) {
SystemFont systemFont = new SystemFont(family.name!, font.t!);
if (fonts.contains(systemFont)) {
continue;
@ -75,6 +76,7 @@ class FontListParser {
return fonts;
}
/// Gets font from the list defined in case the above function doesn't work
List<SystemFont> safelyGetSystemFonts() {
try {
return getSystemFonts();
@ -83,16 +85,16 @@ class FontListParser {
["cursive", "DancingScript-Regular.ttf"],
["monospace", "DroidSansMono.ttf"],
["sans-serif", "Roboto-Regular.ttf"],
["sans-serif-light" "Roboto-Light.ttf"],
["sans-serif-medium", "Roboto-Medium.ttf"],
["sans-serif-black", "Roboto-Black.ttf"],
["sans-serif-condensed", "RobotoCondensed-Regular.ttf"],
["sans-serif-thin", "Roboto-Thin.ttf"],
["sans-serif-light" "Roboto-Light.ttf"],
["sans-serif-medium", "Roboto-Medium.ttf"],
["sans-serif-black", "Roboto-Black.ttf"],
["sans-serif-condensed", "RobotoCondensed-Regular.ttf"],
["sans-serif-thin", "Roboto-Thin.ttf"],
["serif", "NotoSerif-Regular.ttf"]
];
List<SystemFont> fonts = <SystemFont>[];
for (List names in defaultSystemFonts) {
File file = new File("/system/fonts/"+ names[1]);
File file = new File("/system/fonts/" + names[1]);
if (file.existsSync()) {
fonts.add(new SystemFont(names[0], file.path));
}

View File

@ -1,15 +1,19 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:rich_editor/src/extensions/extensions.dart';
import 'package:rich_editor/src/models/callbacks/did_html_change_listener.dart';
import 'package:rich_editor/src/models/callbacks/html_changed_listener.dart';
import 'package:rich_editor/src/models/callbacks/loaded_listener.dart';
import 'package:rich_editor/src/models/editor_state.dart';
import 'package:rich_editor/src/models/enum.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:rich_editor/src/models/enum/command_name.dart';
import '../models/command_state.dart';
/// A class that handles all editor-related javascript functions
class JavascriptExecutorBase {
WebViewController? _controller;
InAppWebViewController? _controller;
String defaultHtml = "<p>\u200B</p>";
@ -18,15 +22,33 @@ class JavascriptExecutorBase {
String defaultEncoding = "UTF-8";
String? htmlField = "";
var didHtmlChange = false;
Map<CommandName, CommandState> commandStates = {};
init(WebViewController controller) {
List<Map<CommandName, CommandState>> commandStatesChangedListeners =
<Map<CommandName, CommandState>>[];
List<DidHtmlChangeListener> didHtmlChangeListeners =
<DidHtmlChangeListener>[];
List<HtmlChangedListener> htmlChangedListeners = <HtmlChangedListener>[];
// protected val fireHtmlChangedListenersQueue = AsyncProducerConsumerQueue<String>(1) { html ->
// fireHtmlChangedListeners(html)
// }
bool isLoaded = false;
List<LoadedListener> loadedListeners = <LoadedListener>[];
init(InAppWebViewController? controller) {
_controller = controller;
}
executeJavascript(String command) async {
return await _controller!.evaluateJavascript('editor.$command');
return await _controller!.evaluateJavascript(source: 'editor.$command');
}
String getCachedHtml() {
@ -41,8 +63,8 @@ class JavascriptExecutorBase {
getCurrentHtml() async {
String? html = await executeJavascript('getEncodedHtml()');
String? decodedHtml = Uri.decodeFull(html!);
if (decodedHtml.startsWith('"') && decodedHtml.endsWith('"')) {
String? decodedHtml = decodeHtml(html!);
if (decodedHtml!.startsWith('"') && decodedHtml.endsWith('"')) {
decodedHtml = decodedHtml.substring(1, decodedHtml.length - 1);
}
return decodedHtml;
@ -54,113 +76,113 @@ class JavascriptExecutorBase {
// Text commands
undo() async {
await executeJavascript("undo()");
await executeJavascript("undo();");
}
redo() async {
await executeJavascript("redo()");
await executeJavascript("redo();");
}
setBold() async {
await executeJavascript("setBold()");
await executeJavascript("setBold();");
}
setItalic() async {
await executeJavascript("setItalic()");
await executeJavascript("setItalic();");
}
setUnderline() async {
await executeJavascript("setUnderline()");
await executeJavascript("setUnderline();");
}
setSubscript() async {
await executeJavascript("setSubscript()");
await executeJavascript("setSubscript();");
}
setSuperscript() async {
await executeJavascript("setSuperscript()");
await executeJavascript("setSuperscript();");
}
setStrikeThrough() async {
await executeJavascript("setStrikeThrough()");
await executeJavascript("setStrikeThrough();");
}
setTextColor(Color? color) async {
String? hex = color!.toHexColorString();
await executeJavascript("setTextColor('$hex')");
await executeJavascript("setTextColor('$hex');");
}
setTextBackgroundColor(Color? color) async {
String? hex = color!.toHexColorString();
await executeJavascript("setTextBackgroundColor('$hex')");
await executeJavascript("setTextBackgroundColor('$hex');");
}
setFontName(String fontName) async {
await executeJavascript("setFontName('$fontName')");
await executeJavascript("setFontName('$fontName');");
}
setFontSize(int fontSize) async {
if (fontSize < 1 || fontSize > 7) {
throw ("Font size should have a value between 1-7");
}
await executeJavascript("setFontSize('$fontSize')");
await executeJavascript("setFontSize('$fontSize');");
}
setHeading(int heading) async {
await executeJavascript("setHeading('$heading')");
await executeJavascript("setHeading('$heading');");
}
setFormattingToParagraph() async {
await executeJavascript("setFormattingToParagraph()");
await executeJavascript("setFormattingToParagraph();");
}
setPreformat() async {
await executeJavascript("setPreformat()");
await executeJavascript("setPreformat();");
}
setBlockQuote() async {
await executeJavascript("setBlockQuote()");
await executeJavascript("setBlockQuote();");
}
removeFormat() async {
await executeJavascript("removeFormat()");
await executeJavascript("removeFormat();");
}
setJustifyLeft() async {
await executeJavascript("setJustifyLeft()");
await executeJavascript("setJustifyLeft();");
}
setJustifyCenter() async {
await executeJavascript("setJustifyCenter()");
await executeJavascript("setJustifyCenter();");
}
setJustifyRight() async {
await executeJavascript("setJustifyRight()");
await executeJavascript("setJustifyRight();");
}
setJustifyFull() async {
await executeJavascript("setJustifyFull()");
await executeJavascript("setJustifyFull();");
}
setIndent() async {
await executeJavascript("setIndent()");
await executeJavascript("setIndent();");
}
setOutdent() async {
await executeJavascript("setOutdent()");
await executeJavascript("setOutdent();");
}
insertBulletList() async {
await executeJavascript("insertBulletList()");
await executeJavascript("insertBulletList();");
}
insertNumberedList() async {
await executeJavascript("insertNumberedList()");
await executeJavascript("insertNumberedList();");
}
// Insert element
insertLink(String url, String title) async {
await executeJavascript("insertLink('$url', '$title')");
await executeJavascript("insertLink('$url', '$title');");
}
/// The rotation parameter is used to signal that the image is rotated and should be rotated by CSS by given value.
@ -172,116 +194,244 @@ class JavascriptExecutorBase {
if (height == null) height = 300;
if (alt == null) alt = '';
await executeJavascript(
"insertImage('$url', '$alt', '$width', '$height', $rotation)",
"insertImage('$url', '$alt', '$width', '$height', $rotation);",
);
}
/// Insert video from Youtube or Device
/// might work with dailymotion but i've not tested that
insertVideo(String url,
{int? width, int? height, bool fromDevice = true}) async {
bool? local;
local = fromDevice ? true : null;
if (width == null) width = 300;
if (height == null) height = 220;
// check if link is yt link
if (url.contains('youtu')) {
// Get Video id from link.
String youtubeId = url.split(r'?v=')[1];
url = 'https://www.youtube.com/embed/$youtubeId';
}
await executeJavascript(
"insertVideo('$url', '$width', '$height', $local);",
);
}
insertCheckbox(String text) async {
await executeJavascript("insertCheckbox('$text')");
await executeJavascript("insertCheckbox('$text');");
}
insertHtml(String html) async {
String? encodedHtml = encodeHtml(html);
await executeJavascript("insertHtml('$encodedHtml')");
await executeJavascript("insertHtml('$encodedHtml');");
}
makeImagesResizeable() async {
await executeJavascript("makeImagesResizeable()");
await executeJavascript("makeImagesResizeable();");
}
disableImageResizing() async {
await executeJavascript("disableImageResizing()");
await executeJavascript("disableImageResizing();");
}
static decodeHtml(String html) {
// Editor settings commands
focus() async {
await executeJavascript("focus();");
}
unFocus() async {
await executeJavascript("blurFocus();");
}
setBackgroundColor(Color? color) async {
String? hex = color!.toHexColorString();
await executeJavascript("setBackgroundColor('$hex');");
}
setBackgroundImage(String image) async {
await executeJavascript("setBackgroundImage('$image');");
}
setBaseTextColor(Color? color) async {
String? hex = color!.toHexColorString();
await executeJavascript("setBaseTextColor('$hex');");
}
setBaseFontFamily(String fontFamily) async {
await executeJavascript("setBaseFontFamily('$fontFamily');");
}
setPadding(EdgeInsets? padding) async {
String left = padding!.left.toString();
String top = padding.top.toString();
String right = padding.right.toString();
String bottom = padding.bottom.toString();
await executeJavascript(
"setPadding('${left}px', '${top}px', '${right}px', '${bottom}px');");
}
// Doesnt actually work for' now
setPlaceholder(String placeholder) async {
await executeJavascript("setPlaceholder('$placeholder');");
}
setEditorWidth(int px) async {
await executeJavascript("setWidth('" + px.toString() + "px');");
}
setEditorHeight(int px) async {
await executeJavascript("setHeight('" + px.toString() + "px');");
}
setInputEnabled(bool inputEnabled) async {
await executeJavascript("setInputEnabled($inputEnabled);");
}
decodeHtml(String html) {
return Uri.decodeFull(html);
}
static encodeHtml(String html) {
encodeHtml(String html) {
return Uri.encodeFull(html);
}
// bool shouldOverrideUrlLoading(String url) {
// String decodedUrl;
// try {
// decodedUrl = decodeHtml(url);
// } catch (e) {
// // No handling
// return false;
// }
//
// if (url.indexOf(editorStateChangedCallbackScheme) == 0) {
// editorStateChanged(
// decodedUrl.substring(editorStateChangedCallbackScheme.length));
// return true;
// }
//
// return false;
// }
//
// editorStateChanged(String statesString) {
// try {
// var editorState = EditorState.fromJson(jsonDecode(statesString));
//
// bool currentHtmlChanged = this.htmlField != editorState.html;
// this.htmlField = editorState.html;
//
// retrievedEditorState(editorState.didHtmlChange, editorState.commandStates)
//
// if (currentHtmlChanged) {
// fireHtmlChangedListenersAsync(editorState.html);
// }
// }
// catch (e) {
// throw("Could not parse command states: $statesString $e");
// }
// }
//
// retrievedEditorState(bool didHtmlChange,
// Map<CommandName, CommandState> commandStates) {
// if (this.didHtmlChange != didHtmlChange) {
// this.didHtmlChange = didHtmlChange;
// didHtmlChangeListeners.forEach {
// it.didHtmlChange(didHtmlChange);
// }
// }
//
// handleRetrievedCommandStates(commandStates)
// }
//
// handleRetrievedCommandStates(Map<CommandName, CommandState> commandStates) {
// determineDerivedCommandStates(commandStates)
//
// this.commandStates = commandStates;
//
// commandStatesChangedListeners.forEach {
// it.invoke(this.commandStates)
// }
// }
bool shouldOverrideUrlLoading(String url) {
String decodedUrl;
try {
decodedUrl = decodeHtml(url);
} catch (e) {
// No handling
return false;
}
// determineDerivedCommandStates(Map<CommandName, CommandState> commandStates) {
// commandStates[CommandName.FORMATBLOCK]?.let { formatCommandState ->
// commandStates.put(CommandName.H1, CommandState(formatCommandState.executable, isFormatActivated(formatCommandState, "h1")))
// commandStates.put(CommandName.H2, CommandState(formatCommandState.executable, isFormatActivated(formatCommandState, "h2")))
// commandStates.put(CommandName.H3, CommandState(formatCommandState.executable, isFormatActivated(formatCommandState, "h3")))
// commandStates.put(CommandName.H4, CommandState(formatCommandState.executable, isFormatActivated(formatCommandState, "h4")))
// commandStates.put(CommandName.H5, CommandState(formatCommandState.executable, isFormatActivated(formatCommandState, "h5")))
// commandStates.put(CommandName.H6, CommandState(formatCommandState.executable, isFormatActivated(formatCommandState, "h6")))
// commandStates.put(CommandName.P, CommandState(formatCommandState.executable, isFormatActivated(formatCommandState, "p")))
// commandStates.put(CommandName.PRE, CommandState(formatCommandState.executable, isFormatActivated(formatCommandState, "pre")))
// commandStates.put(CommandName.BR, CommandState(formatCommandState.executable, isFormatActivated(formatCommandState, "")))
// commandStates.put(CommandName.BLOCKQUOTE, CommandState(formatCommandState.executable, isFormatActivated(formatCommandState, "blockquote")))
// }
//
// commandStates[CommandName.INSERTHTML]?.let { insertHtmlState ->
// commandStates.put(CommandName.INSERTLINK, insertHtmlState)
// commandStates.put(CommandName.INSERTIMAGE, insertHtmlState)
// commandStates.put(CommandName.INSERTCHECKBOX, insertHtmlState)
// }
// }
if (url.indexOf(editorStateChangedCallbackScheme) == 0) {
editorStateChanged(
decodedUrl.substring(editorStateChangedCallbackScheme.length));
return true;
}
// String isFormatActivated(CommandState formatCommandState, String format) {
// return (formatCommandState.value == format)
// .toString(); // rich_text_editor.js reports boolean values as string, so we also have to convert it to string
// }
return false;
}
editorStateChanged(String statesString) {
try {
var editorState = EditorState.fromJson(jsonDecode(statesString));
bool currentHtmlChanged = this.htmlField != editorState.html;
this.htmlField = editorState.html;
retrievedEditorState(
editorState.didHtmlChange!, editorState.commandStates!);
if (currentHtmlChanged) {
// fireHtmlChangedListenersAsync(editorState.html);
}
} catch (e) {
throw ("Could not parse command states: $statesString $e");
}
}
retrievedEditorState(
bool didHtmlChange, Map<CommandName, CommandState> commandStates) {
if (this.didHtmlChange != didHtmlChange) {
this.didHtmlChange = didHtmlChange;
didHtmlChangeListeners.forEach((element) {
element.didHtmlChange(didHtmlChange);
});
}
handleRetrievedCommandStates(commandStates);
}
handleRetrievedCommandStates(Map<CommandName, CommandState> commandStates) {
determineDerivedCommandStates(commandStates);
this.commandStates = commandStates;
commandStatesChangedListeners.forEach((element) {
element = this.commandStates;
});
}
determineDerivedCommandStates(Map<CommandName, CommandState> commandStates) {
if (commandStates[CommandName.FORMATBLOCK] != null) {
var formatCommandState = commandStates[CommandName.FORMATBLOCK];
commandStates.update(
CommandName.H1,
(val) => CommandState(formatCommandState!.executable,
isFormatActivated(formatCommandState, "h1")),
);
commandStates.update(
CommandName.H2,
(val) => CommandState(formatCommandState!.executable,
isFormatActivated(formatCommandState, "h2")));
commandStates.update(
CommandName.H3,
(val) => CommandState(formatCommandState!.executable,
isFormatActivated(formatCommandState, "h3")),
);
commandStates.update(
CommandName.H4,
(val) => CommandState(formatCommandState!.executable,
isFormatActivated(formatCommandState, "h4")),
);
commandStates.update(
CommandName.H5,
(val) => CommandState(formatCommandState!.executable,
isFormatActivated(formatCommandState, "h5")),
);
commandStates.update(
CommandName.H6,
(val) => CommandState(formatCommandState!.executable,
isFormatActivated(formatCommandState, "h6")),
);
commandStates.update(
CommandName.P,
(val) => CommandState(formatCommandState!.executable,
isFormatActivated(formatCommandState, "p")),
);
commandStates.update(
CommandName.PRE,
(val) => CommandState(formatCommandState!.executable,
isFormatActivated(formatCommandState, "pre")),
);
commandStates.update(
CommandName.BR,
(val) => CommandState(formatCommandState!.executable,
isFormatActivated(formatCommandState, "")),
);
commandStates.update(
CommandName.BLOCKQUOTE,
(val) => CommandState(formatCommandState!.executable,
isFormatActivated(formatCommandState, "blockquote")),
);
}
if (commandStates[CommandName.INSERTHTML] != null) {
CommandState? insertHtmlState = commandStates[CommandName.INSERTHTML];
commandStates.update(CommandName.INSERTLINK, (val) => insertHtmlState!);
commandStates.update(CommandName.INSERTIMAGE, (val) => insertHtmlState!);
commandStates.update(
CommandName.INSERTCHECKBOX, (val) => insertHtmlState!);
}
}
String isFormatActivated(CommandState formatCommandState, String format) {
return (formatCommandState.value == format)
.toString(); // rich_text_editor.js reports boolean values as string, so we also have to convert it to string
}
addCommandStatesChangedListener(
Map<CommandName, CommandState> commandStates) {
commandStatesChangedListeners.add(commandStates);
// listener.invoke(commandStates);
}
addDidHtmlChangeListener(DidHtmlChangeListener listener) {
didHtmlChangeListeners.add(listener);
}
addHtmlChangedListener(HtmlChangedListener listener) {
htmlChangedListeners.add(listener);
}
}

View File

@ -5,7 +5,6 @@ class CustomDialogTemplate extends StatelessWidget {
final Function? onDone;
final Function? onCancel;
CustomDialogTemplate({this.body, this.onDone, this.onCancel});
@override

View File

@ -7,33 +7,27 @@ import 'package:rich_editor/src/widgets/fonts_dialog.dart';
import 'package:rich_editor/src/widgets/insert_image_dialog.dart';
import 'package:rich_editor/src/widgets/insert_link_dialog.dart';
import 'package:rich_editor/src/widgets/tab_button.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'color_picker_dialog.dart';
import 'font_size_dialog.dart';
import 'heading_dialog.dart';
class EditorToolBar extends StatelessWidget {
final WebViewController? controller;
final Function(File image)? getImageUrl;
final Function(File video)? getVideoUrl;
final JavascriptExecutorBase javascriptExecutorBase;
final JavascriptExecutorBase javascriptExecutor;
final bool? enableVideo;
EditorToolBar({
this.controller,
this.getImageUrl,
this.getVideoUrl,
required this.javascriptExecutorBase,
required this.javascriptExecutor,
this.enableVideo,
});
@override
Widget build(BuildContext context) {
// if (controller != null) {
// javascriptExecutorBase.init(controller!);
// }
return Container(
// color: Color(0xff424242),
height: 54.0,
child: Column(
children: [
@ -46,21 +40,20 @@ class EditorToolBar extends StatelessWidget {
tooltip: 'Bold',
icon: Icons.format_bold,
onTap: () async {
await javascriptExecutorBase.setBold();
await javascriptExecutor.setBold();
},
),
TabButton(
tooltip: 'Italic',
icon: Icons.format_italic,
onTap: () async {
await javascriptExecutorBase.setItalic();
await javascriptExecutor.setItalic();
},
),
TabButton(
tooltip: 'Insert Link',
icon: Icons.link,
onTap: () async {
_closeKeyboard();
var link = await showDialog(
context: context,
barrierDismissible: false,
@ -69,17 +62,15 @@ class EditorToolBar extends StatelessWidget {
},
);
if (link != null)
await javascriptExecutorBase.insertLink(link[0], link[1]);
await javascriptExecutor.insertLink(link[0], link[1]);
},
),
TabButton(
tooltip: 'Insert Image',
icon: Icons.image,
onTap: () async {
_closeKeyboard();
var link = await showDialog(
context: context,
barrierDismissible: false,
builder: (_) {
return InsertImageDialog();
},
@ -88,74 +79,97 @@ class EditorToolBar extends StatelessWidget {
if (getImageUrl != null && link[2]) {
link[0] = await getImageUrl!(File(link[0]));
}
await javascriptExecutorBase.insertImage(
await javascriptExecutor.insertImage(
link[0],
alt: link[1],
);
}
},
),
Visibility(
visible: enableVideo!,
child: TabButton(
tooltip: 'Insert video',
icon: Icons.video_call_sharp,
onTap: () async {
var link = await showDialog(
context: context,
builder: (_) {
return InsertImageDialog(isVideo: true);
},
);
if (link != null) {
if (getVideoUrl != null && link[2]) {
link[0] = await getVideoUrl!(File(link[0]));
}
await javascriptExecutor.insertVideo(
link[0],
fromDevice: link[2],
);
}
},
),
),
TabButton(
tooltip: 'Underline',
icon: Icons.format_underline,
onTap: () async {
await javascriptExecutorBase.setUnderline();
await javascriptExecutor.setUnderline();
},
),
TabButton(
tooltip: 'Strike through',
icon: Icons.format_strikethrough,
onTap: () async {
await javascriptExecutorBase.setStrikeThrough();
await javascriptExecutor.setStrikeThrough();
},
),
TabButton(
tooltip: 'Superscript',
icon: Icons.superscript,
onTap: () async {
await javascriptExecutorBase.setSuperscript();
await javascriptExecutor.setSuperscript();
},
),
TabButton(
tooltip: 'Subscript',
icon: Icons.subscript,
onTap: () async {
await javascriptExecutorBase.setSubscript();
await javascriptExecutor.setSubscript();
},
),
TabButton(
tooltip: 'Clear format',
icon: Icons.format_clear,
onTap: () async {
await javascriptExecutorBase.removeFormat();
await javascriptExecutor.removeFormat();
},
),
TabButton(
tooltip: 'Undo',
icon: Icons.undo,
onTap: () async {
await javascriptExecutorBase.undo();
await javascriptExecutor.undo();
},
),
TabButton(
tooltip: 'Redo',
icon: Icons.redo,
onTap: () async {
await javascriptExecutorBase.redo();
await javascriptExecutor.redo();
},
),
TabButton(
tooltip: 'Blockquote',
icon: Icons.format_quote,
onTap: () async {
await javascriptExecutorBase.setBlockQuote();
await javascriptExecutor.setBlockQuote();
},
),
TabButton(
tooltip: 'Font format',
icon: Icons.text_format,
onTap: () async {
_closeKeyboard();
var command = await showDialog(
// isScrollControlled: true,
context: context,
@ -165,13 +179,13 @@ class EditorToolBar extends StatelessWidget {
);
if (command != null) {
if (command == 'p') {
await javascriptExecutorBase.setFormattingToParagraph();
await javascriptExecutor.setFormattingToParagraph();
} else if (command == 'pre') {
await javascriptExecutorBase.setPreformat();
await javascriptExecutor.setPreformat();
} else if (command == 'blockquote') {
await javascriptExecutorBase.setBlockQuote();
await javascriptExecutor.setBlockQuote();
} else {
await javascriptExecutorBase
await javascriptExecutor
.setHeading(int.tryParse(command)!);
}
}
@ -184,9 +198,6 @@ class EditorToolBar extends StatelessWidget {
tooltip: 'Font face',
icon: Icons.font_download,
onTap: () async {
Directory fontsDir = Directory("/system/fonts/");
File file = File('/system/etc/fonts.xml');
// debugPrint(await file.readAsString());
var command = await showDialog(
// isScrollControlled: true,
context: context,
@ -195,7 +206,7 @@ class EditorToolBar extends StatelessWidget {
},
);
if (command != null)
await javascriptExecutorBase.setFontName(command);
await javascriptExecutor.setFontName(command);
},
),
),
@ -203,7 +214,6 @@ class EditorToolBar extends StatelessWidget {
icon: Icons.format_size,
tooltip: 'Font Size',
onTap: () async {
_closeKeyboard();
String? command = await showDialog(
// isScrollControlled: true,
context: context,
@ -212,7 +222,7 @@ class EditorToolBar extends StatelessWidget {
},
);
if (command != null)
await javascriptExecutorBase
await javascriptExecutor
.setFontSize(int.tryParse(command)!);
},
),
@ -220,7 +230,6 @@ class EditorToolBar extends StatelessWidget {
tooltip: 'Text Color',
icon: Icons.format_color_text,
onTap: () async {
_closeKeyboard();
var color = await showDialog(
context: context,
builder: (BuildContext context) {
@ -228,14 +237,13 @@ class EditorToolBar extends StatelessWidget {
},
);
if (color != null)
await javascriptExecutorBase.setTextColor(color);
await javascriptExecutor.setTextColor(color);
},
),
TabButton(
tooltip: 'Background Color',
icon: Icons.format_color_fill,
onTap: () async {
_closeKeyboard();
var color = await showDialog(
context: context,
builder: (BuildContext context) {
@ -243,80 +251,77 @@ class EditorToolBar extends StatelessWidget {
},
);
if (color != null)
await javascriptExecutorBase
.setTextBackgroundColor(color);
await javascriptExecutor.setTextBackgroundColor(color);
},
),
TabButton(
tooltip: 'Increase Indent',
icon: Icons.format_indent_increase,
onTap: () async {
await javascriptExecutorBase.setIndent();
await javascriptExecutor.setIndent();
},
),
TabButton(
tooltip: 'Decrease Indent',
icon: Icons.format_indent_decrease,
onTap: () async {
await javascriptExecutorBase.setOutdent();
await javascriptExecutor.setOutdent();
},
),
TabButton(
tooltip: 'Align Left',
icon: Icons.format_align_left_outlined,
onTap: () async {
await javascriptExecutorBase.setJustifyLeft();
await javascriptExecutor.setJustifyLeft();
},
),
TabButton(
tooltip: 'Align Center',
icon: Icons.format_align_center,
onTap: () async {
await javascriptExecutorBase.setJustifyCenter();
await javascriptExecutor.setJustifyCenter();
},
),
TabButton(
tooltip: 'Align Right',
icon: Icons.format_align_right,
onTap: () async {
await javascriptExecutorBase.setJustifyRight();
await javascriptExecutor.setJustifyRight();
},
),
TabButton(
tooltip: 'Justify',
icon: Icons.format_align_justify,
onTap: () async {
await javascriptExecutorBase.setJustifyFull();
await javascriptExecutor.setJustifyFull();
},
),
TabButton(
tooltip: 'Bullet List',
icon: Icons.format_list_bulleted,
onTap: () async {
await javascriptExecutorBase.insertBulletList();
await javascriptExecutor.insertBulletList();
},
),
TabButton(
tooltip: 'Numbered List',
icon: Icons.format_list_numbered,
onTap: () async {
await javascriptExecutorBase.insertNumberedList();
await javascriptExecutor.insertNumberedList();
},
),
TabButton(
tooltip: 'Checkbox',
icon: Icons.check_box_outlined,
onTap: () async {
_closeKeyboard();
var text = await showDialog(
context: context,
builder: (BuildContext context) {
return CheckDialog();
},
);
print(text);
if (text != null)
await javascriptExecutorBase.insertCheckbox(text);
await javascriptExecutor.insertCheckbox(text);
},
),
@ -325,7 +330,7 @@ class EditorToolBar extends StatelessWidget {
// tooltip: 'Search',
// icon: Icons.search,
// onTap: () async {
// // await javascriptExecutorBase.insertNumberedList();
// // await javascriptExecutor.insertNumberedList();
// },
// ),
],
@ -335,10 +340,4 @@ class EditorToolBar extends StatelessWidget {
),
);
}
// Hide the keyboard using JavaScript since it's being opened in a WebView
// https://stackoverflow.com/a/8263376/10835183
_closeKeyboard() async {
// controller!.evaluateJavascript('document.activeElement.blur();');
}
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'html_text.dart';
class FontSizeDialog extends StatelessWidget {
List formats = [
@ -22,7 +23,7 @@ class FontSizeDialog extends StatelessWidget {
children: [
for (Map format in formats)
InkWell(
child: Html(data: format['title']),
child: HtmlText(html: format['title']),
onTap: () => Navigator.pop(context, format['id']),
)
],

View File

@ -1,11 +1,10 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:path/path.dart';
import 'package:rich_editor/src/models/system_font.dart';
import 'package:rich_editor/src/utils/font_list_parser.dart';
import 'html_text.dart';
class FontsDialog extends StatelessWidget {
List<SystemFont> getSystemFonts() {
return FontListParser().getSystemFonts();
@ -21,21 +20,16 @@ class FontsDialog extends StatelessWidget {
children: [
for (SystemFont font in getSystemFonts())
InkWell(
child: Html(data: '<p style="font-family:${font.name}">'
'${basename(font.path!)}</p>'),
onTap: () => Navigator.pop(context, font.path),
child: HtmlText(
html: '<p style="font-family:${font.name}">'
'${basename(font.path!)}</p>'),
onTap: () {
Navigator.pop(context, font.name);
},
)
],
),
),
);
}
fontSlug(FileSystemEntity font) {
String name = basename(font.path);
String slug = name.toLowerCase();
slug = slug.replaceAll(extension(font.path), '');
// print(slug);
return slug;
}
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'html_text.dart';
class HeadingDialog extends StatelessWidget {
List formats = [
@ -27,7 +28,7 @@ class HeadingDialog extends StatelessWidget {
children: [
for (Map format in formats)
InkWell(
child: Html(data: format['title']),
child: HtmlText(html: format['title']),
onTap: () => Navigator.pop(context, format['id']),
)
],

View File

@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
class HtmlText extends StatelessWidget {
final String html;
HtmlText({required this.html});
@override
Widget build(BuildContext context) {
return Container(
child: HtmlWidget(html),
height: 40.0,
);
}
}

View File

@ -4,6 +4,10 @@ import 'package:image_picker/image_picker.dart';
import 'custom_dialog_template.dart';
class InsertImageDialog extends StatefulWidget {
final bool isVideo;
InsertImageDialog({this.isVideo = false});
@override
_InsertImageDialogState createState() => _InsertImageDialogState();
}
@ -21,7 +25,7 @@ class _InsertImageDialogState extends State<InsertImageDialog> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Image link'),
Text(widget.isVideo ? 'Video link' : 'Image link'),
ElevatedButton(
onPressed: () => getImage(),
child: Text('...'),
@ -34,12 +38,21 @@ class _InsertImageDialogState extends State<InsertImageDialog> {
hintText: '',
),
),
SizedBox(height: 20.0),
Text('Alt text (optional)'),
TextField(
controller: alt,
decoration: InputDecoration(
hintText: '',
Visibility(
visible: !widget.isVideo,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 20.0),
Text('Alt text (optional)'),
TextField(
controller: alt,
decoration: InputDecoration(
hintText: '',
),
),
],
),
),
],
@ -50,11 +63,16 @@ class _InsertImageDialogState extends State<InsertImageDialog> {
Future getImage() async {
final picker = ImagePicker();
var image = await picker.getImage(
source: ImageSource.gallery,
maxWidth: 800.0,
maxHeight: 600.0,
);
var image;
if (widget.isVideo) {
image = await picker.getVideo(source: ImageSource.gallery);
} else {
image = await picker.getImage(
source: ImageSource.gallery,
maxWidth: 800.0,
maxHeight: 600.0,
);
}
if (image != null) {
link.text = image.path;

View File

@ -4,8 +4,9 @@ class TabButton extends StatelessWidget {
final IconData? icon;
final Function? onTap;
final String tooltip;
final bool selected;
TabButton({this.icon, this.onTap, this.tooltip = ''});
TabButton({this.icon, this.onTap, this.tooltip = '', this.selected = false});
@override
Widget build(BuildContext context) {
@ -17,7 +18,9 @@ class TabButton extends StatelessWidget {
height: 40.0,
width: 40.0,
decoration: BoxDecoration(
// color: Color(0xff212121),
color: selected
? Theme.of(context).accentColor.withOpacity(0.2)
: Colors.transparent,
borderRadius: BorderRadius.all(
Radius.circular(5.0),
),
@ -32,7 +35,9 @@ class TabButton extends StatelessWidget {
padding: const EdgeInsets.all(5.0),
child: Icon(
icon,
// color: Theme.of(context).accentColor,
color: selected
? Theme.of(context).accentColor
: Theme.of(context).iconTheme.color,
),
),
),

View File

@ -29,20 +29,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
chewie:
dependency: transitive
description:
name: chewie
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
chewie_audio:
dependency: transitive
description:
name: chewie_audio
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
clock:
dependency: transitive
description:
@ -64,13 +50,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.17.0"
cupertino_icons:
dependency: transitive
description:
name: cupertino_icons
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
fake_async:
dependency: transitive
description:
@ -78,13 +57,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
ffi:
dependency: transitive
description:
name: ffi
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
@ -97,27 +69,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.0"
flutter_html:
flutter_inappwebview:
dependency: "direct main"
description:
name: flutter_html
name: flutter_inappwebview
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
flutter_layout_grid:
dependency: transitive
description:
name: flutter_layout_grid
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
flutter_math_fork:
dependency: transitive
description:
name: flutter_math_fork
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.3"
version: "5.3.2"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@ -125,13 +83,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
flutter_svg:
dependency: transitive
description:
name: flutter_svg
url: "https://pub.dartlang.org"
source: hosted
version: "0.22.0"
flutter_test:
dependency: "direct dev"
description: flutter
@ -142,6 +93,13 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_widget_from_html_core:
dependency: "direct main"
description:
name: flutter_widget_from_html_core
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.1+1"
html:
dependency: transitive
description:
@ -212,13 +170,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
nested:
dependency: transitive
description:
name: nested
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
path:
dependency: "direct main"
description:
@ -226,20 +177,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
path_drawing:
dependency: transitive
description:
name: path_drawing
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.1"
path_parsing:
dependency: transitive
description:
name: path_parsing
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.1"
pedantic:
dependency: transitive
description:
@ -261,20 +198,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
provider:
dependency: transitive
description:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.0"
quiver:
dependency: transitive
description:
name: quiver
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
sky_engine:
dependency: transitive
description: flutter
@ -322,13 +245,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
tuple:
dependency: transitive
description:
name: tuple
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
typed_data:
dependency: transitive
description:
@ -343,76 +259,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
video_player:
dependency: transitive
description:
name: video_player
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.4"
video_player_platform_interface:
dependency: transitive
description:
name: video_player_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.0"
video_player_web:
dependency: transitive
description:
name: video_player_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
wakelock:
dependency: transitive
description:
name: wakelock
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.2"
wakelock_macos:
dependency: transitive
description:
name: wakelock_macos
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.0+1"
wakelock_platform_interface:
dependency: transitive
description:
name: wakelock_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.1+1"
wakelock_web:
dependency: transitive
description:
name: wakelock_web
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0+1"
wakelock_windows:
dependency: transitive
description:
name: wakelock_windows
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.0"
webview_flutter:
dependency: "direct main"
description:
name: webview_flutter
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.7"
win32:
dependency: transitive
description:
name: win32
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
xml:
dependency: transitive
description:
@ -428,5 +274,5 @@ packages:
source: hosted
version: "5.2.0"
sdks:
dart: ">=2.13.0 <3.0.0"
dart: ">=2.12.0 <3.0.0"
flutter: ">=2.0.0"

View File

@ -1,6 +1,6 @@
name: rich_editor
description: WYSIWYG editor for Flutter with a rich set of supported formatting options.
version: 0.0.2
version: 0.0.3
homepage: https://github.com/JideGuru/rich_editor
environment:
@ -10,10 +10,10 @@ environment:
dependencies:
flutter:
sdk: flutter
webview_flutter: ^2.0.4
flutter_inappwebview: ^5.3.2
mime: ^1.0.0
image_picker: ^0.7.5+3
flutter_html: ^2.0.0
flutter_widget_from_html_core: ^0.6.1+1
flutter_colorpicker: ^0.4.0
path: ^1.8.0
xml2json: ^5.2.0
@ -27,10 +27,9 @@ dev_dependencies:
# The following section is specific to Flutter.
flutter:
# To add assets to your package, add an assets section, like this:
assets:
- assets/editor/
- assets/editor/
#
# For details regarding assets in packages, see
# https://flutter.dev/assets-and-images/#from-packages

View File

@ -1,12 +1 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:rich_editor/rich_editor.dart';
void main() {
// test('adds one to input values', () {
// final calculator = Calculator();
// expect(calculator.addOne(2), 3);
// expect(calculator.addOne(-7), -6);
// expect(calculator.addOne(0), 1);
// });
}
void main() {}