diff --git a/CHANGELOG.md b/CHANGELOG.md index 8363b27..ced5a46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index dc11704..5d9341d 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -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 diff --git a/example/android/build.gradle b/example/android/build.gradle index f789415..1ad09f5 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.6.21' + ext.kotlin_version = '1.7.10' repositories { google() mavenCentral() diff --git a/lib/src/editor.dart b/lib/src/editor.dart index 725851a..49d2861 100644 --- a/lib/src/editor.dart +++ b/lib/src/editor.dart @@ -172,10 +172,6 @@ class HtmlEditorState extends State { function onSelectionChange() { let {anchorNode, anchorOffset, focusNode, focusOffset} = document.getSelection(); // traverse all parents to find , or elements: - var isBold = false; - var isItalic = false; - var isUnderline = false; - var isStrikeThrough = false; var node = anchorNode; var textAlign = undefined; var nestedBlockqotes = 0; @@ -197,51 +193,18 @@ class HtmlEditorState extends State { // 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; } @@ -258,26 +221,6 @@ class HtmlEditorState extends State { 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); @@ -484,6 +427,10 @@ class HtmlEditorState extends State { document.execCommand("styleWithCSS", false, true); $jsHandleLazyLoadingBackgroundImage + + observeTextFormatting(editor, (format) => { + window.flutter_inappwebview.callHandler('FormatSettings', format); + }); } function displayCursorCoordinates(event) { @@ -504,6 +451,8 @@ class HtmlEditorState extends State { let result = [x,y].toString(); window.flutter_inappwebview.callHandler('InternalUpdateCursorCoordinates', result); } + + $jsHandleTextFormatting @@ -736,18 +685,20 @@ pre { void _onFormatSettingsReceived(List 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.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 parameters) { diff --git a/lib/src/utils/javascript_utils.dart b/lib/src/utils/javascript_utils.dart index 21864ce..82f96e1 100644 --- a/lib/src/utils/javascript_utils.dart +++ b/lib/src/utils/javascript_utils.dart @@ -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(); + } + }); + } '''; \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 2a5b381..7eacd5a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: