Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## [0.1.6] - 2025-06-26
- Fix changing text formatting style does not work correctly when selecting multiple

## [0.1.5] - 2025-06-23
- Fix for inserting logical signature with incorrect quoting

Expand Down
2 changes: 1 addition & 1 deletion example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ android {
applicationId "de.enough.example"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
minSdkVersion 19
minSdkVersion flutter.minSdkVersion
targetSdkVersion 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
Expand Down
2 changes: 1 addition & 1 deletion example/android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.6.21'
ext.kotlin_version = '1.7.10'
repositories {
google()
mavenCentral()
Expand Down
91 changes: 21 additions & 70 deletions lib/src/editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,6 @@ class HtmlEditorState extends State<HtmlEditor> {
function onSelectionChange() {
let {anchorNode, anchorOffset, focusNode, focusOffset} = document.getSelection();
// traverse all parents to find <b>, <i> or <u> elements:
var isBold = false;
var isItalic = false;
var isUnderline = false;
var isStrikeThrough = false;
var node = anchorNode;
var textAlign = undefined;
var nestedBlockqotes = 0;
Expand All @@ -197,51 +193,18 @@ class HtmlEditorState extends State<HtmlEditor> {
// window.flutter_inappwebview.callHandler('OffsetTracker', JSON.stringify(boundingRect));
// }
// }
if (node.nodeName == 'B') {
isBold = true;
} else if (node.nodeName === 'I') {
isItalic = true;
} else if (node.nodeName === 'U') {
isUnderline = true;
} else if (node.nodeName === 'STRIKE') {
isStrikeThrough = true;
} else if (node.nodeName === 'BLOCKQUOTE') {
if (node.nodeName === 'BLOCKQUOTE') {
nestedBlockqotes++;
rootBlockquote = node;
} else if (node.nodeName === 'UL' || node.nodeName === 'OL') {
isChildOfList = true;
} else if (node.nodeName === 'SPAN' && node.style != undefined) {
// check for color, bold, etc in style:
if (node.style.fontWeight === 'bold' || node.style.fontWeight === '700') {
isBold = true;
}
if (node.style.fontStyle === 'italic') {
isItalic = true;
}
if (fontSize == undefined && node.style.fontSize != undefined) {
fontSize = node.style.fontSize;
}
if (fontFamily == undefined && node.style.fontFamily != undefined) {
fontFamily = node.style.fontFamily;
}
var textDecorationLine = node.style.textDecorationLine;
if (textDecorationLine === '') {
textDecorationLine = node.style.textDecoration;
}
if (textDecorationLine != undefined) {
if (textDecorationLine === 'underline') {
isUnderline = true;
} else if (textDecorationLine === 'line-through') {
isStrikeThrough = true;
} else {
if (!isUnderline) {
isUnderline = textDecorationLine.includes('underline');
}
if (!isStrikeThrough) {
isStrikeThrough = textDecorationLine.includes('line-through');
}
}
}
if (foregroundColor == undefined && node.style.color != undefined) {
foregroundColor = node.style.color;
}
Expand All @@ -258,26 +221,6 @@ class HtmlEditorState extends State<HtmlEditor> {
node = node.parentNode;
}
isInList = isChildOfList;
if (isBold != isSelectionBold || isItalic != isSelectionItalic || isUnderline != isSelectionUnderline || isStrikeThrough != isSelectionStrikeThrough) {
isSelectionBold = isBold;
isSelectionItalic = isItalic;
isSelectionUnderline = isUnderline;
isSelectionStrikeThrough = isStrikeThrough;
var message = 0;
if (isBold) {
message += 1;
}
if (isItalic) {
message += 2;
}
if (isUnderline) {
message += 4;
}
if (isStrikeThrough) {
message += 8;
}
window.flutter_inappwebview.callHandler('FormatSettings', message);
}
if (textAlign != selectionTextAlign) {
selectionTextAlign = textAlign;
window.flutter_inappwebview.callHandler('AlignSettings', textAlign);
Expand Down Expand Up @@ -484,6 +427,10 @@ class HtmlEditorState extends State<HtmlEditor> {
document.execCommand("styleWithCSS", false, true);

$jsHandleLazyLoadingBackgroundImage

observeTextFormatting(editor, (format) => {
window.flutter_inappwebview.callHandler('FormatSettings', format);
});
}

function displayCursorCoordinates(event) {
Expand All @@ -504,6 +451,8 @@ class HtmlEditorState extends State<HtmlEditor> {
let result = [x,y].toString();
window.flutter_inappwebview.callHandler('InternalUpdateCursorCoordinates', result);
}

$jsHandleTextFormatting
</script>
</head>
<body onload="onLoaded();">
Expand Down Expand Up @@ -736,18 +685,20 @@ pre {

void _onFormatSettingsReceived(List<dynamic> parameters) {
log('_onFormatSettingsReceived: $parameters');
final int numericMessage = parameters.first;
final callback = _api.onFormatSettingsChanged;
if (callback != null) {
callback(
FormatSettings(
isBold: (numericMessage & 1) == 1,
isItalic: (numericMessage & 2) == 2,
isUnderline: (numericMessage & 4) == 4,
isStrikeThrough: (numericMessage & 8) == 8,
),
);
}
final format = Map<String, dynamic>.from(parameters[0]);
final isBold = format['bold'] == true;
final isItalic = format['italic'] == true;
final isUnderline = format['underline'] == true;
final isStrikeThrough = format['strikeThrough'] == true;

_api.onFormatSettingsChanged?.call(
FormatSettings(
isBold: isBold,
isItalic: isItalic,
isUnderline: isUnderline,
isStrikeThrough: isStrikeThrough,
),
);
}

void _onFontSizeSettingsReceived(List<dynamic> parameters) {
Expand Down
81 changes: 81 additions & 0 deletions lib/src/utils/javascript_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -205,4 +205,85 @@ const String jsHandleLazyLoadingBackgroundImage = '''
lazyImages.forEach((lazyImage) => {
lazyImageObserver.observe(lazyImage);
});
''';

const String jsHandleTextFormatting= '''
function observeTextFormatting(editorElement, onFormatChange) {
let lastFormat = {};

function detectFormatting() {
const selection = window.getSelection();
let node = selection.rangeCount > 0 ? selection.getRangeAt(0).startContainer : null;

if (!node) return;

if (node.nodeType === Node.TEXT_NODE) {
node = node.parentNode;
}

let format = {
bold: false,
italic: false,
underline: false,
strikeThrough: false,
};

let current = node;

while (current && current.nodeType === 1) {
const tag = current.tagName?.toLowerCase?.() || "";
const style = current.style || {};
const computed = window.getComputedStyle(current);

// Tag-based checks
if (tag === 'b' || tag === 'strong') format.bold = true;
if (tag === 'i' || tag === 'em') format.italic = true;
if (tag === 'u') format.underline = true;
if (['s', 'strike', 'del'].includes(tag)) format.strikeThrough = true;

// Inline style checks
if (style.fontWeight === 'bold' || style.fontWeight === '700') format.bold = true;
if (style.fontStyle === 'italic') format.italic = true;
if (style.textDecoration?.includes('underline')) format.underline = true;
if (style.textDecoration?.includes('line-through')) format.strikeThrough = true;

// Computed style checks
if (computed.fontWeight === 'bold' || parseInt(computed.fontWeight) >= 600) format.bold = true;
if (computed.fontStyle === 'italic') format.italic = true;
if (computed.textDecorationLine?.includes('underline')) format.underline = true;
if (computed.textDecorationLine?.includes('line-through')) format.strikeThrough = true;

current = current.parentNode;
}

const formatChanged = (
format.bold !== lastFormat.bold ||
format.italic !== lastFormat.italic ||
format.underline !== lastFormat.underline ||
format.strikeThrough !== lastFormat.strikeThrough
);

if (formatChanged) {
lastFormat = format;
onFormatChange(format);
}
}

const observer = new MutationObserver(() => {
detectFormatting();
});

observer.observe(editorElement, {
childList: true,
characterData: true,
attributes: true,
subtree: true,
});

document.addEventListener('selectionchange', () => {
if (editorElement.contains(document.activeElement)) {
detectFormatting();
}
});
}
''';
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: enough_html_editor
description: Slim HTML editor for Flutter with full API control and optional Flutter-based widget controls.
version: 0.1.5
version: 0.1.6
homepage: https://github.com/Enough-Software/enough_html_editor

environment:
Expand Down