added project'

This commit is contained in:
2022-10-30 15:25:33 +07:00
commit 4dd5d2f6ff
121 changed files with 14320 additions and 0 deletions

Binary file not shown.

Binary file not shown.

2
lib/assets/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,403 @@
class SelectionPreserver {
constructor(rootNode) {
if (rootNode === undefined || rootNode === null) {
throw new Error("Please provide a valid rootNode.");
}
this.rootNode = rootNode;
this.rangeStartContainerAddress = null;
this.rangeStartOffset = null;
}
preserve() {
const selection = window.getSelection();
this.rangeStartOffset = selection.getRangeAt(0).startOffset;
this.rangeStartContainerAddress = this.findRangeStartContainerAddress(
selection
);
}
restore(restoreIndex) {
if (
this.rangeStartOffset === null ||
this.rangeStartContainerAddress === null
) {
throw new Error("Please call preserve() first.");
}
let rangeStartContainer = this.findRangeStartContainer();
const range = document.createRange();
const offSet = restoreIndex || this.rangeStartOffset;
range.setStart(rangeStartContainer, offSet);
range.collapse();
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
findRangeStartContainer() {
let rangeStartContainer = this.rootNode;
this.rangeStartContainerAddress.forEach(address => {
rangeStartContainer = rangeStartContainer.childNodes[address];
});
return rangeStartContainer;
}
findRangeStartContainerAddress(selection) {
let rangeStartContainerAddress = [];
for (
let currentContainer = selection.getRangeAt(0).startContainer;
currentContainer !== this.rootNode;
currentContainer = currentContainer.parentNode
) {
const parent = currentContainer.parentElement;
const children = parent.childNodes;
for (let i = 0; i < children.length; i++) {
if (children[i] === currentContainer) {
rangeStartContainerAddress = [i, ...rangeStartContainerAddress];
break;
}
}
}
return rangeStartContainerAddress;
}
}
const WORD_REGEX = /^[^\s]+$/;
const UP_KEY_CODE = 38;
const DOWN_KEY_CODE = 40;
const ENTER_KEY_CODE = 13;
(function(factory) {
if (typeof define === "function" && define.amd) {
define(["jquery"], factory);
} else if (typeof module === "object" && module.exports) {
module.exports = factory(require("jquery"));
} else {
factory(window.jQuery);
}
})(function($) {
$.extend($.summernote.plugins, {
summernoteAtMention: function(context) {
/************************
* Setup instance vars. *
************************/
this.editableEl = context.layoutInfo.editable[0];
this.editorEl = context.layoutInfo.editor[0];
this.autocompleteAnchor = { left: null, top: null };
this.autocompleteContainer = null;
this.showingAutocomplete = false;
this.selectedIndex = null;
this.suggestions = null;
this.getSuggestions = _ => {
return [];
};
/********************
* Read-in options. *
********************/
if (
context.options &&
context.options.callbacks &&
context.options.callbacks.summernoteAtMention
) {
const summernoteCallbacks =
context.options.callbacks.summernoteAtMention;
if (summernoteCallbacks.getSuggestions) {
this.getSuggestions = summernoteCallbacks.getSuggestions;
}
if (summernoteCallbacks.onSelect) {
this.onSelect = summernoteCallbacks.onSelect;
}
}
/**********
* Events *
**********/
this.events = {
"summernote.blur": () => {
if (this.showingAutocomplete) this.hideAutocomplete();
},
"summernote.keydown": (_, event) => {
if (this.showingAutocomplete) {
switch (event.keyCode) {
case ENTER_KEY_CODE: {
event.preventDefault();
event.stopPropagation();
this.handleEnter();
break;
}
case UP_KEY_CODE: {
event.preventDefault();
event.stopPropagation();
const newIndex =
this.selectedIndex === 0 ? 0 : this.selectedIndex - 1;
this.updateAutocomplete(this.suggestions, newIndex);
break;
}
case DOWN_KEY_CODE: {
event.preventDefault();
event.stopPropagation();
const newIndex =
this.selectedIndex === this.suggestions.length - 1
? this.selectedIndex
: this.selectedIndex + 1;
this.updateAutocomplete(this.suggestions, newIndex);
break;
}
}
}
},
"summernote.keyup": async (_, event) => {
const selection = document.getSelection();
const currentText = selection.anchorNode.nodeValue;
const { word, absoluteIndex } = this.findWordAndIndices(
currentText || "",
selection.anchorOffset
);
const trimmedWord = word.slice(1);
if (
this.showingAutocomplete &&
![DOWN_KEY_CODE, UP_KEY_CODE, ENTER_KEY_CODE].includes(
event.keyCode
)
) {
if (word[0] === "@") {
const suggestions = await this.getSuggestions(trimmedWord);
this.updateAutocomplete(suggestions, this.selectedIndex);
} else {
this.hideAutocomplete();
}
} else if (!this.showingAutocomplete && word[0] === "@") {
this.suggestions = await this.getSuggestions(trimmedWord);
this.selectedIndex = 0;
this.showAutocomplete(absoluteIndex, selection.anchorNode);
}
}
};
/***********
* Helpers *
***********/
this.handleEnter = () => {
this.handleSelection();
};
this.handleClick = suggestion => {
const selectedIndex = this.suggestions.findIndex(s => s === suggestion);
if (selectedIndex === -1) {
throw new Error("Unable to find suggestion in suggestions.");
}
this.selectedIndex = selectedIndex;
this.handleSelection();
};
this.handleSelection = () => {
if (this.suggestions === null || this.suggestions.length === 0) {
return;
}
const newWord = this.suggestions[this.selectedIndex].trimStart();
if (this.onSelect !== undefined) {
this.onSelect(newWord);
}
const selection = document.getSelection();
const currentText = selection.anchorNode.nodeValue;
const { word, absoluteIndex } = this.findWordAndIndices(
currentText || "",
selection.anchorOffset
);
const selectionPreserver = new SelectionPreserver(this.editableEl);
selectionPreserver.preserve();
selection.anchorNode.textContent =
currentText.slice(0, absoluteIndex + 1) +
newWord +
" " +
currentText.slice(absoluteIndex + word.length);
selectionPreserver.restore(absoluteIndex + newWord.length + 1);
if (context.options.callbacks.onChange !== undefined) {
context.options.callbacks.onChange(this.editableEl.innerHTML);
}
};
this.updateAutocomplete = (suggestions, selectedIndex) => {
this.selectedIndex = selectedIndex;
this.suggestions = suggestions;
this.renderAutocomplete();
};
this.showAutocomplete = (atTextIndex, indexAnchor) => {
if (this.showingAutocomplete) {
throw new Error(
"Cannot call showAutocomplete if autocomplete is already showing."
);
}
this.setAutocompleteAnchor(atTextIndex, indexAnchor);
this.renderAutocompleteContainer();
this.renderAutocomplete();
this.showingAutocomplete = true;
};
this.renderAutocompleteContainer = () => {
this.autocompleteContainer = document.createElement("div");
this.autocompleteContainer.style.top =
String(this.autocompleteAnchor.top) + "px";
this.autocompleteContainer.style.left =
String(this.autocompleteAnchor.left) + "px";
this.autocompleteContainer.style.position = "absolute";
this.autocompleteContainer.style.backgroundColor = "#e4e4e4";
this.autocompleteContainer.style.zIndex = Number.MAX_SAFE_INTEGER;
document.body.appendChild(this.autocompleteContainer);
};
this.renderAutocomplete = () => {
if (this.autocompleteContainer === null) {
throw new Error(
"Cannot call renderAutocomplete without an autocompleteContainer. "
);
}
const autocompleteContent = document.createElement("div");
this.suggestions.forEach((suggestion, idx) => {
const suggestionDiv = document.createElement("div");
suggestionDiv.textContent = suggestion;
suggestionDiv.style.padding = "5px 10px";
if (this.selectedIndex === idx) {
suggestionDiv.style.backgroundColor = "#2e6da4";
suggestionDiv.style.color = "white";
}
suggestionDiv.addEventListener("mousedown", () => {
this.handleClick(suggestion);
});
autocompleteContent.appendChild(suggestionDiv);
});
this.autocompleteContainer.innerHTML = "";
this.autocompleteContainer.appendChild(autocompleteContent);
};
this.hideAutocomplete = () => {
if (!this.showingAutocomplete)
throw new Error(
"Cannot call hideAutocomplete if autocomplete is not showing."
);
document.body.removeChild(this.autocompleteContainer);
this.autocompleteAnchor = { left: null, top: null };
this.selectedIndex = null;
this.suggestions = null;
this.showingAutocomplete = false;
};
this.findWordAndIndices = (text, offset) => {
if (offset > text.length) {
return { word: "", relativeIndex: 0 };
} else {
let leftWord = "";
let rightWord = "";
let relativeIndex = 0;
let absoluteIndex = offset;
for (let currentOffset = offset; currentOffset > 0; currentOffset--) {
if (text[currentOffset - 1].match(WORD_REGEX)) {
leftWord = text[currentOffset - 1] + leftWord;
relativeIndex++;
absoluteIndex--;
} else {
break;
}
}
for (
let currentOffset = offset - 1;
currentOffset > 0 && currentOffset < text.length - 1;
currentOffset++
) {
if (text[currentOffset + 1].match(WORD_REGEX)) {
rightWord = rightWord + text[currentOffset + 1];
} else {
break;
}
}
return {
word: leftWord + rightWord,
relativeIndex,
absoluteIndex
};
}
};
this.setAutocompleteAnchor = (atTextIndex, indexAnchor) => {
let html = indexAnchor.parentNode.innerHTML;
const text = indexAnchor.nodeValue;
let atIndex = -1;
for (let i = 0; i <= atTextIndex; i++) {
if (text[i] === "@") {
atIndex++;
}
}
let htmlIndex;
for (let i = 0, htmlAtIndex = 0; i < html.length; i++) {
if (html[i] === "@") {
if (htmlAtIndex === atIndex) {
htmlIndex = i;
break;
} else {
htmlAtIndex++;
}
}
}
const atNodeId = "at-node-" + String(Math.floor(Math.random() * 10000));
const spanString = `<span id="${atNodeId}">@</span>`;
const selectionPreserver = new SelectionPreserver(this.editableEl);
selectionPreserver.preserve();
indexAnchor.parentNode.innerHTML =
html.slice(0, htmlIndex) + spanString + html.slice(htmlIndex + 1);
const anchorElement = document.querySelector("#" + atNodeId);
const anchorBoundingRect = anchorElement.getBoundingClientRect();
this.autocompleteAnchor = {
top: anchorBoundingRect.top + anchorBoundingRect.height + 2,
left: anchorBoundingRect.left
};
selectionPreserver.findRangeStartContainer().parentNode.innerHTML = html;
selectionPreserver.restore();
};
}
});
});

View File

@@ -0,0 +1,24 @@
.note-editing-area, .note-status-output, .note-codable, .CodeMirror, .CodeMirror-gutter, .note-modal-content, .note-input, .note-editable {
background: #121212 !important;
}
.panel-heading, .note-toolbar, .note-statusbar {
background: #343434 !important;
}
input, select, textarea, .CodeMirror, .note-editable, [class^="note-icon-"], .caseConverter-toggle,
button > b, button > code, button > var, button > kbd, button > samp, button > small, button > ins, button > del, button > p, button > i {
color: #fff !important;
}
textarea:focus, input:focus, span, label, .note-status-output {
color: #fff !important;
}
.note-icon-font {
color: #000 !important;
}
.note-btn:not(.note-color-btn) {
background-color: #121212 !important;
}
.note-btn:focus,
.note-btn:active,
.note-btn.active {
background-color: #343434 !important;
}

1470
lib/assets/summernote-lite.min.css vendored Normal file

File diff suppressed because it is too large Load Diff

3
lib/assets/summernote-lite.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,32 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="description" content="Flutter Summernote HTML Editor">
<meta name="author" content="tneotia">
<title>Summernote Text Editor HTML</title>
<script src="jquery.min.js"></script>
<!--summernote js and css-->
<link href="summernote-lite.min.css" rel="stylesheet">
<script src="summernote-lite.min.js"></script>
<!--darkCSS-->
</head>
<body>
<div id="summernote-2"></div>
<!--headString-->
<!--summernoteScripts-->
<!--minor styling to improve editor design-->
<style>
body {
display: block;
margin: 0px;
}
.note-editor.note-airframe, .note-editor.note-frame {
border: 0px solid #a9a9a9;
}
.note-frame {
border-radius: 0px;
}
</style>
</body>
</html>

View File

@@ -0,0 +1,31 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="description" content="Flutter Summernote HTML Editor">
<meta name="author" content="tneotia">
<title>Summernote Text Editor HTML</title>
<script src="jquery.min.js"></script>
<!--summernote js and css-->
<link href="summernote-lite.min.css" rel="stylesheet">
<script src="summernote-lite.min.js"></script>
</head>
<body>
<div id="summernote-2"></div>
<!--summernote at mention plugin-->
<script src="plugins/summernote-at-mention/summernote-at-mention.js"></script>
<!--minor styling to improve editor design-->
<style>
body {
display: block;
margin: 0px;
}
.note-editor.note-airframe, .note-editor.note-frame {
border: 0px solid #a9a9a9;
}
.note-frame {
border-radius: 0px;
}
</style>
</body>
</html>

102
lib/html_editor.dart Normal file
View File

@@ -0,0 +1,102 @@
library html_editor;
export 'package:html_editor_enhanced/src/widgets/toolbar_widget.dart';
export 'package:html_editor_enhanced/utils/callbacks.dart';
export 'package:html_editor_enhanced/utils/toolbar.dart';
export 'package:html_editor_enhanced/utils/plugins.dart';
export 'package:html_editor_enhanced/utils/file_upload_model.dart';
export 'package:html_editor_enhanced/utils/options.dart';
export 'package:html_editor_enhanced/utils/utils.dart'
hide setState, intersperse, getRandString;
export 'package:html_editor_enhanced/src/html_editor_unsupported.dart'
if (dart.library.html) 'package:html_editor_enhanced/src/html_editor_web.dart'
if (dart.library.io) 'package:html_editor_enhanced/src/html_editor_mobile.dart';
export 'package:html_editor_enhanced/src/html_editor_controller_unsupported.dart'
if (dart.library.html) 'package:html_editor_enhanced/src/html_editor_controller_web.dart'
if (dart.library.io) 'package:html_editor_enhanced/src/html_editor_controller_mobile.dart';
export 'package:html_editor_enhanced/utils/shims/flutter_inappwebview_fake.dart'
if (dart.library.io) 'package:flutter_inappwebview/flutter_inappwebview.dart';
/// Defines the 3 different cases for file insertion failing
enum UploadError { unsupportedFile, exceededMaxSize, jsException }
/// Manages the notification type for a notification displayed at the bottom of
/// the editor
enum NotificationType { info, warning, success, danger, plaintext }
/// Manages the way the toolbar displays:
/// [nativeGrid] - a grid view (non scrollable) of all the buttons
/// [nativeScrollable] - a scrollable one-line view of all the buttons
/// [nativeExpandable] - has an icon to switch between grid and scrollable formats
/// on the fly
/// [summernote] - uses the default summernote buttons (no native controls and
/// reduced feature support) //todo
enum ToolbarType { nativeGrid, nativeScrollable, nativeExpandable }
/// Manages the position of the toolbar, whether above or below the editor
/// [custom] - removes the toolbar. This is useful when you want to implement the
/// toolbar in a custom location using [ToolbarWidget]
///
/// Note: This is ignored when [ToolbarType.summernote] is set.
enum ToolbarPosition { aboveEditor, belowEditor, custom }
/// Returns the type of button pressed in the `onButtonPressed` function
enum ButtonType {
style,
bold,
italic,
underline,
clearFormatting,
strikethrough,
superscript,
subscript,
foregroundColor,
highlightColor,
ul,
ol,
alignLeft,
alignCenter,
alignRight,
alignJustify,
increaseIndent,
decreaseIndent,
ltr,
rtl,
link,
picture,
audio,
video,
otherFile,
table,
hr,
fullscreen,
codeview,
undo,
redo,
help,
copy,
paste
}
/// Returns the type of dropdown changed in the `onDropdownChanged` function
enum DropdownType {
style,
fontName,
fontSize,
fontSizeUnit,
listStyles,
lineHeight,
caseConverter
}
/// Sets the direction the dropdown menu opens
enum DropdownMenuDirection { down, up }
/// Returns the type of file inserted in `onLinkInsertInt
enum InsertFileType { image, audio, video }
/// Sets how the virtual keyboard appears on mobile devices
enum HtmlInputType { decimal, email, numeric, tel, url, text }

View File

@@ -0,0 +1,291 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:html_editor_enhanced/html_editor.dart';
import 'package:html_editor_enhanced/src/html_editor_controller_unsupported.dart'
as unsupported;
/// Controller for mobile
class HtmlEditorController extends unsupported.HtmlEditorController {
HtmlEditorController({
this.processInputHtml = true,
this.processNewLineAsBr = false,
this.processOutputHtml = true,
});
/// Toolbar widget state to call various methods. For internal use only.
@override
ToolbarWidgetState? toolbar;
/// Determines whether text processing should happen on input HTML, e.g.
/// whether a new line should be converted to a <br>.
///
/// The default value is false.
@override
final bool processInputHtml;
/// Determines whether newlines (\n) should be written as <br>. This is not
/// recommended for HTML documents.
///
/// The default value is false.
@override
final bool processNewLineAsBr;
/// Determines whether text processing should happen on output HTML, e.g.
/// whether <p><br></p> is returned as "". For reference, Summernote uses
/// that HTML as the default HTML (when no text is in the editor).
///
/// The default value is true.
@override
final bool processOutputHtml;
/// Manages the [InAppWebViewController] for the [HtmlEditorController]
InAppWebViewController? _editorController;
/// Allows the [InAppWebViewController] for the Html editor to be accessed
/// outside of the package itself for endless control and customization.
@override
// ignore: unnecessary_getters_setters
InAppWebViewController? get editorController => _editorController;
/// Internal method to set the [InAppWebViewController] when webview initialization
/// is complete
@override
// ignore: unnecessary_getters_setters
set editorController(dynamic controller) =>
_editorController = controller as InAppWebViewController?;
/// A function to quickly call a document.execCommand function in a readable format
@override
void execCommand(String command, {String? argument}) {
_evaluateJavascript(
source:
"document.execCommand('$command', false${argument == null ? "" : ", '$argument'"});");
}
/// Gets the text from the editor and returns it as a [String].
@override
Future<String> getText() async {
var text = await _evaluateJavascript(
source: "\$('#summernote-2').summernote('code');") as String?;
if (processOutputHtml &&
(text == null ||
text.isEmpty ||
text == '<p></p>' ||
text == '<p><br></p>' ||
text == '<p><br/></p>')) text = '';
return text ?? '';
}
/// Sets the text of the editor. Some pre-processing is applied to convert
/// [String] elements like "\n" to HTML elements.
@override
void setText(String text) {
text = _processHtml(html: text);
_evaluateJavascript(
source: "\$('#summernote-2').summernote('code', '$text');");
}
/// Sets the editor to full-screen mode.
@override
void setFullScreen() {
_evaluateJavascript(
source: '\$("#summernote-2").summernote("fullscreen.toggle");');
}
/// Sets the focus to the editor.
@override
void setFocus() {
_evaluateJavascript(source: "\$('#summernote-2').summernote('focus');");
}
/// Clears the editor of any text.
@override
void clear() {
_evaluateJavascript(source: "\$('#summernote-2').summernote('reset');");
}
/// Sets the hint for the editor.
@override
void setHint(String text) {
text = _processHtml(html: text);
var hint = '\$(".note-placeholder").html("$text");';
_evaluateJavascript(source: hint);
}
/// toggles the codeview in the Html editor
@override
void toggleCodeView() {
_evaluateJavascript(
source: "\$('#summernote-2').summernote('codeview.toggle');");
}
/// disables the Html editor
@override
void disable() {
toolbar!.disable();
_evaluateJavascript(source: "\$('#summernote-2').summernote('disable');");
}
/// enables the Html editor
@override
void enable() {
toolbar!.enable();
_evaluateJavascript(source: "\$('#summernote-2').summernote('enable');");
}
/// Undoes the last action
@override
void undo() {
_evaluateJavascript(source: "\$('#summernote-2').summernote('undo');");
}
/// Redoes the last action
@override
void redo() {
_evaluateJavascript(source: "\$('#summernote-2').summernote('redo');");
}
/// Insert text at the end of the current HTML content in the editor
/// Note: This method should only be used for plaintext strings
@override
void insertText(String text) {
_evaluateJavascript(
source: "\$('#summernote-2').summernote('insertText', '$text');");
}
/// Insert HTML at the position of the cursor in the editor
/// Note: This method should not be used for plaintext strings
@override
void insertHtml(String html) {
html = _processHtml(html: html);
_evaluateJavascript(
source: "\$('#summernote-2').summernote('pasteHTML', '$html');");
}
/// Insert a network image at the position of the cursor in the editor
@override
void insertNetworkImage(String url, {String filename = ''}) {
_evaluateJavascript(
source:
"\$('#summernote-2').summernote('insertImage', '$url', '$filename');");
}
/// Insert a link at the position of the cursor in the editor
@override
void insertLink(String text, String url, bool isNewWindow) {
_evaluateJavascript(source: """
\$('#summernote-2').summernote('createLink', {
text: "$text",
url: '$url',
isNewWindow: $isNewWindow
});
""");
}
/// Clears the focus from the webview by hiding the keyboard, calling the
/// clearFocus method on the [InAppWebViewController], and resetting the height
/// in case it was changed.
@override
void clearFocus() {
SystemChannels.textInput.invokeMethod('TextInput.hide');
}
/// Reloads the IFrameElement, throws an exception on mobile
@override
void reloadWeb() {
throw Exception(
'Non-Flutter Web environment detected, please make sure you are importing package:html_editor_enhanced/html_editor.dart and check kIsWeb before calling this function');
}
/// Resets the height of the editor back to the original if it was changed to
/// accommodate the keyboard. This should only be used on mobile, and only
/// when [adjustHeightForKeyboard] is enabled.
@override
void resetHeight() {
_evaluateJavascript(
source:
"window.flutter_inappwebview.callHandler('setHeight', 'reset');");
}
/// Recalculates the height of the editor to remove any vertical scrolling.
/// This method will not do anything if [autoAdjustHeight] is turned off.
@override
void recalculateHeight() {
_evaluateJavascript(
source:
"var height = document.body.scrollHeight; window.flutter_inappwebview.callHandler('setHeight', height);");
}
/// Add a notification to the bottom of the editor. This is styled similar to
/// Bootstrap alerts. You can set the HTML to be displayed in the alert,
/// and the notificationType determines how the alert is displayed.
@override
void addNotification(String html, NotificationType notificationType) async {
await _evaluateJavascript(source: """
\$('.note-status-output').html(
'<div class="alert alert-${describeEnum(notificationType)}">$html</div>'
);
""");
recalculateHeight();
}
/// Remove the current notification from the bottom of the editor
@override
void removeNotification() async {
await _evaluateJavascript(source: "\$('.note-status-output').empty();");
recalculateHeight();
}
/// Helper function to process input html
String _processHtml({required html}) {
if (processInputHtml) {
html = html
.replaceAll("'", r"\'")
.replaceAll('"', r'\"')
.replaceAll('\r', '')
.replaceAll('\r\n', '');
}
if (processNewLineAsBr) {
html = html.replaceAll('\n', '<br/>').replaceAll('\n\n', '<br/>');
} else {
html = html.replaceAll('\n', '').replaceAll('\n\n', '');
}
return html;
}
/// Helper function to evaluate JS and check the current environment
dynamic _evaluateJavascript({required source}) async {
if (!kIsWeb) {
if (editorController == null || await editorController!.isLoading()) {
throw Exception(
'HTML editor is still loading, please wait before evaluating this JS: $source!');
}
var result = await editorController!.evaluateJavascript(source: source);
return result;
} else {
throw Exception(
'Flutter Web environment detected, please make sure you are importing package:html_editor_enhanced/html_editor.dart');
}
}
/// Internal function to change list style on Web
@override
void changeListStyle(String changed) {}
/// Internal function to change line height on Web
@override
void changeLineHeight(String changed) {}
/// Internal function to change text direction on Web
@override
void changeTextDirection(String changed) {}
/// Internal function to change case on Web
@override
void changeCase(String changed) {}
/// Internal function to insert table on Web
@override
void insertTable(String dimensions) {}
}

View File

@@ -0,0 +1,174 @@
import 'package:html_editor_enhanced/html_editor.dart';
import 'package:meta/meta.dart';
/// Fallback controller (should never be used)
class HtmlEditorController {
HtmlEditorController({
this.processInputHtml = true,
this.processNewLineAsBr = false,
this.processOutputHtml = true,
});
/// Toolbar widget state to call various methods. For internal use only.
@internal
ToolbarWidgetState? toolbar;
/// Determines whether text processing should happen on input HTML, e.g.
/// whether a new line should be converted to a <br>.
///
/// The default value is true.
final bool processInputHtml;
/// Determines whether newlines (\n) should be written as <br>. This is not
/// recommended for HTML documents.
///
/// The default value is false.
final bool processNewLineAsBr;
/// Determines whether text processing should happen on output HTML, e.g.
/// whether <p><br></p> is returned as "". For reference, Summernote uses
/// that HTML as the default HTML (when no text is in the editor).
///
/// The default value is true.
final bool processOutputHtml;
/// Internally tracks the character count in the editor
int _characterCount = 0;
/// Gets the current character count
// ignore: unnecessary_getters_setters
int get characterCount => _characterCount;
/// Sets the current character count. Marked as internal method - this should
/// not be used outside of the package itself.
// ignore: unnecessary_getters_setters
@internal
set characterCount(int count) => _characterCount = count;
/// Allows the [InAppWebViewController] for the Html editor to be accessed
/// outside of the package itself for endless control and customization.
dynamic get editorController => null;
/// Internal method to set the [InAppWebViewController] when webview initialization
/// is complete
@internal
set editorController(dynamic controller) => {};
/// Internal method to set the view ID when iframe initialization
/// is complete
@internal
set viewId(String? viewId) => {};
/// Add a notification to the bottom of the editor. This is styled similar to
/// Bootstrap alerts. You can set the HTML to be displayed in the alert,
/// and the notificationType determines how the alert is displayed.
void addNotification(String html, NotificationType notificationType) {}
/// Clears the editor of any text.
void clear() {}
/// Clears the focus from the webview by hiding the keyboard, calling the
/// clearFocus method on the [InAppWebViewController], and resetting the height
/// in case it was changed.
void clearFocus() {}
/// disables the Html editor
void disable() {}
/// enables the Html editor
void enable() {}
/// A function to quickly call a document.execCommand function in a readable format
void execCommand(String command, {String? argument}) {}
/// A function to execute JS passed as a [WebScript] to the editor. This should
/// only be used on Flutter Web.
Future<dynamic> evaluateJavascriptWeb(String name,
{bool hasReturnValue = false}) =>
Future.value();
/// Gets the text from the editor and returns it as a [String].
Future<String> getText() => Future.value('');
/// Gets the selected HTML from the editor. You should use
/// [controller.editorController.getSelectedText()] on mobile.
///
/// [withHtmlTags] may not work properly when the selected text is entirely
/// within one HTML tag. However if the selected text spans multiple different
/// tags, it should work as expected.
Future<String> getSelectedTextWeb({bool withHtmlTags = false}) =>
Future.value('');
/// Insert HTML at the position of the cursor in the editor
/// Note: This method should not be used for plaintext strings
void insertHtml(String html) {}
/// Insert a link at the position of the cursor in the editor
void insertLink(String text, String url, bool isNewWindow) {}
/// Insert a network image at the position of the cursor in the editor
void insertNetworkImage(String url, {String filename = ''}) {}
/// Insert text at the end of the current HTML content in the editor
/// Note: This method should only be used for plaintext strings
void insertText(String text) {}
/// Recalculates the height of the editor to remove any vertical scrolling.
/// This method will not do anything if [autoAdjustHeight] is turned off.
void recalculateHeight() {}
/// Redoes the last action
void redo() {}
/// Refresh the page
///
/// Note: This should only be used in Flutter Web!!!
void reloadWeb() {}
/// Remove the current notification from the bottom of the editor
void removeNotification() {}
/// Resets the height of the editor back to the original if it was changed to
/// accommodate the keyboard. This should only be used on mobile, and only
/// when [adjustHeightForKeyboard] is enabled.
void resetHeight() {}
/// Sets the hint for the editor.
void setHint(String text) {}
/// Sets the focus to the editor.
void setFocus() {}
/// Sets the editor to full-screen mode.
void setFullScreen() {}
/// Sets the text of the editor. Some pre-processing is applied to convert
/// [String] elements like "\n" to HTML elements.
void setText(String text) {}
/// toggles the codeview in the Html editor
void toggleCodeView() {}
/// Undoes the last action
void undo() {}
/// Internal function to change list style on Web
@internal
void changeListStyle(String changed) {}
/// Internal function to change line height on Web
@internal
void changeLineHeight(String changed) {}
/// Internal function to change text direction on Web
@internal
void changeTextDirection(String changed) {}
/// Internal function to change case on Web
@internal
void changeCase(String changed) {}
/// Internal function to insert table on Web
@internal
void insertTable(String dimensions) {}
}

View File

@@ -0,0 +1,329 @@
import 'dart:convert';
// ignore: avoid_web_libraries_in_flutter
import 'dart:html' as html;
import 'package:flutter/foundation.dart';
import 'package:html_editor_enhanced/html_editor.dart';
import 'package:html_editor_enhanced/src/html_editor_controller_unsupported.dart'
as unsupported;
import 'package:meta/meta.dart';
/// Controller for web
class HtmlEditorController extends unsupported.HtmlEditorController {
HtmlEditorController({
this.processInputHtml = true,
this.processNewLineAsBr = false,
this.processOutputHtml = true,
});
/// Toolbar widget state to call various methods. For internal use only.
@override
ToolbarWidgetState? toolbar;
/// Determines whether text processing should happen on input HTML, e.g.
/// whether a new line should be converted to a <br>.
///
/// The default value is true.
@override
final bool processInputHtml;
/// Determines whether newlines (\n) should be written as <br>. This is not
/// recommended for HTML documents.
///
/// The default value is false.
@override
final bool processNewLineAsBr;
/// Determines whether text processing should happen on output HTML, e.g.
/// whether <p><br></p> is returned as "". For reference, Summernote uses
/// that HTML as the default HTML (when no text is in the editor).
///
/// The default value is true.
@override
final bool processOutputHtml;
/// Manages the view ID for the [HtmlEditorController] on web
String? _viewId;
/// Internal method to set the view ID when iframe initialization
/// is complete
@override
@internal
set viewId(String? viewId) => _viewId = viewId;
/// Gets the text from the editor and returns it as a [String].
@override
Future<String> getText() async {
_evaluateJavascriptWeb(data: {'type': 'toIframe: getText'});
var e = await html.window.onMessage.firstWhere(
(element) => json.decode(element.data)['type'] == 'toDart: getText');
String text = json.decode(e.data)['text'];
if (processOutputHtml &&
(text.isEmpty ||
text == '<p></p>' ||
text == '<p><br></p>' ||
text == '<p><br/></p>')) text = '';
return text;
}
@override
Future<String> getSelectedTextWeb({bool withHtmlTags = false}) async {
if (withHtmlTags) {
_evaluateJavascriptWeb(data: {'type': 'toIframe: getSelectedTextHtml'});
} else {
_evaluateJavascriptWeb(data: {'type': 'toIframe: getSelectedText'});
}
var e = await html.window.onMessage.firstWhere((element) =>
json.decode(element.data)['type'] == 'toDart: getSelectedText');
return json.decode(e.data)['text'];
}
/// Sets the text of the editor. Some pre-processing is applied to convert
/// [String] elements like "\n" to HTML elements.
@override
void setText(String text) {
text = _processHtml(html: text);
_evaluateJavascriptWeb(data: {'type': 'toIframe: setText', 'text': text});
}
/// Sets the editor to full-screen mode.
@override
void setFullScreen() {
_evaluateJavascriptWeb(data: {'type': 'toIframe: setFullScreen'});
}
/// Sets the focus to the editor.
@override
void setFocus() {
_evaluateJavascriptWeb(data: {'type': 'toIframe: setFocus'});
}
/// Clears the editor of any text.
@override
void clear() {
_evaluateJavascriptWeb(data: {'type': 'toIframe: clear'});
}
/// Sets the hint for the editor.
@override
void setHint(String text) {
text = _processHtml(html: text);
_evaluateJavascriptWeb(data: {'type': 'toIframe: setHint', 'text': text});
}
/// toggles the codeview in the Html editor
@override
void toggleCodeView() {
_evaluateJavascriptWeb(data: {'type': 'toIframe: toggleCodeview'});
}
/// disables the Html editor
@override
void disable() {
toolbar!.disable();
_evaluateJavascriptWeb(data: {'type': 'toIframe: disable'});
}
/// enables the Html editor
@override
void enable() {
toolbar!.enable();
_evaluateJavascriptWeb(data: {'type': 'toIframe: enable'});
}
/// Undoes the last action
@override
void undo() {
_evaluateJavascriptWeb(data: {'type': 'toIframe: undo'});
}
/// Redoes the last action
@override
void redo() {
_evaluateJavascriptWeb(data: {'type': 'toIframe: redo'});
}
/// Insert text at the end of the current HTML content in the editor
/// Note: This method should only be used for plaintext strings
@override
void insertText(String text) {
_evaluateJavascriptWeb(
data: {'type': 'toIframe: insertText', 'text': text});
}
/// Insert HTML at the position of the cursor in the editor
/// Note: This method should not be used for plaintext strings
@override
void insertHtml(String html) {
html = _processHtml(html: html);
_evaluateJavascriptWeb(
data: {'type': 'toIframe: insertHtml', 'html': html});
}
/// Insert a network image at the position of the cursor in the editor
@override
void insertNetworkImage(String url, {String filename = ''}) {
_evaluateJavascriptWeb(data: {
'type': 'toIframe: insertNetworkImage',
'url': url,
'filename': filename
});
}
/// Insert a link at the position of the cursor in the editor
@override
void insertLink(String text, String url, bool isNewWindow) {
_evaluateJavascriptWeb(data: {
'type': 'toIframe: insertLink',
'text': text,
'url': url,
'isNewWindow': isNewWindow
});
}
/// Clears the focus from the webview by hiding the keyboard, calling the
/// clearFocus method on the [InAppWebViewController], and resetting the height
/// in case it was changed.
@override
void clearFocus() {
throw Exception(
'Flutter Web environment detected, please make sure you are importing package:html_editor_enhanced/html_editor.dart and check kIsWeb before calling this method.');
}
/// Resets the height of the editor back to the original if it was changed to
/// accommodate the keyboard. This should only be used on mobile, and only
/// when [adjustHeightForKeyboard] is enabled.
@override
void resetHeight() {
throw Exception(
'Flutter Web environment detected, please make sure you are importing package:html_editor_enhanced/html_editor.dart and check kIsWeb before calling this method.');
}
/// Refresh the page
///
/// Note: This should only be used in Flutter Web!!!
@override
void reloadWeb() {
_evaluateJavascriptWeb(data: {'type': 'toIframe: reload'});
}
/// Recalculates the height of the editor to remove any vertical scrolling.
/// This method will not do anything if [autoAdjustHeight] is turned off.
@override
void recalculateHeight() {
_evaluateJavascriptWeb(data: {
'type': 'toIframe: getHeight',
});
}
/// A function to quickly call a document.execCommand function in a readable format
@override
void execCommand(String command, {String? argument}) {
_evaluateJavascriptWeb(data: {
'type': 'toIframe: execCommand',
'command': command,
'argument': argument
});
}
/// A function to execute JS passed as a [WebScript] to the editor. This should
/// only be used on Flutter Web.
@override
Future<dynamic> evaluateJavascriptWeb(String name,
{bool hasReturnValue = false}) async {
_evaluateJavascriptWeb(data: {'type': 'toIframe: $name'});
if (hasReturnValue) {
var e = await html.window.onMessage.firstWhere(
(element) => json.decode(element.data)['type'] == 'toDart: $name');
return json.decode(e.data);
}
}
/// Internal function to change list style on Web
@override
void changeListStyle(String changed) {
_evaluateJavascriptWeb(
data: {'type': 'toIframe: changeListStyle', 'changed': changed});
}
/// Internal function to change line height on Web
@override
void changeLineHeight(String changed) {
_evaluateJavascriptWeb(
data: {'type': 'toIframe: changeLineHeight', 'changed': changed});
}
/// Internal function to change text direction on Web
@override
void changeTextDirection(String direction) {
_evaluateJavascriptWeb(data: {
'type': 'toIframe: changeTextDirection',
'direction': direction
});
}
/// Internal function to change case on Web
@override
void changeCase(String changed) {
_evaluateJavascriptWeb(
data: {'type': 'toIframe: changeCase', 'case': changed});
}
/// Internal function to insert table on Web
@override
void insertTable(String dimensions) {
_evaluateJavascriptWeb(
data: {'type': 'toIframe: insertTable', 'dimensions': dimensions});
}
/// Add a notification to the bottom of the editor. This is styled similar to
/// Bootstrap alerts. You can set the HTML to be displayed in the alert,
/// and the notificationType determines how the alert is displayed.
@override
void addNotification(String html, NotificationType notificationType) {
if (notificationType == NotificationType.plaintext) {
_evaluateJavascriptWeb(
data: {'type': 'toIframe: addNotification', 'html': html});
} else {
_evaluateJavascriptWeb(data: {
'type': 'toIframe: addNotification',
'html': html,
'alertType': 'alert alert-${describeEnum(notificationType)}'
});
}
recalculateHeight();
}
/// Remove the current notification from the bottom of the editor
@override
void removeNotification() {
_evaluateJavascriptWeb(data: {'type': 'toIframe: removeNotification'});
recalculateHeight();
}
/// Helper function to process input html
String _processHtml({required html}) {
if (processInputHtml) {
html = html.replaceAll('\r', '').replaceAll('\r\n', '');
}
if (processNewLineAsBr) {
html = html.replaceAll('\n', '<br/>').replaceAll('\n\n', '<br/>');
} else {
html = html.replaceAll('\n', '').replaceAll('\n\n', '');
}
return html;
}
/// Helper function to run javascript and check current environment
void _evaluateJavascriptWeb({required Map<String, Object?> data}) async {
if (kIsWeb) {
data['view'] = _viewId;
final jsonEncoder = JsonEncoder();
var json = jsonEncoder.convert(data);
html.window.postMessage(json, '*');
} else {
throw Exception(
'Non-Flutter Web environment detected, please make sure you are importing package:html_editor_enhanced/html_editor.dart');
}
}
}

View File

@@ -0,0 +1,57 @@
import 'package:html_editor_enhanced/html_editor.dart'
hide HtmlEditorController;
import 'package:html_editor_enhanced/src/html_editor_controller_mobile.dart';
import 'package:html_editor_enhanced/src/widgets/html_editor_widget_mobile.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
/// HtmlEditor class for mobile
class HtmlEditor extends StatelessWidget {
HtmlEditor({
Key? key,
required this.controller,
this.callbacks,
this.htmlEditorOptions = const HtmlEditorOptions(),
this.htmlToolbarOptions = const HtmlToolbarOptions(),
this.otherOptions = const OtherOptions(),
this.plugins = const [],
}) : super(key: key);
/// The controller that is passed to the widget, which allows multiple [HtmlEditor]
/// widgets to be used on the same page independently.
final HtmlEditorController controller;
/// Sets & activates Summernote's callbacks. See the functions available in
/// [Callbacks] for more details.
final Callbacks? callbacks;
/// Defines options for the html editor
final HtmlEditorOptions htmlEditorOptions;
/// Defines options for the editor toolbar
final HtmlToolbarOptions htmlToolbarOptions;
/// Defines other options
final OtherOptions otherOptions;
/// Sets the list of Summernote plugins enabled in the editor.
final List<Plugins> plugins;
@override
Widget build(BuildContext context) {
if (!kIsWeb) {
return HtmlEditorWidget(
key: key,
controller: controller,
callbacks: callbacks,
plugins: plugins,
htmlEditorOptions: htmlEditorOptions,
htmlToolbarOptions: htmlToolbarOptions,
otherOptions: otherOptions,
);
} else {
return Text(
'Flutter Web environment detected, please make sure you are importing package:html_editor_enhanced/html_editor.dart');
}
}
}

View File

@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:html_editor_enhanced/html_editor.dart';
/// Fallback HtmlEditor class (should never be called)
class HtmlEditor extends StatelessWidget {
HtmlEditor({
Key? key,
required this.controller,
this.callbacks,
this.htmlEditorOptions = const HtmlEditorOptions(),
this.htmlToolbarOptions = const HtmlToolbarOptions(),
this.otherOptions = const OtherOptions(),
this.plugins = const [],
}) : super(key: key);
/// The controller that is passed to the widget, which allows multiple [HtmlEditor]
/// widgets to be used on the same page independently.
final HtmlEditorController controller;
/// Sets & activates Summernote's callbacks. See the functions available in
/// [Callbacks] for more details.
final Callbacks? callbacks;
/// Defines options for the html editor
final HtmlEditorOptions htmlEditorOptions;
/// Defines options for the editor toolbar
final HtmlToolbarOptions htmlToolbarOptions;
/// Defines other options
final OtherOptions otherOptions;
/// Sets the list of Summernote plugins enabled in the editor.
final List<Plugins> plugins;
@override
Widget build(BuildContext context) {
return Text('Unsupported in this environment');
}
}

View File

@@ -0,0 +1,56 @@
import 'package:html_editor_enhanced/html_editor.dart';
import 'package:html_editor_enhanced/src/widgets/html_editor_widget_web.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
/// HtmlEditor class for web
class HtmlEditor extends StatelessWidget {
HtmlEditor({
Key? key,
required this.controller,
this.callbacks,
this.htmlEditorOptions = const HtmlEditorOptions(),
this.htmlToolbarOptions = const HtmlToolbarOptions(),
this.otherOptions = const OtherOptions(),
this.plugins = const [],
}) : super(key: key);
/// The controller that is passed to the widget, which allows multiple [HtmlEditor]
/// widgets to be used on the same page independently.
final HtmlEditorController controller;
/// Sets & activates Summernote's callbacks. See the functions available in
/// [Callbacks] for more details.
final Callbacks? callbacks;
/// Defines options for the html editor
final HtmlEditorOptions htmlEditorOptions;
/// Defines options for the editor toolbar
final HtmlToolbarOptions htmlToolbarOptions;
/// Defines other options
final OtherOptions otherOptions;
/// Sets the list of Summernote plugins enabled in the editor.
final List<Plugins> plugins;
@override
Widget build(BuildContext context) {
if (kIsWeb) {
return HtmlEditorWidget(
key: key,
controller: controller,
callbacks: callbacks,
plugins: plugins,
htmlEditorOptions: htmlEditorOptions,
htmlToolbarOptions: htmlToolbarOptions,
otherOptions: otherOptions,
initBC: context,
);
} else {
return Text(
'Non-Flutter Web environment detected, please make sure you are importing package:html_editor_enhanced/html_editor.dart');
}
}
}

View File

@@ -0,0 +1,791 @@
import 'dart:async';
import 'dart:collection';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'package:html_editor_enhanced/html_editor.dart'
hide NavigationActionPolicy, UserScript, ContextMenu;
import 'package:html_editor_enhanced/utils/utils.dart';
import 'package:visibility_detector/visibility_detector.dart';
/// The HTML Editor widget itself, for mobile (uses InAppWebView)
class HtmlEditorWidget extends StatefulWidget {
HtmlEditorWidget({
Key? key,
required this.controller,
this.callbacks,
required this.plugins,
required this.htmlEditorOptions,
required this.htmlToolbarOptions,
required this.otherOptions,
}) : super(key: key);
final HtmlEditorController controller;
final Callbacks? callbacks;
final List<Plugins> plugins;
final HtmlEditorOptions htmlEditorOptions;
final HtmlToolbarOptions htmlToolbarOptions;
final OtherOptions otherOptions;
@override
_HtmlEditorWidgetMobileState createState() => _HtmlEditorWidgetMobileState();
}
/// State for the mobile Html editor widget
///
/// A stateful widget is necessary here to allow the height to dynamically adjust.
class _HtmlEditorWidgetMobileState extends State<HtmlEditorWidget> {
/// Tracks whether the callbacks were initialized or not to prevent re-initializing them
bool callbacksInitialized = false;
/// The height of the document loaded in the editor
late double docHeight;
/// The file path to the html code
late String filePath;
/// String to use when creating the key for the widget
late String key;
/// Stream to transfer the [VisibilityInfo.visibleFraction] to the [onWindowFocus]
/// function of the webview
StreamController<double> visibleStream = StreamController<double>.broadcast();
/// Helps get the height of the toolbar to accurately adjust the height of
/// the editor when the keyboard is visible.
GlobalKey toolbarKey = GlobalKey();
/// Variable to cache the viewable size of the editor to update it in case
/// the editor is focused much after its visibility changes
double? cachedVisibleDecimal;
@override
void initState() {
docHeight = widget.otherOptions.height;
key = getRandString(10);
if (widget.htmlEditorOptions.filePath != null) {
filePath = widget.htmlEditorOptions.filePath!;
} else if (widget.plugins.isEmpty) {
filePath =
'packages/html_editor_enhanced/assets/summernote-no-plugins.html';
} else {
filePath = 'packages/html_editor_enhanced/assets/summernote.html';
}
super.initState();
}
@override
void dispose() {
visibleStream.close();
super.dispose();
}
/// resets the height of the editor to the original height
void resetHeight() async {
if (mounted) {
this.setState(() {
docHeight = widget.otherOptions.height;
});
await widget.controller.editorController!.evaluateJavascript(
source:
"\$('div.note-editable').outerHeight(${widget.otherOptions.height - (toolbarKey.currentContext?.size?.height ?? 0)});");
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
SystemChannels.textInput.invokeMethod('TextInput.hide');
},
child: VisibilityDetector(
key: Key(key),
onVisibilityChanged: (VisibilityInfo info) async {
if (!visibleStream.isClosed) {
cachedVisibleDecimal = info.visibleFraction == 1
? (info.size.height / widget.otherOptions.height).clamp(0, 1)
: info.visibleFraction;
visibleStream.add(info.visibleFraction == 1
? (info.size.height / widget.otherOptions.height).clamp(0, 1)
: info.visibleFraction);
}
},
child: Container(
height: docHeight + 10,
decoration: widget.otherOptions.decoration,
child: Column(
children: [
widget.htmlToolbarOptions.toolbarPosition ==
ToolbarPosition.aboveEditor
? ToolbarWidget(
key: toolbarKey,
controller: widget.controller,
htmlToolbarOptions: widget.htmlToolbarOptions,
callbacks: widget.callbacks)
: Container(height: 0, width: 0),
Expanded(
child: InAppWebView(
initialFile: filePath,
onWebViewCreated: (InAppWebViewController controller) {
widget.controller.editorController = controller;
controller.addJavaScriptHandler(
handlerName: 'FormatSettings',
callback: (e) {
var json = e[0] as Map<String, dynamic>;
print(json);
if (widget.controller.toolbar != null) {
widget.controller.toolbar!.updateToolbar(json);
}
});
},
initialOptions: InAppWebViewGroupOptions(
crossPlatform: InAppWebViewOptions(
javaScriptEnabled: true,
transparentBackground: true,
useShouldOverrideUrlLoading: true,
),
android: AndroidInAppWebViewOptions(
useHybridComposition: widget
.htmlEditorOptions.androidUseHybridComposition,
loadWithOverviewMode: true,
)),
initialUserScripts:
widget.htmlEditorOptions.mobileInitialScripts
as UnmodifiableListView<UserScript>?,
contextMenu: widget.htmlEditorOptions.mobileContextMenu
as ContextMenu?,
gestureRecognizers: {
Factory<VerticalDragGestureRecognizer>(
() => VerticalDragGestureRecognizer()),
Factory<LongPressGestureRecognizer>(() =>
LongPressGestureRecognizer(
duration: widget
.htmlEditorOptions.mobileLongPressDuration)),
},
shouldOverrideUrlLoading: (controller, action) async {
if (!action.request.url.toString().contains(filePath)) {
return (await widget.callbacks?.onNavigationRequestMobile
?.call(action.request.url.toString()))
as NavigationActionPolicy? ??
NavigationActionPolicy.ALLOW;
}
return NavigationActionPolicy.ALLOW;
},
onConsoleMessage: (controller, message) {
print(message.message);
},
onWindowFocus: (controller) async {
if (widget.htmlEditorOptions.shouldEnsureVisible &&
Scrollable.of(context) != null) {
await Scrollable.of(context)!.position.ensureVisible(
context.findRenderObject()!,
);
}
if (widget.htmlEditorOptions.adjustHeightForKeyboard &&
mounted &&
!visibleStream.isClosed) {
Future<void> setHeightJS() async {
await controller.evaluateJavascript(source: """
\$('div.note-editable').outerHeight(${max(docHeight - (toolbarKey.currentContext?.size?.height ?? 0), 30)});
// from https://stackoverflow.com/a/67152280
var selection = window.getSelection();
if (selection.rangeCount) {
var firstRange = selection.getRangeAt(0);
if (firstRange.commonAncestorContainer !== document) {
var tempAnchorEl = document.createElement('br');
firstRange.insertNode(tempAnchorEl);
tempAnchorEl.scrollIntoView({
block: 'end',
});
tempAnchorEl.remove();
}
}
""");
}
/// this is a workaround so jumping between focus on different
/// editable elements still resizes the editor
if ((cachedVisibleDecimal ?? 0) > 0.1) {
this.setState(() {
docHeight = widget.otherOptions.height *
cachedVisibleDecimal!;
});
await setHeightJS();
}
var visibleDecimal = await visibleStream.stream.first;
var newHeight = widget.otherOptions.height;
if (visibleDecimal > 0.1) {
this.setState(() {
docHeight = newHeight * visibleDecimal;
});
//todo add support for traditional summernote controls again?
await setHeightJS();
}
}
},
onLoadStop:
(InAppWebViewController controller, Uri? uri) async {
var url = uri.toString();
var maximumFileSize = 10485760;
if (url.contains(filePath)) {
var summernoteToolbar = '[\n';
var summernoteCallbacks = '''callbacks: {
onKeydown: function(e) {
var chars = \$(".note-editable").text();
var totalChars = chars.length;
${widget.htmlEditorOptions.characterLimit != null ? '''allowedKeys = (
e.which === 8 || /* BACKSPACE */
e.which === 35 || /* END */
e.which === 36 || /* HOME */
e.which === 37 || /* LEFT */
e.which === 38 || /* UP */
e.which === 39 || /* RIGHT*/
e.which === 40 || /* DOWN */
e.which === 46 || /* DEL*/
e.ctrlKey === true && e.which === 65 || /* CTRL + A */
e.ctrlKey === true && e.which === 88 || /* CTRL + X */
e.ctrlKey === true && e.which === 67 || /* CTRL + C */
e.ctrlKey === true && e.which === 86 || /* CTRL + V */
e.ctrlKey === true && e.which === 90 /* CTRL + Z */
);
if (!allowedKeys && \$(e.target).text().length >= ${widget.htmlEditorOptions.characterLimit}) {
e.preventDefault();
}''' : ''}
window.flutter_inappwebview.callHandler('totalChars', totalChars);
},
''';
if (widget.plugins.isNotEmpty) {
summernoteToolbar = summernoteToolbar + "['plugins', [";
for (var p in widget.plugins) {
summernoteToolbar = summernoteToolbar +
(p.getToolbarString().isNotEmpty
? "'${p.getToolbarString()}'"
: '') +
(p == widget.plugins.last
? ']]\n'
: p.getToolbarString().isNotEmpty
? ', '
: '');
if (p is SummernoteAtMention) {
summernoteCallbacks = summernoteCallbacks +
"""
\nsummernoteAtMention: {
getSuggestions: async function(value) {
var result = await window.flutter_inappwebview.callHandler('getSuggestions', value);
var resultList = result.split(',');
return resultList;
},
onSelect: (value) => {
window.flutter_inappwebview.callHandler('onSelectMention', value);
},
},
""";
controller.addJavaScriptHandler(
handlerName: 'getSuggestions',
callback: (value) {
return p.getSuggestionsMobile!
.call(value.first.toString())
.toString()
.replaceAll('[', '')
.replaceAll(']', '');
});
if (p.onSelect != null) {
controller.addJavaScriptHandler(
handlerName: 'onSelectMention',
callback: (value) {
p.onSelect!.call(value.first.toString());
});
}
}
}
}
if (widget.callbacks != null) {
if (widget.callbacks!.onImageLinkInsert != null) {
summernoteCallbacks = summernoteCallbacks +
"""
onImageLinkInsert: function(url) {
window.flutter_inappwebview.callHandler('onImageLinkInsert', url);
},
""";
}
if (widget.callbacks!.onImageUpload != null) {
summernoteCallbacks = summernoteCallbacks +
"""
onImageUpload: function(files) {
var reader = new FileReader();
var base64 = "<an error occurred>";
reader.onload = function (_) {
base64 = reader.result;
var newObject = {
'lastModified': files[0].lastModified,
'lastModifiedDate': files[0].lastModifiedDate,
'name': files[0].name,
'size': files[0].size,
'type': files[0].type,
'base64': base64
};
window.flutter_inappwebview.callHandler('onImageUpload', JSON.stringify(newObject));
};
reader.onerror = function (_) {
var newObject = {
'lastModified': files[0].lastModified,
'lastModifiedDate': files[0].lastModifiedDate,
'name': files[0].name,
'size': files[0].size,
'type': files[0].type,
'base64': base64
};
window.flutter_inappwebview.callHandler('onImageUpload', JSON.stringify(newObject));
};
reader.readAsDataURL(files[0]);
},
""";
}
if (widget.callbacks!.onImageUploadError != null) {
summernoteCallbacks = summernoteCallbacks +
"""
onImageUploadError: function(file, error) {
if (typeof file === 'string') {
window.flutter_inappwebview.callHandler('onImageUploadError', file, error);
} else {
var newObject = {
'lastModified': file.lastModified,
'lastModifiedDate': file.lastModifiedDate,
'name': file.name,
'size': file.size,
'type': file.type,
};
window.flutter_inappwebview.callHandler('onImageUploadError', JSON.stringify(newObject), error);
}
},
""";
}
}
summernoteToolbar = summernoteToolbar + '],';
summernoteCallbacks = summernoteCallbacks + '}';
await controller.evaluateJavascript(source: """
\$('#summernote-2').summernote({
placeholder: "${widget.htmlEditorOptions.hint ?? ""}",
tabsize: 2,
height: ${widget.otherOptions.height - (toolbarKey.currentContext?.size?.height ?? 0)},
toolbar: $summernoteToolbar
disableGrammar: false,
spellCheck: ${widget.htmlEditorOptions.spellCheck},
maximumFileSize: $maximumFileSize,
${widget.htmlEditorOptions.customOptions}
$summernoteCallbacks
});
\$('#summernote-2').on('summernote.change', function(_, contents, \$editable) {
window.flutter_inappwebview.callHandler('onChangeContent', contents);
});
function onSelectionChange() {
let {anchorNode, anchorOffset, focusNode, focusOffset} = document.getSelection();
var isBold = false;
var isItalic = false;
var isUnderline = false;
var isStrikethrough = false;
var isSuperscript = false;
var isSubscript = false;
var isUL = false;
var isOL = false;
var isLeft = false;
var isRight = false;
var isCenter = false;
var isFull = false;
var parent;
var fontName;
var fontSize = 16;
var foreColor = "000000";
var backColor = "FFFF00";
var focusNode2 = \$(window.getSelection().focusNode);
var parentList = focusNode2.closest("div.note-editable ol, div.note-editable ul");
var parentListType = parentList.css('list-style-type');
var lineHeight = \$(focusNode.parentNode).css('line-height');
var direction = \$(focusNode.parentNode).css('direction');
if (document.queryCommandState) {
isBold = document.queryCommandState('bold');
isItalic = document.queryCommandState('italic');
isUnderline = document.queryCommandState('underline');
isStrikethrough = document.queryCommandState('strikeThrough');
isSuperscript = document.queryCommandState('superscript');
isSubscript = document.queryCommandState('subscript');
isUL = document.queryCommandState('insertUnorderedList');
isOL = document.queryCommandState('insertOrderedList');
isLeft = document.queryCommandState('justifyLeft');
isRight = document.queryCommandState('justifyRight');
isCenter = document.queryCommandState('justifyCenter');
isFull = document.queryCommandState('justifyFull');
}
if (document.queryCommandValue) {
parent = document.queryCommandValue('formatBlock');
fontSize = document.queryCommandValue('fontSize');
foreColor = document.queryCommandValue('foreColor');
backColor = document.queryCommandValue('hiliteColor');
fontName = document.queryCommandValue('fontName');
}
var message = {
'style': parent,
'fontName': fontName,
'fontSize': fontSize,
'font': [isBold, isItalic, isUnderline],
'miscFont': [isStrikethrough, isSuperscript, isSubscript],
'color': [foreColor, backColor],
'paragraph': [isUL, isOL],
'listStyle': parentListType,
'align': [isLeft, isCenter, isRight, isFull],
'lineHeight': lineHeight,
'direction': direction,
};
window.flutter_inappwebview.callHandler('FormatSettings', message);
}
""");
await controller.evaluateJavascript(
source:
"document.onselectionchange = onSelectionChange; console.log('done');");
await controller.evaluateJavascript(
source:
"document.getElementsByClassName('note-editable')[0].setAttribute('inputmode', '${describeEnum(widget.htmlEditorOptions.inputType)}');");
if ((Theme.of(context).brightness == Brightness.dark ||
widget.htmlEditorOptions.darkMode == true) &&
widget.htmlEditorOptions.darkMode != false) {
//todo fix for iOS (https://github.com/pichillilorenzo/flutter_inappwebview/issues/695)
var darkCSS =
'<link href=\"${(widget.htmlEditorOptions.filePath != null ? "file:///android_asset/flutter_assets/packages/html_editor_enhanced/assets/" : "") + "summernote-lite-dark.css"}\" rel=\"stylesheet\">';
await controller.evaluateJavascript(
source: "\$('head').append('$darkCSS');");
}
//set the text once the editor is loaded
if (widget.htmlEditorOptions.initialText != null) {
widget.controller
.setText(widget.htmlEditorOptions.initialText!);
}
//adjusts the height of the editor when it is loaded
if (widget.htmlEditorOptions.autoAdjustHeight) {
controller.addJavaScriptHandler(
handlerName: 'setHeight',
callback: (height) {
if (height.first == 'reset') {
resetHeight();
} else {
setState(mounted, this.setState, () {
docHeight = (double.tryParse(
height.first.toString()) ??
widget.otherOptions.height) +
(toolbarKey
.currentContext?.size?.height ??
0);
});
}
});
await controller.evaluateJavascript(
source:
"var height = document.body.scrollHeight; window.flutter_inappwebview.callHandler('setHeight', height);");
}
//reset the editor's height if the keyboard disappears at any point
if (widget.htmlEditorOptions.adjustHeightForKeyboard) {
var keyboardVisibilityController =
KeyboardVisibilityController();
keyboardVisibilityController.onChange
.listen((bool visible) {
if (!visible && mounted) {
controller.clearFocus();
resetHeight();
}
});
}
widget.controller.editorController!.addJavaScriptHandler(
handlerName: 'totalChars',
callback: (keyCode) {
widget.controller.characterCount =
keyCode.first as int;
});
//disable editor if necessary
if (widget.htmlEditorOptions.disabled &&
!callbacksInitialized) {
widget.controller.disable();
}
//initialize callbacks
if (widget.callbacks != null && !callbacksInitialized) {
addJSCallbacks(widget.callbacks!);
addJSHandlers(widget.callbacks!);
callbacksInitialized = true;
}
//call onInit callback
if (widget.callbacks != null &&
widget.callbacks!.onInit != null) {
widget.callbacks!.onInit!.call();
}
//add onChange handler
controller.addJavaScriptHandler(
handlerName: 'onChangeContent',
callback: (contents) {
if (widget.htmlEditorOptions.shouldEnsureVisible &&
Scrollable.of(context) != null) {
Scrollable.of(context)!.position.ensureVisible(
context.findRenderObject()!,
);
}
if (widget.callbacks != null &&
widget.callbacks!.onChangeContent != null) {
widget.callbacks!.onChangeContent!
.call(contents.first.toString());
}
});
}
},
),
),
widget.htmlToolbarOptions.toolbarPosition ==
ToolbarPosition.belowEditor
? ToolbarWidget(
key: toolbarKey,
controller: widget.controller,
htmlToolbarOptions: widget.htmlToolbarOptions,
callbacks: widget.callbacks)
: Container(height: 0, width: 0),
],
),
),
),
);
}
/// adds the callbacks set by the user into the scripts
void addJSCallbacks(Callbacks c) {
if (c.onBeforeCommand != null) {
widget.controller.editorController!.evaluateJavascript(source: """
\$('#summernote-2').on('summernote.before.command', function(_, contents) {
window.flutter_inappwebview.callHandler('onBeforeCommand', contents);
});
""");
}
if (c.onChangeCodeview != null) {
widget.controller.editorController!.evaluateJavascript(source: """
\$('#summernote-2').on('summernote.change.codeview', function(_, contents, \$editable) {
window.flutter_inappwebview.callHandler('onChangeCodeview', contents);
});
""");
}
if (c.onDialogShown != null) {
widget.controller.editorController!.evaluateJavascript(source: """
\$('#summernote-2').on('summernote.dialog.shown', function() {
window.flutter_inappwebview.callHandler('onDialogShown', 'fired');
});
""");
}
if (c.onEnter != null) {
widget.controller.editorController!.evaluateJavascript(source: """
\$('#summernote-2').on('summernote.enter', function() {
window.flutter_inappwebview.callHandler('onEnter', 'fired');
});
""");
}
if (c.onFocus != null) {
widget.controller.editorController!.evaluateJavascript(source: """
\$('#summernote-2').on('summernote.focus', function() {
window.flutter_inappwebview.callHandler('onFocus', 'fired');
});
""");
}
if (c.onBlur != null) {
widget.controller.editorController!.evaluateJavascript(source: """
\$('#summernote-2').on('summernote.blur', function() {
window.flutter_inappwebview.callHandler('onBlur', 'fired');
});
""");
}
if (c.onBlurCodeview != null) {
widget.controller.editorController!.evaluateJavascript(source: """
\$('#summernote-2').on('summernote.blur.codeview', function() {
window.flutter_inappwebview.callHandler('onBlurCodeview', 'fired');
});
""");
}
if (c.onKeyDown != null) {
widget.controller.editorController!.evaluateJavascript(source: """
\$('#summernote-2').on('summernote.keydown', function(_, e) {
window.flutter_inappwebview.callHandler('onKeyDown', e.keyCode);
});
""");
}
if (c.onKeyUp != null) {
widget.controller.editorController!.evaluateJavascript(source: """
\$('#summernote-2').on('summernote.keyup', function(_, e) {
window.flutter_inappwebview.callHandler('onKeyUp', e.keyCode);
});
""");
}
if (c.onMouseDown != null) {
widget.controller.editorController!.evaluateJavascript(source: """
\$('#summernote-2').on('summernote.mousedown', function(_) {
window.flutter_inappwebview.callHandler('onMouseDown', 'fired');
});
""");
}
if (c.onMouseUp != null) {
widget.controller.editorController!.evaluateJavascript(source: """
\$('#summernote-2').on('summernote.mouseup', function(_) {
window.flutter_inappwebview.callHandler('onMouseUp', 'fired');
});
""");
}
if (c.onPaste != null) {
widget.controller.editorController!.evaluateJavascript(source: """
\$('#summernote-2').on('summernote.paste', function(_) {
window.flutter_inappwebview.callHandler('onPaste', 'fired');
});
""");
}
if (c.onScroll != null) {
widget.controller.editorController!.evaluateJavascript(source: """
\$('#summernote-2').on('summernote.scroll', function(_) {
window.flutter_inappwebview.callHandler('onScroll', 'fired');
});
""");
}
}
/// creates flutter_inappwebview JavaScript Handlers to handle any callbacks the
/// user has defined
void addJSHandlers(Callbacks c) {
if (c.onBeforeCommand != null) {
widget.controller.editorController!.addJavaScriptHandler(
handlerName: 'onBeforeCommand',
callback: (contents) {
c.onBeforeCommand!.call(contents.first.toString());
});
}
if (c.onChangeCodeview != null) {
widget.controller.editorController!.addJavaScriptHandler(
handlerName: 'onChangeCodeview',
callback: (contents) {
c.onChangeCodeview!.call(contents.first.toString());
});
}
if (c.onDialogShown != null) {
widget.controller.editorController!.addJavaScriptHandler(
handlerName: 'onDialogShown',
callback: (_) {
c.onDialogShown!.call();
});
}
if (c.onEnter != null) {
widget.controller.editorController!.addJavaScriptHandler(
handlerName: 'onEnter',
callback: (_) {
c.onEnter!.call();
});
}
if (c.onFocus != null) {
widget.controller.editorController!.addJavaScriptHandler(
handlerName: 'onFocus',
callback: (_) {
c.onFocus!.call();
});
}
if (c.onBlur != null) {
widget.controller.editorController!.addJavaScriptHandler(
handlerName: 'onBlur',
callback: (_) {
c.onBlur!.call();
});
}
if (c.onBlurCodeview != null) {
widget.controller.editorController!.addJavaScriptHandler(
handlerName: 'onBlurCodeview',
callback: (_) {
c.onBlurCodeview!.call();
});
}
if (c.onImageLinkInsert != null) {
widget.controller.editorController!.addJavaScriptHandler(
handlerName: 'onImageLinkInsert',
callback: (url) {
c.onImageLinkInsert!.call(url.first.toString());
});
}
if (c.onImageUpload != null) {
widget.controller.editorController!.addJavaScriptHandler(
handlerName: 'onImageUpload',
callback: (files) {
var file = fileUploadFromJson(files.first);
c.onImageUpload!.call(file);
});
}
if (c.onImageUploadError != null) {
widget.controller.editorController!.addJavaScriptHandler(
handlerName: 'onImageUploadError',
callback: (args) {
if (!args.first.toString().startsWith('{')) {
c.onImageUploadError!.call(
null,
args.first,
args.last.contains('base64')
? UploadError.jsException
: args.last.contains('unsupported')
? UploadError.unsupportedFile
: UploadError.exceededMaxSize);
} else {
var file = fileUploadFromJson(args.first.toString());
c.onImageUploadError!.call(
file,
null,
args.last.contains('base64')
? UploadError.jsException
: args.last.contains('unsupported')
? UploadError.unsupportedFile
: UploadError.exceededMaxSize);
}
});
}
if (c.onKeyDown != null) {
widget.controller.editorController!.addJavaScriptHandler(
handlerName: 'onKeyDown',
callback: (keyCode) {
c.onKeyDown!.call(keyCode.first);
});
}
if (c.onKeyUp != null) {
widget.controller.editorController!.addJavaScriptHandler(
handlerName: 'onKeyUp',
callback: (keyCode) {
c.onKeyUp!.call(keyCode.first);
});
}
if (c.onMouseDown != null) {
widget.controller.editorController!.addJavaScriptHandler(
handlerName: 'onMouseDown',
callback: (_) {
c.onMouseDown!.call();
});
}
if (c.onMouseUp != null) {
widget.controller.editorController!.addJavaScriptHandler(
handlerName: 'onMouseUp',
callback: (_) {
c.onMouseUp!.call();
});
}
if (c.onPaste != null) {
widget.controller.editorController!.addJavaScriptHandler(
handlerName: 'onPaste',
callback: (_) {
c.onPaste!.call();
});
}
if (c.onScroll != null) {
widget.controller.editorController!.addJavaScriptHandler(
handlerName: 'onScroll',
callback: (_) {
c.onScroll!.call();
});
}
}
}

View File

@@ -0,0 +1,799 @@
export 'dart:html';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:html_editor_enhanced/html_editor.dart';
import 'package:html_editor_enhanced/utils/utils.dart';
// ignore: avoid_web_libraries_in_flutter
import 'dart:html' as html;
import 'package:html_editor_enhanced/utils/shims/dart_ui.dart' as ui;
/// The HTML Editor widget itself, for web (uses IFrameElement)
class HtmlEditorWidget extends StatefulWidget {
HtmlEditorWidget({
Key? key,
required this.controller,
this.callbacks,
required this.plugins,
required this.htmlEditorOptions,
required this.htmlToolbarOptions,
required this.otherOptions,
required this.initBC,
}) : super(key: key);
final HtmlEditorController controller;
final Callbacks? callbacks;
final List<Plugins> plugins;
final HtmlEditorOptions htmlEditorOptions;
final HtmlToolbarOptions htmlToolbarOptions;
final OtherOptions otherOptions;
final BuildContext initBC;
@override
_HtmlEditorWidgetWebState createState() => _HtmlEditorWidgetWebState();
}
/// State for the web Html editor widget
///
/// A stateful widget is necessary here, otherwise the IFrameElement will be
/// rebuilt excessively, hurting performance
class _HtmlEditorWidgetWebState extends State<HtmlEditorWidget> {
/// The view ID for the IFrameElement. Must be unique.
late String createdViewId;
/// The actual height of the editor, used to automatically set the height
late double actualHeight;
/// A Future that is observed by the [FutureBuilder]. We don't use a function
/// as the Future on the [FutureBuilder] because when the widget is rebuilt,
/// the function may be excessively called, hurting performance.
Future<bool>? summernoteInit;
/// Helps get the height of the toolbar to accurately adjust the height of
/// the editor when the keyboard is visible.
GlobalKey toolbarKey = GlobalKey();
/// Tracks whether the editor was disabled onInit (to avoid re-disabling on reload)
bool alreadyDisabled = false;
@override
void initState() {
actualHeight = widget.otherOptions.height;
createdViewId = getRandString(10);
widget.controller.viewId = createdViewId;
initSummernote();
super.initState();
}
void initSummernote() async {
var headString = '';
var summernoteCallbacks = '''callbacks: {
onKeydown: function(e) {
var chars = \$(".note-editable").text();
var totalChars = chars.length;
${widget.htmlEditorOptions.characterLimit != null ? '''allowedKeys = (
e.which === 8 || /* BACKSPACE */
e.which === 35 || /* END */
e.which === 36 || /* HOME */
e.which === 37 || /* LEFT */
e.which === 38 || /* UP */
e.which === 39 || /* RIGHT*/
e.which === 40 || /* DOWN */
e.which === 46 || /* DEL*/
e.ctrlKey === true && e.which === 65 || /* CTRL + A */
e.ctrlKey === true && e.which === 88 || /* CTRL + X */
e.ctrlKey === true && e.which === 67 || /* CTRL + C */
e.ctrlKey === true && e.which === 86 || /* CTRL + V */
e.ctrlKey === true && e.which === 90 /* CTRL + Z */
);
if (!allowedKeys && \$(e.target).text().length >= ${widget.htmlEditorOptions.characterLimit}) {
e.preventDefault();
}''' : ''}
window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: characterCount", "totalChars": totalChars}), "*");
},
''';
var maximumFileSize = 10485760;
for (var p in widget.plugins) {
headString = headString + p.getHeadString() + '\n';
if (p is SummernoteAtMention) {
summernoteCallbacks = summernoteCallbacks +
'''
\nsummernoteAtMention: {
getSuggestions: (value) => {
const mentions = ${p.getMentionsWeb()};
return mentions.filter((mention) => {
return mention.includes(value);
});
},
onSelect: (value) => {
window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: onSelectMention", "value": value}), "*");
},
},
''';
if (p.onSelect != null) {
html.window.onMessage.listen((event) {
var data = json.decode(event.data);
if (data['type'] != null &&
data['type'].contains('toDart:') &&
data['view'] == createdViewId &&
data['type'].contains('onSelectMention')) {
p.onSelect!.call(data['value']);
}
});
}
}
}
if (widget.callbacks != null) {
if (widget.callbacks!.onImageLinkInsert != null) {
summernoteCallbacks = summernoteCallbacks +
'''
onImageLinkInsert: function(url) {
window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: onImageLinkInsert", "url": url}), "*");
},
''';
}
if (widget.callbacks!.onImageUpload != null) {
summernoteCallbacks = summernoteCallbacks +
"""
onImageUpload: function(files) {
var reader = new FileReader();
var base64 = "<an error occurred>";
reader.onload = function (_) {
base64 = reader.result;
var newObject = {
'lastModified': files[0].lastModified,
'lastModifiedDate': files[0].lastModifiedDate,
'name': files[0].name,
'size': files[0].size,
'type': files[0].type,
'base64': base64
};
window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: onImageUpload", "lastModified": files[0].lastModified, "lastModifiedDate": files[0].lastModifiedDate, "name": files[0].name, "size": files[0].size, "mimeType": files[0].type, "base64": base64}), "*");
};
reader.onerror = function (_) {
var newObject = {
'lastModified': files[0].lastModified,
'lastModifiedDate': files[0].lastModifiedDate,
'name': files[0].name,
'size': files[0].size,
'type': files[0].type,
'base64': base64
};
window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: onImageUpload", "lastModified": files[0].lastModified, "lastModifiedDate": files[0].lastModifiedDate, "name": files[0].name, "size": files[0].size, "mimeType": files[0].type, "base64": base64}), "*");
};
reader.readAsDataURL(files[0]);
},
""";
}
if (widget.callbacks!.onImageUploadError != null) {
summernoteCallbacks = summernoteCallbacks +
"""
onImageUploadError: function(file, error) {
if (typeof file === 'string') {
window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: onImageUploadError", "base64": file, "error": error}), "*");
} else {
window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: onImageUploadError", "lastModified": file.lastModified, "lastModifiedDate": file.lastModifiedDate, "name": file.name, "size": file.size, "mimeType": file.type, "error": error}), "*");
}
},
""";
}
}
summernoteCallbacks = summernoteCallbacks + '}';
var darkCSS = '';
if ((Theme.of(widget.initBC).brightness == Brightness.dark ||
widget.htmlEditorOptions.darkMode == true) &&
widget.htmlEditorOptions.darkMode != false) {
darkCSS =
'<link href=\"assets/packages/html_editor_enhanced/assets/summernote-lite-dark.css\" rel=\"stylesheet\">';
}
var jsCallbacks = '';
if (widget.callbacks != null) {
jsCallbacks = getJsCallbacks(widget.callbacks!);
}
var userScripts = '';
if (widget.htmlEditorOptions.webInitialScripts != null) {
widget.htmlEditorOptions.webInitialScripts!.forEach((element) {
userScripts = userScripts +
'''
if (data["type"].includes("${element.name}")) {
${element.script}
}
''' +
'\n';
});
}
var summernoteScripts = """
<script type="text/javascript">
\$(document).ready(function () {
\$('#summernote-2').summernote({
placeholder: "${widget.htmlEditorOptions.hint}",
tabsize: 2,
height: ${widget.otherOptions.height},
disableGrammar: false,
spellCheck: ${widget.htmlEditorOptions.spellCheck},
maximumFileSize: $maximumFileSize,
${widget.htmlEditorOptions.customOptions}
$summernoteCallbacks
});
\$('#summernote-2').on('summernote.change', function(_, contents, \$editable) {
window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: onChangeContent", "contents": contents}), "*");
});
});
window.parent.addEventListener('message', handleMessage, false);
document.onselectionchange = onSelectionChange;
console.log('done');
function handleMessage(e) {
if (e && e.data && e.data.includes("toIframe:")) {
var data = JSON.parse(e.data);
if (data["view"].includes("$createdViewId")) {
if (data["type"].includes("getText")) {
var str = \$('#summernote-2').summernote('code');
window.parent.postMessage(JSON.stringify({"type": "toDart: getText", "text": str}), "*");
}
if (data["type"].includes("getHeight")) {
var height = document.body.scrollHeight;
window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: htmlHeight", "height": height}), "*");
}
if (data["type"].includes("setInputType")) {
document.getElementsByClassName('note-editable')[0].setAttribute('inputmode', '${describeEnum(widget.htmlEditorOptions.inputType)}');
}
if (data["type"].includes("setText")) {
\$('#summernote-2').summernote('code', data["text"]);
}
if (data["type"].includes("setFullScreen")) {
\$("#summernote-2").summernote("fullscreen.toggle");
}
if (data["type"].includes("setFocus")) {
\$('#summernote-2').summernote('focus');
}
if (data["type"].includes("clear")) {
\$('#summernote-2').summernote('reset');
}
if (data["type"].includes("setHint")) {
\$(".note-placeholder").html(data["text"]);
}
if (data["type"].includes("toggleCodeview")) {
\$('#summernote-2').summernote('codeview.toggle');
}
if (data["type"].includes("disable")) {
\$('#summernote-2').summernote('disable');
}
if (data["type"].includes("enable")) {
\$('#summernote-2').summernote('enable');
}
if (data["type"].includes("undo")) {
\$('#summernote-2').summernote('undo');
}
if (data["type"].includes("redo")) {
\$('#summernote-2').summernote('redo');
}
if (data["type"].includes("insertText")) {
\$('#summernote-2').summernote('insertText', data["text"]);
}
if (data["type"].includes("insertHtml")) {
\$('#summernote-2').summernote('pasteHTML', data["html"]);
}
if (data["type"].includes("insertNetworkImage")) {
\$('#summernote-2').summernote('insertImage', data["url"], data["filename"]);
}
if (data["type"].includes("insertLink")) {
\$('#summernote-2').summernote('createLink', {
text: data["text"],
url: data["url"],
isNewWindow: data["isNewWindow"]
});
}
if (data["type"].includes("reload")) {
window.location.reload();
}
if (data["type"].includes("addNotification")) {
if (data["alertType"] === null) {
\$('.note-status-output').html(
data["html"]
);
} else {
\$('.note-status-output').html(
'<div class="' + data["alertType"] + '">' +
data["html"] +
'</div>'
);
}
}
if (data["type"].includes("removeNotification")) {
\$('.note-status-output').empty();
}
if (data["type"].includes("execCommand")) {
if (data["argument"] === null) {
document.execCommand(data["command"], false);
} else {
document.execCommand(data["command"], false, data["argument"]);
}
}
if (data["type"].includes("changeListStyle")) {
var \$focusNode = \$(window.getSelection().focusNode);
var \$parentList = \$focusNode.closest("div.note-editable ol, div.note-editable ul");
\$parentList.css("list-style-type", data["changed"]);
}
if (data["type"].includes("changeLineHeight")) {
\$('#summernote-2').summernote('lineHeight', data["changed"]);
}
if (data["type"].includes("changeTextDirection")) {
var s=document.getSelection();
if(s==''){
document.execCommand("insertHTML", false, "<p dir='"+data['direction']+"'></p>");
}else{
document.execCommand("insertHTML", false, "<div dir='"+data['direction']+"'>"+ document.getSelection()+"</div>");
}
}
if (data["type"].includes("changeCase")) {
var selected = \$('#summernote-2').summernote('createRange');
if(selected.toString()){
var texto;
var count = 0;
var value = data["case"];
console.log(value);
var nodes = selected.nodes();
for (var i=0; i< nodes.length; ++i) {
if (nodes[i].nodeName == "#text") {
count++;
texto = nodes[i].nodeValue.toLowerCase();
nodes[i].nodeValue = texto;
if (value == 'upper') {
nodes[i].nodeValue = texto.toUpperCase();
}
else if (value == 'sentence' && count==1) {
nodes[i].nodeValue = texto.charAt(0).toUpperCase() + texto.slice(1).toLowerCase();
} else if (value == 'title') {
var sentence = texto.split(" ");
for(var j = 0; j< sentence.length; j++){
sentence[j] = sentence[j][0].toUpperCase() + sentence[j].slice(1);
}
nodes[i].nodeValue = sentence.join(" ");
}
}
}
}
}
if (data["type"].includes("insertTable")) {
\$('#summernote-2').summernote('insertTable', data["dimensions"]);
}
if (data["type"].includes("getSelectedTextHtml")) {
var range = window.getSelection().getRangeAt(0);
var content = range.cloneContents();
var span = document.createElement('span');
span.appendChild(content);
var htmlContent = span.innerHTML;
window.parent.postMessage(JSON.stringify({"type": "toDart: getSelectedText", "text": htmlContent}), "*");
} else if (data["type"].includes("getSelectedText")) {
window.parent.postMessage(JSON.stringify({"type": "toDart: getSelectedText", "text": window.getSelection().toString()}), "*");
}
$userScripts
}
}
}
function onSelectionChange() {
let {anchorNode, anchorOffset, focusNode, focusOffset} = document.getSelection();
var isBold = false;
var isItalic = false;
var isUnderline = false;
var isStrikethrough = false;
var isSuperscript = false;
var isSubscript = false;
var isUL = false;
var isOL = false;
var isLeft = false;
var isRight = false;
var isCenter = false;
var isFull = false;
var parent;
var fontName;
var fontSize = 16;
var foreColor = "000000";
var backColor = "FFFF00";
var focusNode2 = \$(window.getSelection().focusNode);
var parentList = focusNode2.closest("div.note-editable ol, div.note-editable ul");
var parentListType = parentList.css('list-style-type');
var lineHeight = \$(focusNode.parentNode).css('line-height');
var direction = \$(focusNode.parentNode).css('direction');
if (document.queryCommandState) {
isBold = document.queryCommandState('bold');
isItalic = document.queryCommandState('italic');
isUnderline = document.queryCommandState('underline');
isStrikethrough = document.queryCommandState('strikeThrough');
isSuperscript = document.queryCommandState('superscript');
isSubscript = document.queryCommandState('subscript');
isUL = document.queryCommandState('insertUnorderedList');
isOL = document.queryCommandState('insertOrderedList');
isLeft = document.queryCommandState('justifyLeft');
isRight = document.queryCommandState('justifyRight');
isCenter = document.queryCommandState('justifyCenter');
isFull = document.queryCommandState('justifyFull');
}
if (document.queryCommandValue) {
parent = document.queryCommandValue('formatBlock');
fontSize = document.queryCommandValue('fontSize');
foreColor = document.queryCommandValue('foreColor');
backColor = document.queryCommandValue('hiliteColor');
fontName = document.queryCommandValue('fontName');
}
var message = {
'view': "$createdViewId",
'type': "toDart: updateToolbar",
'style': parent,
'fontName': fontName,
'fontSize': fontSize,
'font': [isBold, isItalic, isUnderline],
'miscFont': [isStrikethrough, isSuperscript, isSubscript],
'color': [foreColor, backColor],
'paragraph': [isUL, isOL],
'listStyle': parentListType,
'align': [isLeft, isCenter, isRight, isFull],
'lineHeight': lineHeight,
'direction': direction,
};
window.parent.postMessage(JSON.stringify(message), "*");
}
$jsCallbacks
</script>
""";
var filePath =
'packages/html_editor_enhanced/assets/summernote-no-plugins.html';
if (widget.htmlEditorOptions.filePath != null) {
filePath = widget.htmlEditorOptions.filePath!;
}
var htmlString = await rootBundle.loadString(filePath);
htmlString = htmlString
.replaceFirst('<!--darkCSS-->', darkCSS)
.replaceFirst('<!--headString-->', headString)
.replaceFirst('<!--summernoteScripts-->', summernoteScripts)
.replaceFirst('"jquery.min.js"',
'"assets/packages/html_editor_enhanced/assets/jquery.min.js"')
.replaceFirst('"summernote-lite.min.css"',
'"assets/packages/html_editor_enhanced/assets/summernote-lite.min.css"')
.replaceFirst('"summernote-lite.min.js"',
'"assets/packages/html_editor_enhanced/assets/summernote-lite.min.js"');
if (widget.callbacks != null) addJSListener(widget.callbacks!);
final iframe = html.IFrameElement()
..width = MediaQuery.of(widget.initBC).size.width.toString() //'800'
..height = widget.htmlEditorOptions.autoAdjustHeight
? actualHeight.toString()
: widget.otherOptions.height.toString()
// ignore: unsafe_html, necessary to load HTML string
..srcdoc = htmlString
..style.border = 'none'
..style.overflow = 'hidden'
..onLoad.listen((event) async {
if (widget.htmlEditorOptions.disabled && !alreadyDisabled) {
widget.controller.disable();
alreadyDisabled = true;
}
if (widget.callbacks != null && widget.callbacks!.onInit != null) {
widget.callbacks!.onInit!.call();
}
if (widget.htmlEditorOptions.initialText != null) {
widget.controller.setText(widget.htmlEditorOptions.initialText!);
}
var data = <String, Object>{'type': 'toIframe: getHeight'};
data['view'] = createdViewId;
var data2 = <String, Object>{'type': 'toIframe: setInputType'};
data2['view'] = createdViewId;
final jsonEncoder = JsonEncoder();
var jsonStr = jsonEncoder.convert(data);
var jsonStr2 = jsonEncoder.convert(data2);
html.window.onMessage.listen((event) {
var data = json.decode(event.data);
if (data['type'] != null &&
data['type'].contains('toDart: htmlHeight') &&
data['view'] == createdViewId &&
widget.htmlEditorOptions.autoAdjustHeight) {
final docHeight = data['height'] ?? actualHeight;
if ((docHeight != null && docHeight != actualHeight) &&
mounted &&
docHeight > 0) {
setState(mounted, this.setState, () {
actualHeight =
docHeight + (toolbarKey.currentContext?.size?.height ?? 0);
});
}
}
if (data['type'] != null &&
data['type'].contains('toDart: onChangeContent') &&
data['view'] == createdViewId) {
if (widget.callbacks != null &&
widget.callbacks!.onChangeContent != null) {
widget.callbacks!.onChangeContent!.call(data['contents']);
}
if (widget.htmlEditorOptions.shouldEnsureVisible &&
Scrollable.of(context) != null) {
Scrollable.of(context)!.position.ensureVisible(
context.findRenderObject()!,
duration: const Duration(milliseconds: 100),
curve: Curves.easeIn);
}
}
if (data['type'] != null &&
data['type'].contains('toDart: updateToolbar') &&
data['view'] == createdViewId) {
if (widget.controller.toolbar != null) {
widget.controller.toolbar!.updateToolbar(data);
}
}
});
html.window.postMessage(jsonStr, '*');
html.window.postMessage(jsonStr2, '*');
});
ui.platformViewRegistry
.registerViewFactory(createdViewId, (int viewId) => iframe);
setState(mounted, this.setState, () {
summernoteInit = Future.value(true);
});
}
@override
Widget build(BuildContext context) {
return Container(
height: widget.htmlEditorOptions.autoAdjustHeight
? actualHeight
: widget.otherOptions.height,
child: Column(
children: <Widget>[
widget.htmlToolbarOptions.toolbarPosition ==
ToolbarPosition.aboveEditor
? ToolbarWidget(
key: toolbarKey,
controller: widget.controller,
htmlToolbarOptions: widget.htmlToolbarOptions,
callbacks: widget.callbacks)
: Container(height: 0, width: 0),
Expanded(
child: Directionality(
textDirection: TextDirection.ltr,
child: FutureBuilder<bool>(
future: summernoteInit,
builder: (context, snapshot) {
if (snapshot.hasData) {
return HtmlElementView(
viewType: createdViewId,
);
} else {
return Container(
height: widget.htmlEditorOptions.autoAdjustHeight
? actualHeight
: widget.otherOptions.height);
}
}))),
widget.htmlToolbarOptions.toolbarPosition ==
ToolbarPosition.belowEditor
? ToolbarWidget(
key: toolbarKey,
controller: widget.controller,
htmlToolbarOptions: widget.htmlToolbarOptions,
callbacks: widget.callbacks)
: Container(height: 0, width: 0),
],
),
);
}
/// Adds the callbacks the user set into JavaScript
String getJsCallbacks(Callbacks c) {
var callbacks = '';
if (c.onBeforeCommand != null) {
callbacks = callbacks +
"""
\$('#summernote-2').on('summernote.before.command', function(_, contents, \$editable) {
window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: onBeforeCommand", "contents": contents}), "*");
});\n
""";
}
if (c.onChangeCodeview != null) {
callbacks = callbacks +
"""
\$('#summernote-2').on('summernote.change.codeview', function(_, contents, \$editable) {
window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: onChangeCodeview", "contents": contents}), "*");
});\n
""";
}
if (c.onDialogShown != null) {
callbacks = callbacks +
"""
\$('#summernote-2').on('summernote.dialog.shown', function() {
window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: onDialogShown"}), "*");
});\n
""";
}
if (c.onEnter != null) {
callbacks = callbacks +
"""
\$('#summernote-2').on('summernote.enter', function() {
window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: onEnter"}), "*");
});\n
""";
}
if (c.onFocus != null) {
callbacks = callbacks +
"""
\$('#summernote-2').on('summernote.focus', function() {
window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: onFocus"}), "*");
});\n
""";
}
if (c.onBlur != null) {
callbacks = callbacks +
"""
\$('#summernote-2').on('summernote.blur', function() {
window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: onBlur"}), "*");
});\n
""";
}
if (c.onBlurCodeview != null) {
callbacks = callbacks +
"""
\$('#summernote-2').on('summernote.blur.codeview', function() {
window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: onBlurCodeview"}), "*");
});\n
""";
}
if (c.onKeyDown != null) {
callbacks = callbacks +
"""
\$('#summernote-2').on('summernote.keydown', function(_, e) {
window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: onKeyDown", "keyCode": e.keyCode}), "*");
});\n
""";
}
if (c.onKeyUp != null) {
callbacks = callbacks +
"""
\$('#summernote-2').on('summernote.keyup', function(_, e) {
window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: onKeyUp", "keyCode": e.keyCode}), "*");
});\n
""";
}
if (c.onMouseDown != null) {
callbacks = callbacks +
"""
\$('#summernote-2').on('summernote.mousedown', function(_) {
window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: onMouseDown"}), "*");
});\n
""";
}
if (c.onMouseUp != null) {
callbacks = callbacks +
"""
\$('#summernote-2').on('summernote.mouseup', function(_) {
window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: onMouseUp"}), "*");
});\n
""";
}
if (c.onPaste != null) {
callbacks = callbacks +
"""
\$('#summernote-2').on('summernote.paste', function(_) {
window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: onPaste"}), "*");
});\n
""";
}
if (c.onScroll != null) {
callbacks = callbacks +
"""
\$('#summernote-2').on('summernote.scroll', function(_) {
window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: onScroll"}), "*");
});\n
""";
}
return callbacks;
}
/// Adds an event listener to check when a callback is fired
void addJSListener(Callbacks c) {
html.window.onMessage.listen((event) {
var data = json.decode(event.data);
if (data['type'] != null &&
data['type'].contains('toDart:') &&
data['view'] == createdViewId) {
if (data['type'].contains('onBeforeCommand')) {
c.onBeforeCommand!.call(data['contents']);
}
if (data['type'].contains('onChangeContent')) {
c.onChangeContent!.call(data['contents']);
}
if (data['type'].contains('onChangeCodeview')) {
c.onChangeCodeview!.call(data['contents']);
}
if (data['type'].contains('onDialogShown')) {
c.onDialogShown!.call();
}
if (data['type'].contains('onEnter')) {
c.onEnter!.call();
}
if (data['type'].contains('onFocus')) {
c.onFocus!.call();
}
if (data['type'].contains('onBlur')) {
c.onBlur!.call();
}
if (data['type'].contains('onBlurCodeview')) {
c.onBlurCodeview!.call();
}
if (data['type'].contains('onImageLinkInsert')) {
c.onImageLinkInsert!.call(data['url']);
}
if (data['type'].contains('onImageUpload')) {
var map = <String, dynamic>{
'lastModified': data['lastModified'],
'lastModifiedDate': data['lastModifiedDate'],
'name': data['name'],
'size': data['size'],
'type': data['mimeType'],
'base64': data['base64']
};
var jsonStr = json.encode(map);
var file = fileUploadFromJson(jsonStr);
c.onImageUpload!.call(file);
}
if (data['type'].contains('onImageUploadError')) {
if (data['base64'] != null) {
c.onImageUploadError!.call(
null,
data['base64'],
data['error'].contains('base64')
? UploadError.jsException
: data['error'].contains('unsupported')
? UploadError.unsupportedFile
: UploadError.exceededMaxSize);
} else {
var map = <String, dynamic>{
'lastModified': data['lastModified'],
'lastModifiedDate': data['lastModifiedDate'],
'name': data['name'],
'size': data['size'],
'type': data['mimeType']
};
var jsonStr = json.encode(map);
var file = fileUploadFromJson(jsonStr);
c.onImageUploadError!.call(
file,
null,
data['error'].contains('base64')
? UploadError.jsException
: data['error'].contains('unsupported')
? UploadError.unsupportedFile
: UploadError.exceededMaxSize);
}
}
if (data['type'].contains('onKeyDown')) {
c.onKeyDown!.call(data['keyCode']);
}
if (data['type'].contains('onKeyUp')) {
c.onKeyUp!.call(data['keyCode']);
}
if (data['type'].contains('onMouseDown')) {
c.onMouseDown!.call();
}
if (data['type'].contains('onMouseUp')) {
c.onMouseUp!.call();
}
if (data['type'].contains('onPaste')) {
c.onPaste!.call();
}
if (data['type'].contains('onScroll')) {
c.onScroll!.call();
}
if (data['type'].contains('characterCount')) {
widget.controller.characterCount = data['totalChars'];
}
}
});
}
}

File diff suppressed because it is too large Load Diff

187
lib/utils/callbacks.dart Normal file
View File

@@ -0,0 +1,187 @@
import 'dart:async';
import 'package:html_editor_enhanced/html_editor.dart';
/// Manages all the callback functions the library provides
class Callbacks {
Callbacks({
this.onBeforeCommand,
this.onChangeContent,
this.onChangeCodeview,
this.onChangeSelection,
this.onDialogShown,
this.onEnter,
this.onFocus,
this.onBlur,
this.onBlurCodeview,
this.onImageLinkInsert,
this.onImageUpload,
this.onImageUploadError,
this.onInit,
this.onKeyUp,
this.onKeyDown,
this.onMouseUp,
this.onMouseDown,
this.onNavigationRequestMobile,
this.onPaste,
this.onScroll,
});
/// Called before certain commands are fired and the editor is in rich text view.
/// There is currently no documentation on this parameter, thus it is
/// unclear which commands this will fire before.
///
/// This function will return the current HTML in the editor as an argument.
void Function(String?)? onBeforeCommand;
/// Called whenever the HTML content of the editor is changed and the editor
/// is in rich text view.
///
/// Note: This function also seems to be called if input is detected in the
/// editor field but the content does not change.
/// E.g. repeatedly pressing backspace when the field is empty
/// will also trigger this callback.
///
/// This function will return the current HTML in the editor as an argument.
void Function(String?)? onChangeContent;
/// Called whenever the code of the editor is changed and the editor
/// is in code view.
///
/// Note: This function also seems to be called if input is detected in the
/// editor field but the content does not change.
/// E.g. repeatedly pressing backspace when the field is empty
/// will also trigger this callback.
///
/// This function will return the current code in the codeview as an argument.
void Function(String?)? onChangeCodeview;
/// Called whenever the selection area of the editor is changed.
///
/// It passes all the editor settings at the current selection as an argument.
/// This can be used in custom toolbar item implementations, to update your
/// toolbar item UI when the editor formatting changes.
void Function(EditorSettings)? onChangeSelection;
/// Called whenever a dialog is shown in the editor. The dialogs will be either
/// the link, image, video, or help dialogs.
void Function()? onDialogShown;
/// Called whenever the enter/return key is pressed and the editor
/// is in rich text view. There is currently no way to detect enter/return
/// when the editor is in code view.
///
/// Note: The onChange callback will also be triggered at the same time as
/// this callback, please design your implementation accordingly.
void Function()? onEnter;
/// Called whenever the rich text field gains focus. This will not be called
/// when the code view editor gains focus, instead use [onBlurCodeview] for
/// that.
void Function()? onFocus;
/// Called whenever either the rich text field or the codeview field loses
/// focus. This will also be triggered when switching from the rich text editor
/// to the code view editor.
///
/// Note: Due to the current state of webviews in Flutter, tapping outside
/// the webview or dismissing the keyboard does not trigger this callback.
/// This callback will only be triggered if the user taps on an empty space
/// in the toolbar or switches the view mode of the editor.
void Function()? onBlur;
/// Called whenever the code view either gains or loses focus (the Summernote
/// docs say this will only be called when the code view loses focus but
/// in my testing this is not the case). This will also be triggered when
/// switching between the rich text editor and the code view editor.
///
/// Note: Due to the current state of webviews in Flutter, tapping outside
/// the webview or dismissing the keyboard does not trigger this callback.
/// This callback will only be triggered if the user taps on an empty space
/// in the toolbar or switches the view mode of the editor.
void Function()? onBlurCodeview;
/// Called whenever an image is inserted via a link. The function passes the
/// URL of the image inserted into the editor.
///
/// Note: Setting this function overrides the default summernote image via URL
/// insertion handler! This means you must manually insert the image using
/// [controller.insertNetworkImage] in your callback function, otherwise
/// nothing will be inserted into the editor!
void Function(String?)? onImageLinkInsert;
/// Called whenever an image is inserted via upload. The function passes the
/// [FileUpload] class, containing the filename, size, MIME type, base64 data,
/// and last modified information so you can upload it into your server.
///
/// Note: Setting this function overrides the default summernote upload image
/// insertion handler (base64 handler)! This means you must manually insert
/// the image using [controller.insertNetworkImage] (for uploaded images) or
/// [controller.insertHtml] (for base64 data) in your callback function,
/// otherwise nothing will be inserted into the editor!
void Function(FileUpload)? onImageUpload;
/// Called whenever an image is failed to be inserted via upload. The function
/// passes the [FileUpload] class, containing the filename, size, MIME type,
/// base64 data, and last modified information so you can do error handling.
void Function(FileUpload?, String?, UploadError)? onImageUploadError;
/// Called whenever [InAppWebViewController.onLoadStop] is fired on mobile
/// or when the [IFrameElement.onLoad] stream is fired on web. Note that this
/// method will also be called on refresh on both platforms.
///
/// You can use this method to set certain properties - e.g. set the editor
/// to fullscreen mode - as soon as the editor is ready to accept these commands.
void Function()? onInit;
/// Called whenever a key is released and the editor is in rich text view.
///
/// This function will return the keycode for the released key as an argument.
///
/// Note: The keycode [is broken](https://stackoverflow.com/questions/36753548/keycode-on-android-is-always-229)
/// on Android, you will only ever receive 229, 8 (backspace), or 13 (enter)
/// as a keycode. 8 and 13 only seem to be returned when the editor is empty
/// and those keys are released.
void Function(int?)? onKeyUp;
/// Called whenever a key is downed and the editor is in rich text view.
///
/// This function will return the keycode for the downed key as an argument.
///
/// Note: The keycode [is broken](https://stackoverflow.com/questions/36753548/keycode-on-android-is-always-229)
/// on Android, you will only ever receive 229, 8 (backspace), or 13 (enter)
/// as a keycode. 8 and 13 only seem to be returned when the editor is empty
/// and those keys are downed.
void Function(int?)? onKeyDown;
/// Called whenever the mouse/finger is released and the editor is in rich text view.
void Function()? onMouseUp;
/// Called whenever the mouse/finger is downed and the editor is in rich text view.
void Function()? onMouseDown;
/// Called right before the URL of the webview is changed on mobile. This allows
/// you to prevent URLs from loading, or launch them externally, for example.
///
/// This function passes the URL to be loaded, and you must return a
/// `NavigationActionPolicy` to tell the webview what to do.
FutureOr<NavigationActionPolicy> Function(String)? onNavigationRequestMobile;
/// Called whenever text is pasted into the rich text field. This will not be
/// called when text is pasted into the code view editor.
///
/// Note: This will not be called when programmatically inserting HTML into
/// the editor with [HtmlEditor.insertHtml].
void Function()? onPaste;
/// Called whenever the editor is scrolled and it is in rich text view.
/// Editor scrolled is considered to be the editor box only, not the webview
/// container itself. Thus, this callback will only fire when the content in
/// the editor is longer than the editor height. This function can be called
/// with an explicit scrolling action via the mouse, or also via implied
/// scrolling, e.g. the enter key scrolling the editor to make new text visible.
///
/// Note: This function will be repeatedly called while the editor is scrolled.
/// Make sure to factor that into your implementation.
void Function()? onScroll;
}

View File

@@ -0,0 +1,59 @@
import 'dart:convert';
/// Function that creates an instance of [FileUpload] from a JSON string
FileUpload fileUploadFromJson(String str) =>
FileUpload.fromJson(json.decode(str));
/// The [FileUpload] class stores any known data about a file. This class is used
/// as an argument in some callbacks relating to image and file insertion.
///
/// The class holds last modified information, name, size, type, and the base64
/// of the file.
///
/// Note that all parameters are nullable to prevent any null-exception when
/// getting file data from JavaScript.
class FileUpload {
FileUpload({
this.base64,
this.lastModified,
this.lastModifiedDate,
this.name,
this.size,
this.type,
});
/// The base64 string of the file.
///
/// Note: This includes identifying data (e.g. data:image/jpeg;base64,) at the
/// beginning. To strip this out, use FileUpload().base64.split(",")[1].
String? base64;
/// Last modified information in *milliseconds since epoch* format
DateTime? lastModified;
/// Last modified information in *regular date* format
DateTime? lastModifiedDate;
/// The filename
String? name;
/// The file size in bytes
int? size;
/// The content-type (eg. image/jpeg) of the file
String? type;
/// Creates an instance of [FileUpload] from a JSON string
factory FileUpload.fromJson(Map<String, dynamic> json) => FileUpload(
base64: json['base64'],
lastModified: json['lastModified'] == null
? null
: DateTime.fromMillisecondsSinceEpoch(json['lastModified']),
lastModifiedDate: json['lastModifiedDate'] == null
? null
: DateTime.tryParse(json['lastModifiedDate']),
name: json['name'],
size: json['size'],
type: json['type'],
);
}

429
lib/utils/options.dart Normal file
View File

@@ -0,0 +1,429 @@
import 'dart:async';
import 'dart:collection';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:html_editor_enhanced/html_editor.dart';
/// Options that modify the editor and its behavior
class HtmlEditorOptions {
const HtmlEditorOptions({
this.autoAdjustHeight = true,
this.androidUseHybridComposition = true,
this.adjustHeightForKeyboard = true,
this.characterLimit,
this.customOptions = '',
this.darkMode,
this.disabled = false,
this.filePath,
this.hint,
this.initialText,
this.inputType = HtmlInputType.text,
this.mobileContextMenu,
this.mobileLongPressDuration,
this.mobileInitialScripts,
this.webInitialScripts,
this.shouldEnsureVisible = false,
this.spellCheck = false,
});
/// The editor will automatically adjust its height when the keyboard is active
/// to prevent the keyboard overlapping the editor.
///
/// The default value is true. It is recommended to leave this as true because
/// it significantly improves the UX.
final bool adjustHeightForKeyboard;
/// ALlows devs to set hybrid composition off in case they would like to
/// prioritize animation smoothness over text input experience.
///
/// The recommended value is `true`.
final bool androidUseHybridComposition;
/// The editor will automatically adjust its height once the page is loaded to
/// ensure there is no vertical scrolling or empty space. It will only perform
/// the adjustment when the summernote editor is the loaded page.
///
/// It will also disable vertical scrolling on the webview, so scrolling on
/// the webview will actually scroll the rest of the page rather than doing
/// nothing because it is trying to scroll the webview container.
///
/// The default value is true. It is recommended to leave this as true because
/// it significantly improves the UX.
final bool autoAdjustHeight;
/// Adds a character limit to the editor.
///
/// NOTE: ONLY WORKS ON iOS AND WEB PLATFORMS!!
final int? characterLimit;
/// Set custom options for the summernote editor by using their syntax.
///
/// Please ensure your syntax is correct (and add a comma at the end of your
/// string!) otherwise the editor may not load.
final String customOptions;
/// Sets the editor to dark mode. `null` - switches with system, `false` -
/// always light, `true` - always dark.
///
/// The default value is null (switches with system).
final bool? darkMode;
/// Disable the editor immediately after startup. You can re-enable the editor
/// by calling [controller.enable()].
final bool disabled;
/// Specify the file path to your custom html editor code.
///
/// Make sure to set the editor's HTML ID to be 'summernote-2'.
///
/// If you plan to use this on Web, you must add comments in your HTML so the
/// package can insert the relevant JS code to communicate between Dart and JS.
/// See the README for more details on this.
final String? filePath;
/// Sets the Html editor's hint (text displayed when there is no text in the
/// editor).
final String? hint;
/// The initial text that is be supplied to the Html editor.
final String? initialText;
/// Changes the display of the virtual keyboard on mobile devices.
///
/// See [HtmlInputType] for the supported modes.
///
/// The default value is [HtmlInputType.text] (the standard virtual keyboard)
final HtmlInputType inputType;
/// Customize the context menu for selected text on mobile
final ContextMenu? mobileContextMenu;
/// Set the duration until a long-press is recognized.
///
/// The default value is 500ms.
final Duration? mobileLongPressDuration;
/// Initial JS to inject into the editor.
final UnmodifiableListView<UserScript>? mobileInitialScripts;
/// Initial JS to add to the editor. These can be called at any time using
/// [controller.evaluateJavascriptWeb]
final UnmodifiableListView<WebScript>? webInitialScripts;
/// Specifies whether the widget should scroll to reveal the HTML editor when
/// it is focused or the text content is changed.
/// See the README examples for the best way to implement this.
///
/// Note: Your editor *must* be in a Scrollable type widget (e.g. ListView,
/// SingleChildScrollView, etc.) for this to work. Otherwise, nothing will
/// happen.
final bool shouldEnsureVisible;
/// Specify whether or not the editor should spellcheck its contents.
///
/// Default value is false.
final bool spellCheck;
}
/// Options that modify the toolbar and its behavior
class HtmlToolbarOptions {
const HtmlToolbarOptions({
this.audioExtensions,
this.customToolbarButtons = const [],
this.customToolbarInsertionIndices = const [],
this.defaultToolbarButtons = const [
StyleButtons(),
FontSettingButtons(fontSizeUnit: false),
FontButtons(clearAll: false),
ColorButtons(),
ListButtons(listStyles: false),
ParagraphButtons(
textDirection: false, lineHeight: false, caseConverter: false),
InsertButtons(
video: false,
audio: false,
table: false,
hr: false,
otherFile: false),
],
this.otherFileExtensions,
this.imageExtensions,
this.initiallyExpanded = false,
this.linkInsertInterceptor,
this.mediaLinkInsertInterceptor,
this.mediaUploadInterceptor,
this.onButtonPressed,
this.onDropdownChanged,
this.onOtherFileLinkInsert,
this.onOtherFileUpload,
this.toolbarType = ToolbarType.nativeScrollable,
this.toolbarPosition = ToolbarPosition.aboveEditor,
this.videoExtensions,
this.dropdownElevation = 8,
this.dropdownIcon,
this.dropdownIconColor,
this.dropdownIconSize = 24,
this.dropdownItemHeight = kMinInteractiveDimension,
this.dropdownFocusColor,
this.dropdownBackgroundColor,
this.dropdownMenuDirection,
this.dropdownMenuMaxHeight,
this.dropdownBoxDecoration,
this.buttonColor,
this.buttonSelectedColor,
this.buttonFillColor,
this.buttonFocusColor,
this.buttonHighlightColor,
this.buttonHoverColor,
this.buttonSplashColor,
this.buttonBorderColor,
this.buttonSelectedBorderColor,
this.buttonBorderRadius,
this.buttonBorderWidth,
this.renderBorder = false,
this.textStyle,
this.separatorWidget =
const VerticalDivider(indent: 2, endIndent: 2, color: Colors.grey),
this.renderSeparatorWidget = true,
this.toolbarItemHeight = 36,
this.gridViewHorizontalSpacing = 5,
this.gridViewVerticalSpacing = 5,
});
/// Allows you to set the allowed extensions when a user inserts an audio file
///
/// By default any audio extension is allowed.
final List<String>? audioExtensions;
/// Allows you to create your own buttons that are added to the end of the
/// default buttons list
final List<Widget> customToolbarButtons;
/// Allows you to set where each custom toolbar button is inserted into the
/// toolbar buttons.
///
/// Notes: 1) This list should have the same length as the [customToolbarButtons]
///
/// 2) If any indices > [defaultToolbarButtons.length] then the plugin will
/// automatically account for this and insert the buttons at the end of the
/// [defaultToolbarButtons]
///
/// 3) If any indices < 0 then the plugin will automatically account for this
/// and insert the buttons at the beginning of the [defaultToolbarButtons]
final List<int> customToolbarInsertionIndices;
/// Sets which options are visible in the toolbar for the editor.
final List<Toolbar> defaultToolbarButtons;
/// Allows you to set the allowed extensions when a user inserts an image
///
/// By default any image extension is allowed.
final List<String>? imageExtensions;
/// Allows you to set whether the toolbar starts out expanded (in gridview)
/// or contracted (in scrollview).
///
/// By default it starts out contracted.
///
/// This option only works when you have set [toolbarType] to
/// [ToolbarType.nativeExpandable].
final bool initiallyExpanded;
/// Allows you to intercept any links being inserted into the editor. The
/// function passes the display text, the URL itself, and whether the
/// URL should open a new tab.
///
/// Return a bool to tell the plugin if it should continue with its own handler
/// or if you want to handle the link by yourself.
/// (true = continue with internal handler, false = do not use internal handler)
///
/// If no interceptor is set, the plugin uses the internal handler.
final FutureOr<bool> Function(String, String, bool)? linkInsertInterceptor;
/// Allows you to intercept any image/video/audio inserted as a link into the editor.
/// The function passes the URL of the media inserted.
///
/// Return a bool to tell the plugin if it should continue with its own handler
/// or if you want to handle the image/video link by yourself.
/// (true = continue with internal handler, false = do not use internal handler)
///
/// If no interceptor is set, the plugin uses the internal handler.
final FutureOr<bool> Function(String, InsertFileType)?
mediaLinkInsertInterceptor;
/// Allows you to intercept any image/video/audio files being inserted into the editor.
/// The function passes the PlatformFile class, which contains all the file data
/// including name, size, type, Uint8List bytes, etc.
///
/// Return a bool to tell the plugin if it should continue with its own handler
/// or if you want to handle the image/video/audio upload by yourself.
/// (true = continue with internal handler, false = do not use internal handler)
///
/// If no interceptor is set, the plugin uses the internal handler.
final FutureOr<bool> Function(PlatformFile, InsertFileType)?
mediaUploadInterceptor;
/// Allows you to intercept any button press. The function passes the ButtonType
/// enum, which tells you which button was pressed, the current selected status of
/// the button, and a function to reverse the status (in case you decide to handle
/// the button press yourself).
///
/// Note: In some cases, the button is never active (e.g. copy/paste buttons)
/// so null will be returned for both the selected status and the function.
///
/// Return a bool to tell the plugin if it should continue with its own handler
/// or if you want to handle the button press by yourself.
/// (true = continue with internal handler, false = do not use internal handler)
///
/// If no interceptor is set, the plugin uses the internal handler.
final FutureOr<bool> Function(ButtonType, bool?, Function?)? onButtonPressed;
/// Allows you to intercept any dropdown changes. The function passes the
/// DropdownType enum, which tells you which dropdown was changed,
/// the changed value to indicate what the dropdown was changed to, and the
/// function to update the changed value (in case you decide to handle the
/// dropdown change yourself). The function is null in some cases because
/// the dropdown does not update its value.
///
/// Return a bool to tell the plugin if it should continue with its own handler
/// or if you want to handle the dropdown change by yourself.
/// (true = continue with internal handler, false = do not use internal handler)
///
/// If no interceptor is set, the plugin uses the internal handler.
final FutureOr<bool> Function(DropdownType, dynamic, void Function(dynamic)?)?
onDropdownChanged;
/// Called when a link is inserted for a file using the "other file" button.
///
/// The package does not have a built in handler for these files, so you should
/// provide this callback when using the button.
///
/// The function passes the URL of the file inserted.
final void Function(String)? onOtherFileLinkInsert;
/// Called when a file is uploaded using the "other file" button.
///
/// The package does not have a built in handler for these files, so if you use
/// the button you should provide this callback.
///
/// The function passes the PlatformFile class, which contains all the file data
/// including name, size, type, Uint8List bytes, etc.
final void Function(PlatformFile)? onOtherFileUpload;
/// Allows you to set the allowed extensions when a user inserts a file other
/// than image/audio/video
///
/// By default any other extension is allowed.
final List<String>? otherFileExtensions;
/// Controls how the toolbar displays. See [ToolbarType] for more details.
///
/// By default the toolbar is rendered as a scrollable one-line list.
final ToolbarType toolbarType;
/// Controls where the toolbar is positioned. See [ToolbarPosition] for more details.
///
/// By default the toolbar is above the editor.
final ToolbarPosition toolbarPosition;
/// Allows you to set the allowed extensions when a user inserts a video.
///
/// By default any video extension is allowed.
final List<String>? videoExtensions;
/// Styling options for the toolbar:
/// Determines whether a border is rendered around all toolbar widgets
///
/// The default value is false. True is recommended for [ToolbarType.nativeGrid].
final bool renderBorder;
/// Sets the text style for all toolbar widgets
final TextStyle? textStyle;
/// Sets the separator widget between toolbar sections. This widget is only
/// used in [ToolbarType.nativeScrollable].
///
/// The default widget is [VerticalDivider(indent: 2, endIndent: 2, color: Colors.grey)]
final Widget separatorWidget;
/// Determines whether the separator widget is rendered
///
/// The default value is true
final bool renderSeparatorWidget;
/// Sets the height of the toolbar items
///
/// Button width is affected by this parameter, however dropdown widths are
/// not affected. The plugin will maintain a square shape for all buttons.
///
/// The default value is 36
final double toolbarItemHeight;
/// Sets the vertical spacing between rows when using [ToolbarType.nativeGrid]
///
/// The default value is 5
final double gridViewVerticalSpacing;
/// Sets the horizontal spacing between items when using [ToolbarType.nativeGrid]
///
/// The default value is 5
final double gridViewHorizontalSpacing;
/// Styling options that only apply to dropdowns:
/// (See the [DropdownButton] class for more information)
final int dropdownElevation;
final Widget? dropdownIcon;
final Color? dropdownIconColor;
final double dropdownIconSize;
final double dropdownItemHeight;
final Color? dropdownFocusColor;
final Color? dropdownBackgroundColor;
/// Set the menu opening direction for the dropdown. Only useful when using
/// [ToolbarPosition.custom] since the toolbar otherwise automatically
/// determines the correct direction.
final DropdownMenuDirection? dropdownMenuDirection;
final double? dropdownMenuMaxHeight;
final BoxDecoration? dropdownBoxDecoration;
/// Styling options that only apply to the buttons:
/// (See the [ToggleButtons] class for more information)
final Color? buttonColor;
final Color? buttonSelectedColor;
final Color? buttonFillColor;
final Color? buttonFocusColor;
final Color? buttonHighlightColor;
final Color? buttonHoverColor;
final Color? buttonSplashColor;
final Color? buttonBorderColor;
final Color? buttonSelectedBorderColor;
final BorderRadius? buttonBorderRadius;
final double? buttonBorderWidth;
}
/// Other options such as the height of the widget and the decoration surrounding it
class OtherOptions {
const OtherOptions({
this.decoration = const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(4)),
border:
Border.fromBorderSide(BorderSide(color: Color(0xffececec), width: 1)),
),
this.height = 400,
});
/// The BoxDecoration to use around the Html editor. By default, the widget
/// uses a thin, dark, rounded rectangle border around the widget.
final BoxDecoration decoration;
/// Sets the height of the Html editor widget. This takes the toolbar into
/// account (i.e. this sets the height of the entire widget rather than the
/// editor space)
///
/// The default value is 400.
final double height;
}

53
lib/utils/plugins.dart Normal file
View File

@@ -0,0 +1,53 @@
import 'package:flutter/foundation.dart';
/// Abstract class that all the plguin classes extend
abstract class Plugins {
const Plugins();
/// Provides the JS and CSS tags to be inserted inside <head>. Only used for Web
String getHeadString();
/// Provides the toolbar option for the plugin
String getToolbarString();
}
/// Summernote @ Mention plugin - adds a dropdown to select the person to mention whenever
/// the '@' character is typed into the editor. The list of people to mention is
/// drawn from the [getSuggestionsMobile] (on mobile) or [mentionsWeb] (on Web)
/// parameter. You can detect who was mentioned using the [onSelect] callback.
///
/// README available [here](https://github.com/team-loxo/summernote-at-mention)
class SummernoteAtMention extends Plugins {
/// Function used to get the displayed suggestions on mobile
final List<String> Function(String)? getSuggestionsMobile;
/// List of mentions to display on Web. The default behavior is to only return
/// the mentions containing the string entered by the user in the editor
final List<String>? mentionsWeb;
/// Callback to run code when a mention is selected
final void Function(String)? onSelect;
const SummernoteAtMention(
{this.getSuggestionsMobile, this.mentionsWeb, this.onSelect})
: assert(kIsWeb ? mentionsWeb != null : getSuggestionsMobile != null);
@override
String getHeadString() {
return '<script src=\"assets/packages/html_editor_enhanced/assets/plugins/summernote-at-mention/summernote-at-mention.js\"></script>';
}
@override
String getToolbarString() {
return '';
}
String getMentionsWeb() {
var mentionsString = '[';
for (var e in mentionsWeb!) {
mentionsString =
mentionsString + "'$e'" + (e != mentionsWeb!.last ? ', ' : '');
}
return mentionsString + ']';
}
}

View File

@@ -0,0 +1,10 @@
// Copyright 2019 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// This file shims dart:ui in web-only scenarios, getting rid of the need to
/// suppress analyzer warnings.
// TODO(tneotia): flutter/flutter#55000 Remove this file once web-only dart:ui APIs
// are exposed from a dedicated place.
export 'dart_ui_fake.dart' if (dart.library.html) 'dart_ui_real.dart';

View File

@@ -0,0 +1,21 @@
// Copyright 2019 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Fake interface for the logic that this package needs from (web-only) dart:ui.
// This is conditionally exported so the analyzer sees these methods as available.
/// Shim for web_ui engine.PlatformViewRegistry
/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L62
// ignore: camel_case_types
class platformViewRegistry {
/// Shim for registerViewFactory
/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L72
static void registerViewFactory(
String viewTypeId, dynamic Function(int viewId) viewFactory) {}
}
/// Signature of callbacks that have no arguments and return no data.
typedef VoidCallback = void Function();
dynamic get window => null;

View File

@@ -0,0 +1,5 @@
// Copyright 2019 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
export 'dart:ui';

View File

@@ -0,0 +1,284 @@
///Class that is used by [WebView.shouldOverrideUrlLoading] event.
///It represents the policy to pass back to the decision handler.
class NavigationActionPolicy {
final int _value;
const NavigationActionPolicy._internal(this._value);
int toValue() => _value;
///Cancel the navigation.
static const CANCEL = NavigationActionPolicy._internal(0);
///Allow the navigation to continue.
static const ALLOW = NavigationActionPolicy._internal(1);
@override
bool operator ==(value) => value == _value;
@override
int get hashCode => _value.hashCode;
Map<String, dynamic> toMap() {
return {
'action': _value,
};
}
}
///Class that represents the WebView context menu. It used by [WebView.contextMenu].
///
///**NOTE**: To make it work properly on Android, JavaScript should be enabled!
class ContextMenu {
///Event fired when the context menu for this WebView is being built.
///
///[hitTestResult] represents the hit result for hitting an HTML elements.
final void Function(dynamic hitTestResult)? onCreateContextMenu;
///Event fired when the context menu for this WebView is being hidden.
final void Function()? onHideContextMenu;
///Event fired when a context menu item has been clicked.
///
///[contextMenuItemClicked] represents the [ContextMenuItem] clicked.
final void Function(ContextMenuItem contextMenuItemClicked)?
onContextMenuActionItemClicked;
///Context menu options.
final ContextMenuOptions? options;
///List of the custom [ContextMenuItem].
final List<ContextMenuItem> menuItems;
ContextMenu(
{this.menuItems = const [],
this.onCreateContextMenu,
this.onHideContextMenu,
this.options,
this.onContextMenuActionItemClicked});
Map<String, dynamic> toMap() {
return {
'menuItems': menuItems.map((menuItem) => menuItem.toMap()).toList(),
'options': options?.toMap()
};
}
Map<String, dynamic> toJson() {
return toMap();
}
@override
String toString() {
return toMap().toString();
}
}
///Class that represent an item of the [ContextMenu].
class ContextMenuItem {
///Android menu item ID.
int? androidId;
///iOS menu item ID.
String? iosId;
///Menu item title.
String title;
///Menu item action that will be called when an user clicks on it.
Function()? action;
ContextMenuItem(
{this.androidId, this.iosId, required this.title, this.action});
Map<String, dynamic> toMap() {
return {'androidId': androidId, 'iosId': iosId, 'title': title};
}
Map<String, dynamic> toJson() {
return toMap();
}
@override
String toString() {
return toMap().toString();
}
}
///Class that represents available options used by [ContextMenu].
class ContextMenuOptions {
///Whether all the default system context menu items should be hidden or not. The default value is `false`.
bool hideDefaultSystemContextMenuItems;
ContextMenuOptions({this.hideDefaultSystemContextMenuItems = false});
Map<String, dynamic> toMap() {
return {
'hideDefaultSystemContextMenuItems': hideDefaultSystemContextMenuItems
};
}
Map<String, dynamic> toJson() {
return toMap();
}
@override
String toString() {
return toMap().toString();
}
}
///Class that represents contains the constants for the times at which to inject script content into a [WebView] used by an [UserScript].
class UserScriptInjectionTime {
final int _value;
const UserScriptInjectionTime._internal(this._value);
static final Set<UserScriptInjectionTime> values = {
UserScriptInjectionTime.AT_DOCUMENT_START,
UserScriptInjectionTime.AT_DOCUMENT_END,
};
static UserScriptInjectionTime? fromValue(int? value) {
if (value != null) {
try {
return UserScriptInjectionTime.values
.firstWhere((element) => element.toValue() == value);
} catch (e) {
return null;
}
}
return null;
}
int toValue() => _value;
@override
String toString() {
switch (_value) {
case 1:
return 'AT_DOCUMENT_END';
case 0:
default:
return 'AT_DOCUMENT_START';
}
}
///**NOTE for iOS**: A constant to inject the script after the creation of the webpages document element, but before loading any other content.
///
///**NOTE for Android**: A constant to try to inject the script as soon as the page starts loading.
static const AT_DOCUMENT_START = UserScriptInjectionTime._internal(0);
///**NOTE for iOS**: A constant to inject the script after the document finishes loading, but before loading any other subresources.
///
///**NOTE for Android**: A constant to inject the script as soon as the page finishes loading.
static const AT_DOCUMENT_END = UserScriptInjectionTime._internal(1);
@override
bool operator ==(value) => value == _value;
@override
int get hashCode => _value.hashCode;
}
///Class that represents a script that the [WebView] injects into the web page.
class UserScript {
///The scripts group name.
String? groupName;
///The scripts source code.
String source;
///The time at which to inject the script into the [WebView].
UserScriptInjectionTime injectionTime;
///A Boolean value that indicates whether to inject the script into the main frame.
///Specify true to inject the script only into the main frame, or false to inject it into all frames.
///The default value is `true`.
///
///**NOTE**: available only on iOS.
bool iosForMainFrameOnly;
///A scope of execution in which to evaluate the script to prevent conflicts between different scripts.
///For more information about content worlds, see [ContentWorld].
late ContentWorld contentWorld;
UserScript(
{this.groupName,
required this.source,
required this.injectionTime,
this.iosForMainFrameOnly = true,
ContentWorld? contentWorld}) {
this.contentWorld = contentWorld ?? ContentWorld.PAGE;
}
Map<String, dynamic> toMap() {
return {
'groupName': groupName,
'source': source,
'injectionTime': injectionTime.toValue(),
'iosForMainFrameOnly': iosForMainFrameOnly,
'contentWorld': contentWorld.toMap()
};
}
Map<String, dynamic> toJson() {
return toMap();
}
@override
String toString() {
return toMap().toString();
}
}
final _contentWorldNameRegExp = RegExp(r'[\s]');
///Class that represents an object that defines a scope of execution for JavaScript code and which you use to prevent conflicts between different scripts.
///
///**NOTE for iOS**: available on iOS 14.0+. This class represents the native [WKContentWorld](https://developer.apple.com/documentation/webkit/wkcontentworld) class.
///
///**NOTE for Android**: it will create and append an `<iframe>` HTML element with `id` attribute equals to `flutter_inappwebview_[name]`
///to the webpage's content that contains only the inline `<script>` HTML elements in order to define a new scope of execution for JavaScript code.
///Unfortunately, there isn't any other way to do it.
///There are some limitations:
///- for any [ContentWorld], except [ContentWorld.PAGE] (that is the webpage itself), if you need to access to the `window` or `document` global Object,
///you need to use `window.top` and `window.top.document` because the code runs inside an `<iframe>`;
///- also, the execution of the inline `<script>` could be blocked by the `Content-Security-Policy` header.
class ContentWorld {
///The name of a custom content world.
///It cannot contain space characters.
final String name;
///Returns the custom content world with the specified name.
ContentWorld.world({required this.name}) {
// WINDOW-ID- is used internally by the plugin!
assert(!name.startsWith('WINDOW-ID-') &&
!name.contains(_contentWorldNameRegExp));
}
///The default world for clients.
// ignore: non_constant_identifier_names
static final ContentWorld DEFAULT_CLIENT =
ContentWorld.world(name: 'defaultClient');
///The content world for the current webpages content.
///This property contains the content world for scripts that the current webpage executes.
///Be careful when manipulating variables in this content world.
///If you modify a variable with the same name as one the webpage uses, you may unintentionally disrupt the normal operation of that page.
// ignore: non_constant_identifier_names
static final ContentWorld PAGE = ContentWorld.world(name: 'page');
Map<String, dynamic> toMap() {
return {'name': name};
}
Map<String, dynamic> toJson() {
return toMap();
}
@override
String toString() {
return toMap().toString();
}
}

216
lib/utils/toolbar.dart Normal file
View File

@@ -0,0 +1,216 @@
import 'package:flutter/material.dart';
/// Abstract class that all the toolbar classes extend
abstract class Toolbar {
const Toolbar();
}
/// Style group
class StyleButtons extends Toolbar {
final bool style;
const StyleButtons({
this.style = true,
});
}
/// Font setting group
class FontSettingButtons extends Toolbar {
final bool fontName;
final bool fontSize;
final bool fontSizeUnit;
const FontSettingButtons({
this.fontName = true,
this.fontSize = true,
this.fontSizeUnit = true,
});
}
/// Font group
class FontButtons extends Toolbar {
final bool bold;
final bool italic;
final bool underline;
final bool clearAll;
final bool strikethrough;
final bool superscript;
final bool subscript;
const FontButtons({
this.bold = true,
this.italic = true,
this.underline = true,
this.clearAll = true,
this.strikethrough = true,
this.superscript = true,
this.subscript = true,
});
List<Icon> getIcons1() {
var icons = <Icon>[];
if (bold) icons.add(Icon(Icons.format_bold));
if (italic) icons.add(Icon(Icons.format_italic));
if (underline) icons.add(Icon(Icons.format_underline));
if (clearAll) icons.add(Icon(Icons.format_clear));
return icons;
}
List<Icon> getIcons2() {
var icons = <Icon>[];
if (strikethrough) icons.add(Icon(Icons.format_strikethrough));
if (superscript) icons.add(Icon(Icons.superscript));
if (subscript) icons.add(Icon(Icons.subscript));
return icons;
}
}
/// Color bar group
class ColorButtons extends Toolbar {
final bool foregroundColor;
final bool highlightColor;
const ColorButtons({
this.foregroundColor = true,
this.highlightColor = true,
});
List<Icon> getIcons() {
var icons = <Icon>[];
if (foregroundColor) icons.add(Icon(Icons.format_color_text));
if (highlightColor) icons.add(Icon(Icons.format_color_fill));
return icons;
}
}
/// List group
class ListButtons extends Toolbar {
final bool ul;
final bool ol;
final bool listStyles;
const ListButtons({
this.ul = true,
this.ol = true,
this.listStyles = true,
});
List<Icon> getIcons() {
var icons = <Icon>[];
if (ul) icons.add(Icon(Icons.format_list_bulleted));
if (ol) icons.add(Icon(Icons.format_list_numbered));
return icons;
}
}
/// Paragraph group
class ParagraphButtons extends Toolbar {
final bool alignLeft;
final bool alignCenter;
final bool alignRight;
final bool alignJustify;
final bool increaseIndent;
final bool decreaseIndent;
final bool textDirection;
final bool lineHeight;
final bool caseConverter;
const ParagraphButtons({
this.alignLeft = true,
this.alignCenter = true,
this.alignRight = true,
this.alignJustify = true,
this.increaseIndent = true,
this.decreaseIndent = true,
this.textDirection = true,
this.lineHeight = true,
this.caseConverter = true,
});
List<Icon> getIcons1() {
var icons = <Icon>[];
if (alignLeft) icons.add(Icon(Icons.format_align_left));
if (alignCenter) icons.add(Icon(Icons.format_align_center));
if (alignRight) icons.add(Icon(Icons.format_align_right));
if (alignJustify) icons.add(Icon(Icons.format_align_justify));
return icons;
}
List<Icon> getIcons2() {
var icons = <Icon>[];
if (increaseIndent) icons.add(Icon(Icons.format_indent_increase));
if (decreaseIndent) icons.add(Icon(Icons.format_indent_decrease));
return icons;
}
}
/// Insert group
class InsertButtons extends Toolbar {
final bool link;
final bool picture;
final bool audio;
final bool video;
final bool otherFile;
final bool table;
final bool hr;
const InsertButtons({
this.link = true,
this.picture = true,
this.audio = true,
this.video = true,
this.otherFile = false,
this.table = true,
this.hr = true,
});
List<Icon> getIcons() {
var icons = <Icon>[];
if (link) icons.add(Icon(Icons.link));
if (picture) icons.add(Icon(Icons.image_outlined));
if (audio) icons.add(Icon(Icons.audiotrack_outlined));
if (video) icons.add(Icon(Icons.videocam_outlined));
if (otherFile) icons.add(Icon(Icons.attach_file));
if (table) icons.add(Icon(Icons.table_chart_outlined));
if (hr) icons.add(Icon(Icons.horizontal_rule));
return icons;
}
}
/// Miscellaneous group
class OtherButtons extends Toolbar {
final bool fullscreen;
final bool codeview;
final bool undo;
final bool redo;
final bool help;
final bool copy;
final bool paste;
const OtherButtons({
this.fullscreen = true,
this.codeview = true,
this.undo = true,
this.redo = true,
this.help = true,
this.copy = true,
this.paste = true,
});
List<Icon> getIcons1() {
var icons = <Icon>[];
if (fullscreen) icons.add(Icon(Icons.fullscreen));
if (codeview) icons.add(Icon(Icons.code));
if (undo) icons.add(Icon(Icons.undo));
if (redo) icons.add(Icon(Icons.redo));
if (help) icons.add(Icon(Icons.help_outline));
return icons;
}
List<Icon> getIcons2() {
var icons = <Icon>[];
if (copy) icons.add(Icon(Icons.copy));
if (paste) icons.add(Icon(Icons.paste));
return icons;
}
}

1201
lib/utils/utils.dart Normal file

File diff suppressed because it is too large Load Diff