added project'
This commit is contained in:
BIN
lib/assets/font/summernote.eot
Normal file
BIN
lib/assets/font/summernote.eot
Normal file
Binary file not shown.
BIN
lib/assets/font/summernote.ttf
Normal file
BIN
lib/assets/font/summernote.ttf
Normal file
Binary file not shown.
2
lib/assets/jquery.min.js
vendored
Normal file
2
lib/assets/jquery.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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();
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
24
lib/assets/summernote-lite-dark.css
Normal file
24
lib/assets/summernote-lite-dark.css
Normal 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
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
3
lib/assets/summernote-lite.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
32
lib/assets/summernote-no-plugins.html
Normal file
32
lib/assets/summernote-no-plugins.html
Normal 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>
|
||||
31
lib/assets/summernote.html
Normal file
31
lib/assets/summernote.html
Normal 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
102
lib/html_editor.dart
Normal 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 }
|
||||
291
lib/src/html_editor_controller_mobile.dart
Normal file
291
lib/src/html_editor_controller_mobile.dart
Normal 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) {}
|
||||
}
|
||||
174
lib/src/html_editor_controller_unsupported.dart
Normal file
174
lib/src/html_editor_controller_unsupported.dart
Normal 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) {}
|
||||
}
|
||||
329
lib/src/html_editor_controller_web.dart
Normal file
329
lib/src/html_editor_controller_web.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
57
lib/src/html_editor_mobile.dart
Normal file
57
lib/src/html_editor_mobile.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
40
lib/src/html_editor_unsupported.dart
Normal file
40
lib/src/html_editor_unsupported.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
56
lib/src/html_editor_web.dart
Normal file
56
lib/src/html_editor_web.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
791
lib/src/widgets/html_editor_widget_mobile.dart
Normal file
791
lib/src/widgets/html_editor_widget_mobile.dart
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
799
lib/src/widgets/html_editor_widget_web.dart
Normal file
799
lib/src/widgets/html_editor_widget_web.dart
Normal 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'];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
2986
lib/src/widgets/toolbar_widget.dart
Normal file
2986
lib/src/widgets/toolbar_widget.dart
Normal file
File diff suppressed because it is too large
Load Diff
187
lib/utils/callbacks.dart
Normal file
187
lib/utils/callbacks.dart
Normal 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;
|
||||
}
|
||||
59
lib/utils/file_upload_model.dart
Normal file
59
lib/utils/file_upload_model.dart
Normal 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
429
lib/utils/options.dart
Normal 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
53
lib/utils/plugins.dart
Normal 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 + ']';
|
||||
}
|
||||
}
|
||||
10
lib/utils/shims/dart_ui.dart
Normal file
10
lib/utils/shims/dart_ui.dart
Normal 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';
|
||||
21
lib/utils/shims/dart_ui_fake.dart
Normal file
21
lib/utils/shims/dart_ui_fake.dart
Normal 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;
|
||||
5
lib/utils/shims/dart_ui_real.dart
Normal file
5
lib/utils/shims/dart_ui_real.dart
Normal 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';
|
||||
284
lib/utils/shims/flutter_inappwebview_fake.dart
Normal file
284
lib/utils/shims/flutter_inappwebview_fake.dart
Normal 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 webpage’s 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 script’s group name.
|
||||
String? groupName;
|
||||
|
||||
///The script’s 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 webpage’s 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
216
lib/utils/toolbar.dart
Normal 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
1201
lib/utils/utils.dart
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user