From 21ab3006789f622a5df09024741aac2f24d5e7f4 Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 11 Sep 2023 23:57:22 +0700 Subject: [PATCH] Supports expanded/collapsed signatures --- lib/assets/summernote-no-plugins.html | 64 ++++++++- lib/assets/summernote.html | 63 +++++++++ lib/src/html_editor_controller_mobile.dart | 4 + .../html_editor_controller_unsupported.dart | 3 + lib/src/html_editor_controller_web.dart | 21 ++- lib/src/html_editor_mobile.dart | 4 - lib/src/html_editor_unsupported.dart | 4 - lib/src/html_editor_web.dart | 5 - lib/src/widgets/html_editor_widget_web.dart | 11 +- lib/utils/icon_utils.dart | 10 ++ lib/utils/javascript_utils.dart | 132 +++++++++++++++--- 11 files changed, 279 insertions(+), 42 deletions(-) create mode 100644 lib/utils/icon_utils.dart diff --git a/lib/assets/summernote-no-plugins.html b/lib/assets/summernote-no-plugins.html index f9796bdf..2486eeea 100644 --- a/lib/assets/summernote-no-plugins.html +++ b/lib/assets/summernote-no-plugins.html @@ -12,7 +12,7 @@ -
+
@@ -28,6 +28,68 @@ .note-frame { border-radius: 0px; } + + blockquote { + margin-left: 8px; + margin-right: 8px; + padding-left: 12px; + padding-right: 12px; + border-left: 5px solid #eee; + } + + pre { + display: block; + padding: 10px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.5; + color: #333; + word-break: break-all; + word-wrap: break-word; + background-color: #f5f5f5; + border: 1px solid #ccc; + border-radius: 4px; + overflow: auto; + } + + .tmail-signature { + text-align: left; + margin: 16px 0px 16px 0px; + } + + .tmail-signature-button, + .tmail-signature-button * { + box-sizing: border-box; + } + + .tmail-signature-button { + padding: 6px 40px 6px 16px; + border-radius: 4px; + color: #fff; + background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M10.0003 11.8319L5.53383 8.1098C5.18027 7.81516 4.6548 7.86293 4.36016 8.21649C4.06553 8.57006 4.1133 9.09553 4.46686 9.39016L9.46686 13.5568C9.7759 13.8144 10.2248 13.8144 10.5338 13.5568L15.5338 9.39016C15.8874 9.09553 15.9352 8.57006 15.6405 8.21649C15.3459 7.86293 14.8204 7.81516 14.4669 8.1098L10.0003 11.8319Z' fill='%23AEAEC0'/%3E%3C/svg%3E%0A"); + background-repeat: no-repeat; + background-position: right 16px center; + background-color: #FFFFFF; + border-radius: 36px; + border-style: solid; + border-color: var(--m-3-syslight-outline-shadow-outline-variant, #cac4d0); + border-width: 0.5px; + flex-direction: row; + gap: 8px; + align-items: center; + justify-content: flex-start; + flex-shrink: 0; + position: relative; + cursor: pointer; + color: var(--m-3-syslight-tetirary-tertiary, #8c9caf); + text-align: left; + font: var(--m-3-body-large-2, 400 17px/24px "Inter", sans-serif); + } + + .tmail-signature-content { + padding: 12px; + overflow: hidden; + } \ No newline at end of file diff --git a/lib/assets/summernote.html b/lib/assets/summernote.html index 83c9ae56..69120f86 100644 --- a/lib/assets/summernote.html +++ b/lib/assets/summernote.html @@ -15,6 +15,7 @@ + \ No newline at end of file diff --git a/lib/src/html_editor_controller_mobile.dart b/lib/src/html_editor_controller_mobile.dart index 0013e4fc..d511e1d1 100644 --- a/lib/src/html_editor_controller_mobile.dart +++ b/lib/src/html_editor_controller_mobile.dart @@ -77,6 +77,10 @@ class HtmlEditorController extends unsupported.HtmlEditorController { return text ?? ''; } + /// Gets the text from the editor and returns it as a [String]. With signature content + @override + Future getTextWithSignatureContent() => Future.value(''); + /// Sets the text of the editor. Some pre-processing is applied to convert /// [String] elements like "\n" to HTML elements. @override diff --git a/lib/src/html_editor_controller_unsupported.dart b/lib/src/html_editor_controller_unsupported.dart index 05287cc6..f5f9606f 100644 --- a/lib/src/html_editor_controller_unsupported.dart +++ b/lib/src/html_editor_controller_unsupported.dart @@ -93,6 +93,9 @@ class HtmlEditorController { /// Gets the text from the editor and returns it as a [String]. Future getText() => Future.value(''); + /// Gets the text from the editor and returns it as a [String]. With signature content + Future getTextWithSignatureContent() => Future.value(''); + /// Gets the selected HTML from the editor. You should use /// [controller.editorController.getSelectedText()] on mobile. /// diff --git a/lib/src/html_editor_controller_web.dart b/lib/src/html_editor_controller_web.dart index acaf46db..a49b7e9d 100644 --- a/lib/src/html_editor_controller_web.dart +++ b/lib/src/html_editor_controller_web.dart @@ -66,6 +66,21 @@ class HtmlEditorController extends unsupported.HtmlEditorController { return text; } + /// Gets the text with signature content from the editor and returns it as a [String]. + @override + Future getTextWithSignatureContent() async { + _evaluateJavascriptWeb(data: {'type': 'toIframe: getTextWithSignatureContent'}); + var e = await html.window.onMessage.firstWhere( + (element) => json.decode(element.data)['type'] == 'toDart: getTextWithSignatureContent'); + String text = json.decode(e.data)['text']; + if (processOutputHtml && + (text.isEmpty || + text == '

' || + text == '


' || + text == '


')) text = ''; + return text; + } + @override Future getSelectedTextWeb({bool withHtmlTags = false}) async { if (withHtmlTags) { @@ -360,7 +375,11 @@ class HtmlEditorController extends unsupported.HtmlEditorController { @override void insertSignature(String signature) { _evaluateJavascriptWeb( - data: {'type': 'toIframe: insertSignature', 'signature': signature}); + data: { + 'type': 'toIframe: insertSignature', + 'signature': signature, + } + ); } @override diff --git a/lib/src/html_editor_mobile.dart b/lib/src/html_editor_mobile.dart index 5a15dc1a..4ac3e1ea 100644 --- a/lib/src/html_editor_mobile.dart +++ b/lib/src/html_editor_mobile.dart @@ -15,7 +15,6 @@ class HtmlEditor extends StatelessWidget { this.htmlToolbarOptions = const HtmlToolbarOptions(), this.otherOptions = const OtherOptions(), this.plugins = const [], - this.blockQuotedContent, }) : super(key: key); /// The controller that is passed to the widget, which allows multiple [HtmlEditor] @@ -38,9 +37,6 @@ class HtmlEditor extends StatelessWidget { /// Sets the list of Summernote plugins enabled in the editor. final List plugins; - /// Content attach in editor when initial - final String? blockQuotedContent; - @override Widget build(BuildContext context) { if (!kIsWeb) { diff --git a/lib/src/html_editor_unsupported.dart b/lib/src/html_editor_unsupported.dart index 99474a00..32d34e1e 100644 --- a/lib/src/html_editor_unsupported.dart +++ b/lib/src/html_editor_unsupported.dart @@ -11,7 +11,6 @@ class HtmlEditor extends StatelessWidget { this.htmlToolbarOptions = const HtmlToolbarOptions(), this.otherOptions = const OtherOptions(), this.plugins = const [], - this.blockQuotedContent, }) : super(key: key); /// The controller that is passed to the widget, which allows multiple [HtmlEditor] @@ -34,9 +33,6 @@ class HtmlEditor extends StatelessWidget { /// Sets the list of Summernote plugins enabled in the editor. final List plugins; - /// Content attach in editor when initial - final String? blockQuotedContent; - @override Widget build(BuildContext context) { return const Text('Unsupported in this environment'); diff --git a/lib/src/html_editor_web.dart b/lib/src/html_editor_web.dart index f025babf..8976e6a7 100644 --- a/lib/src/html_editor_web.dart +++ b/lib/src/html_editor_web.dart @@ -13,7 +13,6 @@ class HtmlEditor extends StatelessWidget { this.htmlToolbarOptions = const HtmlToolbarOptions(), this.otherOptions = const OtherOptions(), this.plugins = const [], - this.blockQuotedContent, }) : super(key: key); /// The controller that is passed to the widget, which allows multiple [HtmlEditor] @@ -36,9 +35,6 @@ class HtmlEditor extends StatelessWidget { /// Sets the list of Summernote plugins enabled in the editor. final List plugins; - /// Content attach in editor when initial - final String? blockQuotedContent; - @override Widget build(BuildContext context) { if (kIsWeb) { @@ -51,7 +47,6 @@ class HtmlEditor extends StatelessWidget { htmlToolbarOptions: htmlToolbarOptions, otherOptions: otherOptions, initBC: context, - blockQuotedContent: blockQuotedContent, ); } else { return const Text( diff --git a/lib/src/widgets/html_editor_widget_web.dart b/lib/src/widgets/html_editor_widget_web.dart index 4ef436a6..40531d66 100644 --- a/lib/src/widgets/html_editor_widget_web.dart +++ b/lib/src/widgets/html_editor_widget_web.dart @@ -24,7 +24,6 @@ class HtmlEditorWidget extends StatefulWidget { required this.htmlToolbarOptions, required this.otherOptions, required this.initBC, - this.blockQuotedContent, }) : super(key: key); final HtmlEditorController controller; @@ -34,7 +33,6 @@ class HtmlEditorWidget extends StatefulWidget { final HtmlToolbarOptions htmlToolbarOptions; final OtherOptions otherOptions; final BuildContext initBC; - final String? blockQuotedContent; @override State createState() => _HtmlEditorWidgetWebState(); @@ -235,6 +233,12 @@ class _HtmlEditorWidgetWebState extends State { var str = \$('#summernote-2').summernote('code'); window.parent.postMessage(JSON.stringify({"type": "toDart: getText", "text": str}), "*"); } + if (data["type"].includes("getTextWithSignatureContent")) { + ${JavascriptUtils.jsHandleReplaceSignatureContent} + + var str = \$('#summernote-2').summernote('code'); + window.parent.postMessage(JSON.stringify({"type": "toDart: getTextWithSignatureContent", "text": str}), "*"); + } if (data["type"].includes("getHeight")) { var height = document.body.scrollHeight; window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: htmlHeight", "height": height}), "*"); @@ -429,6 +433,7 @@ class _HtmlEditorWidgetWebState extends State { } } + ${JavascriptUtils.jsHandleOnClickSignature} ${JavascriptUtils.jsDetectBrowser} function onSelectionChange() { @@ -516,8 +521,6 @@ class _HtmlEditorWidgetWebState extends State { .replaceFirst('', summernoteScripts) .replaceFirst('', widget.htmlEditorOptions.customBodyCssStyle) - .replaceFirst( - '', widget.blockQuotedContent ?? '') .replaceFirst('"jquery.min.js"', '"assets/packages/html_editor_enhanced/assets/jquery.min.js"') .replaceFirst('"summernote-lite.min.css"', diff --git a/lib/utils/icon_utils.dart b/lib/utils/icon_utils.dart new file mode 100644 index 00000000..149138d7 --- /dev/null +++ b/lib/utils/icon_utils.dart @@ -0,0 +1,10 @@ + +class IconUtils { + static const String chevronUpSVGIconUrlEncoded = ''' + url("data:image/svg+xml,%3Csvg class='chevron-down' width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath d='M14.5352 11.9709C14.8347 12.2276 15.2857 12.193 15.5424 11.8934C15.7991 11.5939 15.7644 11.143 15.4649 10.8863L10.4649 6.60054C10.1974 6.37127 9.8027 6.37127 9.53521 6.60054L4.53521 10.8863C4.23569 11.143 4.201 11.5939 4.45773 11.8934C4.71446 12.193 5.16539 12.2276 5.46491 11.9709L10.0001 8.08364L14.5352 11.9709Z' fill='%23AEAEC0' /%3E%3C/svg%3E") + '''; + + static const String chevronDownSVGIconUrlEncoded = ''' + url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M10.0003 11.8319L5.53383 8.1098C5.18027 7.81516 4.6548 7.86293 4.36016 8.21649C4.06553 8.57006 4.1133 9.09553 4.46686 9.39016L9.46686 13.5568C9.7759 13.8144 10.2248 13.8144 10.5338 13.5568L15.5338 9.39016C15.8874 9.09553 15.9352 8.57006 15.6405 8.21649C15.3459 7.86293 14.8204 7.81516 14.4669 8.1098L10.0003 11.8319Z' fill='%23AEAEC0'/%3E%3C/svg%3E%0A") + '''; +} \ No newline at end of file diff --git a/lib/utils/javascript_utils.dart b/lib/utils/javascript_utils.dart index 888cc1e4..477e8a86 100644 --- a/lib/utils/javascript_utils.dart +++ b/lib/utils/javascript_utils.dart @@ -1,41 +1,127 @@ +import 'package:html_editor_enhanced/utils/icon_utils.dart'; + class JavascriptUtils { static const String jsHandleInsertSignature = ''' - const nodeSignature = document.getElementsByClassName('tmail-signature'); - if (nodeSignature.length <= 0) { - const nodeEditor = document.getElementsByClassName('note-editable')[0]; - - const divSignature = document.createElement('div'); - divSignature.setAttribute('class', 'tmail-signature'); - divSignature.innerHTML = data['signature']; - - const listHeaderQuotedMessage = nodeEditor.querySelectorAll('cite'); - const listQuotedMessage = nodeEditor.querySelectorAll('blockquote'); - - if (listHeaderQuotedMessage.length > 0) { - nodeEditor.insertBefore(divSignature, listHeaderQuotedMessage[0]); - } else if (listQuotedMessage.length > 0) { - nodeEditor.insertBefore(divSignature, listQuotedMessage[0]); + const signatureNode = document.querySelector('.note-editable > .tmail-signature'); + if (signatureNode) { + const currentSignatureContent = document.querySelector('.note-editable > .tmail-signature > .tmail-signature-content'); + const currentSignatureButton = document.querySelector('.note-editable > .tmail-signature > .tmail-signature-button'); + + if (currentSignatureContent && currentSignatureButton) { + currentSignatureContent.innerHTML = data['signature']; + currentSignatureButton.contentEditable = "false"; + currentSignatureButton.setAttribute('onclick', 'handleOnClickSignature()'); + if (currentSignatureContent.style.display === 'none') { + currentSignatureButton.style.backgroundImage = `${IconUtils.chevronDownSVGIconUrlEncoded}`; + } else { + currentSignatureButton.style.backgroundImage = `${IconUtils.chevronUpSVGIconUrlEncoded}`; + } } else { - nodeEditor.appendChild(divSignature); + const signatureContainer = document.createElement('div'); + signatureContainer.setAttribute('class', 'tmail-signature'); + + const signatureContent = document.createElement('div'); + signatureContent.setAttribute('class', 'tmail-signature-content'); + signatureContent.innerHTML = data['signature']; + signatureContent.style.display = 'none'; + + const signatureButton = document.createElement('button'); + signatureButton.setAttribute('class', 'tmail-signature-button'); + signatureButton.textContent = 'Signature'; + signatureButton.contentEditable = "false"; + signatureButton.style.backgroundImage = `${IconUtils.chevronDownSVGIconUrlEncoded}`; + signatureButton.setAttribute('onclick', 'handleOnClickSignature()'); + + signatureContainer.appendChild(signatureButton); + signatureContainer.appendChild(signatureContent); + + if (signatureNode.outerHTML) { + signatureNode.outerHTML = signatureContainer.outerHTML; + } else { + signatureNode.parentNode.replaceChild(signatureContainer, signatureNode); + } } } else { - nodeSignature[0].innerHTML = data['signature']; + const signatureContainer = document.createElement('div'); + signatureContainer.setAttribute('class', 'tmail-signature'); + + const signatureContent = document.createElement('div'); + signatureContent.setAttribute('class', 'tmail-signature-content'); + signatureContent.innerHTML = data['signature']; + signatureContent.style.display = 'none'; + + const signatureButton = document.createElement('button'); + signatureButton.setAttribute('class', 'tmail-signature-button'); + signatureButton.textContent = 'Signature'; + signatureButton.contentEditable = "false"; + signatureButton.style.backgroundImage = `${IconUtils.chevronDownSVGIconUrlEncoded}`; + signatureButton.setAttribute('onclick', 'handleOnClickSignature()'); + + signatureContainer.appendChild(signatureButton); + signatureContainer.appendChild(signatureContent); + + const nodeEditor = document.querySelector('.note-editable'); + if (nodeEditor) { + const headerQuotedMessage = document.querySelector('.note-editable > cite'); + const quotedMessage = document.querySelector('.note-editable > blockquote'); + + if (headerQuotedMessage) { + nodeEditor.insertBefore(signatureContainer, headerQuotedMessage); + } else if (quotedMessage) { + nodeEditor.insertBefore(signatureContainer, quotedMessage); + } else { + nodeEditor.appendChild(signatureContainer); + } + } } '''; static const String jsHandleRemoveSignature = ''' - const nodeSignature = document.getElementsByClassName('tmail-signature'); - if (nodeSignature.length > 0) { - nodeSignature[0].remove(); + const nodeSignature = document.querySelector('.note-editable > .tmail-signature'); + if (nodeSignature) { + nodeSignature.remove(); + } + '''; + + static const String jsHandleReplaceSignatureContent = ''' + const nodeSignature = document.querySelector('.note-editable > .tmail-signature'); + const signatureContent = document.querySelector('.note-editable > .tmail-signature > .tmail-signature-content'); + if (nodeSignature && signatureContent) { + signatureContent.className = 'tmail-signature'; + signatureContent.style.display = 'block'; + + if (nodeSignature.outerHTML) { + nodeSignature.outerHTML = signatureContent.outerHTML; + } else { + nodeSignature.parentNode.replaceChild(signatureContent, nodeSignature); + } } '''; static const String jsHandleUpdateBodyDirection = ''' - const nodeEditor = document.getElementsByClassName('note-editable')[0]; - const currentDirection = data['direction']; - nodeEditor.style.direction = currentDirection.toString(); + const nodeEditor = document.querySelector('.note-editable'); + if (nodeEditor) { + const currentDirection = data['direction']; + nodeEditor.style.direction = currentDirection.toString(); + } + '''; + + static const String jsHandleOnClickSignature = ''' + function handleOnClickSignature() { + const contentElement = document.querySelector('.note-editable > .tmail-signature > .tmail-signature-content'); + const buttonElement = document.querySelector('.note-editable > .tmail-signature > .tmail-signature-button'); + if (contentElement && buttonElement) { + if (contentElement.style.display === 'block') { + contentElement.style.display = 'none'; + buttonElement.style.backgroundImage = `${IconUtils.chevronDownSVGIconUrlEncoded}`; + } else { + contentElement.style.display = 'block'; + buttonElement.style.backgroundImage = `${IconUtils.chevronUpSVGIconUrlEncoded}`; + } + } + } '''; static const String jsDetectBrowser = '''