diff --git a/CHANGELOG.md b/CHANGELOG.md index a745d7b9..e97089e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,188 +1,286 @@ +# [7.1.14] +- Add indents change for multiline selection. + +# [7.1.13] +- Add custom recognizer. + +# [7.1.12] +- Add superscript and subscript styles. + +# [7.1.11] +- Add inserting indents for lines of list if text is selected. + +# [7.1.10] +- Image embedding tweaks + - Add MediaButton which is intened to superseed the ImageButton and VideoButton. Only image selection is working. + - Implement image insert for web (image as base64) + +# [7.1.9] + +- Editor tweaks PR from [bambinoua](https://github.com/bambinoua). + - Shortcuts now working in Mac OS + - QuillDialogTheme is extended with new properties buttonStyle, linkDialogConstraints, imageDialogConstraints, isWrappable, runSpacing, + - Added LinkStyleButton2 with new LinkStyleDialog (similar to Quill implementation + - Conditinally use Row or Wrap for dialog's children. + - Update minimum Dart SDK version to 2.17.0 to use enum extensions. + - Use merging shortcuts and actions correclty (if the key combination is the same) + # [7.1.8] -* Dropdown tweaks. +- Dropdown tweaks + - Add itemHeight, itemPadding, defaultItemColor for customization of dropdown items. + - Remove alignment property as useless. + - Fix bugs with max width when width property is null. # [7.1.7] -* Toolbar tweaks. + +- Toolbar tweaks. + - Implement tooltips for embed CameraButton, VideoButton, FormulaButton, ImageButton. + - Extends customization for SelectAlignmentButton, QuillFontFamilyButton, QuillFontSizeButton adding padding, text style, alignment, width. + - Add renderFontFamilies to QuillFontFamilyButton to show font faces in dropdown. + - Add AxisDivider and its named constructors for for use in parent project. + - Export ToolbarButtons enum to allow specify tooltips for SelectAlignmentButton. + - Export QuillFontFamilyButton, SearchButton as they were not exported before. + - Deprecate items property in QuillFontFamilyButton, QuillFontSizeButton as the it can be built usinr rawItemsMap. + - Make onSelection QuillFontFamilyButton, QuillFontSizeButton omittable as no need to execute callback outside if controller is passed to widget. + +Now the package is more friendly for web projects. # [7.1.6] -* Add enableUnfocusOnTapOutside field to RawEditor and Editor widgets. + +- Add enableUnfocusOnTapOutside field to RawEditor and Editor widgets. # [7.1.5] -* Add tooltips for toolbar buttons. + +- Add tooltips for toolbar buttons. # [7.1.4] -* Fix inserting tab character in lists. + +- Fix inserting tab character in lists. # [7.1.3] -* Fix ios cursor bug when word.length==1. + +- Fix ios cursor bug when word.length==1. # [7.1.2] -* Fix non scrollable editor exception, when tapped under content. + +- Fix non scrollable editor exception, when tapped under content. # [7.1.1] -* customLinkPrefixes parameter - makes possible to open links with custom protoco. + +- customLinkPrefixes parameter - makes possible to open links with custom protoco. # [7.1.0] -* Fix ordered list numeration with several lists in document. + +- Fix ordered list numeration with several lists in document. # [7.0.9] -* Use const constructor for EmbedBuilder. + +- Use const constructor for EmbedBuilder. # [7.0.8] -* Fix IME position bug with scroller. + +- Fix IME position bug with scroller. # [7.0.7] -* Add TextFieldTapRegion for contextMenu. + +- Add TextFieldTapRegion for contextMenu. # [7.0.6] -* Fix line style loss on new line from non string. + +- Fix line style loss on new line from non string. # [7.0.5] -* Fix IME position bug for Mac and Windows. -* Unfocus when tap outside editor. fix the bug that cant refocus in afterButtonPressed after click ToggleStyleButton on Mac. + +- Fix IME position bug for Mac and Windows. +- Unfocus when tap outside editor. fix the bug that cant refocus in afterButtonPressed after click ToggleStyleButton on Mac. # [7.0.4] -* Have text selection span full line height for uneven sized text. + +- Have text selection span full line height for uneven sized text. # [7.0.3] -* Fix ordered list numeration for lists with more than one level of list. + +- Fix ordered list numeration for lists with more than one level of list. # [7.0.2] -* Allow widgets to override widget span properties. + +- Allow widgets to override widget span properties. # [7.0.1] -* Update i18n_extension dependency to version 8.0.0. + +- Update i18n_extension dependency to version 8.0.0. # [7.0.0] -* Breaking change: Tuples are no longer used. They have been replaced with a number of data classes. + +- Breaking change: Tuples are no longer used. They have been replaced with a number of data classes. # [6.4.4] -* Increased compatibility with Flutter widget tests. + +- Increased compatibility with Flutter widget tests. # [6.4.3] -* Update dependencies (collection: 1.17.0, flutter_keyboard_visibility: 5.4.0, quiver: 3.2.1, tuple: 2.0.1, url_launcher: 6.1.9, characters: 1.2.1, i18n_extension: 7.0.0, device_info_plus: 8.1.0) + +- Update dependencies (collection: 1.17.0, flutter_keyboard_visibility: 5.4.0, quiver: 3.2.1, tuple: 2.0.1, url_launcher: 6.1.9, characters: 1.2.1, i18n_extension: 7.0.0, device_info_plus: 8.1.0) # [6.4.2] -* Replace `buildToolbar` with `contextMenuBuilder`. + +- Replace `buildToolbar` with `contextMenuBuilder`. # [6.4.1] -* Control the detect word boundary behaviour. + +- Control the detect word boundary behaviour. # [6.4.0] -* Use `axis` to make the toolbar vertical. -* Use `toolbarIconCrossAlignment` to align the toolbar icons on the cross axis. -* Breaking change: `QuillToolbar`'s parameter `toolbarHeight` was renamed to `toolbarSize`. + +- Use `axis` to make the toolbar vertical. +- Use `toolbarIconCrossAlignment` to align the toolbar icons on the cross axis. +- Breaking change: `QuillToolbar`'s parameter `toolbarHeight` was renamed to `toolbarSize`. # [6.3.5] -* Ability to add custom shortcuts. + +- Ability to add custom shortcuts. # [6.3.4] -* Update clipboard status prior to showing selected text overlay. + +- Update clipboard status prior to showing selected text overlay. # [6.3.3] -* Fixed handling of mac intents. + +- Fixed handling of mac intents. # [6.3.2] -* Added `unknownEmbedBuilder` to QuillEditor. -* Fix error style when input chinese japanese or korean. + +- Added `unknownEmbedBuilder` to QuillEditor. +- Fix error style when input chinese japanese or korean. # [6.3.1] -* Add color property to the basic factory function. + +- Add color property to the basic factory function. # [6.3.0] -* Support Flutter 3.7. + +- Support Flutter 3.7. # [6.2.2] -* Fix: nextLine getter null where no assertion. + +- Fix: nextLine getter null where no assertion. # [6.2.1] -* Revert "Align numerical and bullet lists along with text content". + +- Revert "Align numerical and bullet lists along with text content". # [6.2.0] -* Align numerical and bullet lists along with text content. + +- Align numerical and bullet lists along with text content. # [6.1.12] -* Apply i18n for default font dropdown option labels corresponding to 'Clear'. + +- Apply i18n for default font dropdown option labels corresponding to 'Clear'. # [6.1.11] -* Remove iOS hack for delaying focus calculation. + +- Remove iOS hack for delaying focus calculation. # [6.1.10] -* Delay focus calculation for iOS. + +- Delay focus calculation for iOS. # [6.1.9] -* Bump keyboard show up wait to 1 sec. + +- Bump keyboard show up wait to 1 sec. # [6.1.8] -* Recalculate focus when showing keyboard. + +- Recalculate focus when showing keyboard. # [6.1.7] -* Add czech localizations. + +- Add czech localizations. # [6.1.6] -* Upgrade i18n_extension to 6.0.0. + +- Upgrade i18n_extension to 6.0.0. # [6.1.5] -* Fix formatting exception. + +- Fix formatting exception. # [6.1.4] -* Add double quotes validation. + +- Add double quotes validation. # [6.1.3] -* Revert "fix order list numbering (#988)". + +- Revert "fix order list numbering (#988)". # [6.1.2] -* Add typing shortcuts. + +- Add typing shortcuts. # [6.1.1] -* Fix order list numbering. + +- Fix order list numbering. # [6.1.0] -* Add keyboard shortcuts for editor actions. + +- Add keyboard shortcuts for editor actions. # [6.0.10] -* Upgrade device info plus to ^7.0.0. + +- Upgrade device info plus to ^7.0.0. # [6.0.9] -* Don't throw showAutocorrectionPromptRect not implemented. The function is called with every keystroke as a user is typing. + +- Don't throw showAutocorrectionPromptRect not implemented. The function is called with every keystroke as a user is typing. # [6.0.8+1] -* Fixes null pointer when setting documents. + +- Fixes null pointer when setting documents. # [6.0.8] -* Make QuillController.document mutable. + +- Make QuillController.document mutable. # [6.0.7] -* Allow disabling of selection toolbar. + +- Allow disabling of selection toolbar. # [6.0.6+1] -* Revert 6.0.6. + +- Revert 6.0.6. # [6.0.6] -* Fix wrong custom embed key. + +- Fix wrong custom embed key. # [6.0.5] -* Fixes toolbar buttons stealing focus from editor. + +- Fixes toolbar buttons stealing focus from editor. # [6.0.4] -* Bug fix for Type 'Uint8List' not found. + +- Bug fix for Type 'Uint8List' not found. # [6.0.3] -* Add ability to paste images. + +- Add ability to paste images. # [6.0.2] -* Address Dart Analysis issues. + +- Address Dart Analysis issues. # [6.0.1] -* Changed translation country code (zh_HK -> zh_hk) to lower case, which is required for i18n_extension used in flutter_quill. -* Add localization in example's main to demonstrate translation. -* Issue [Windows] selection's copy / paste tool bar not shown #861: add selection's copy / paste toolbar, escape to hide toolbar, mouse right click to show toolbar, ctrl-Y / ctrl-Z to undo / redo. -* Image and video displayed in Windows platform caused screen flickering while selecting text, a sample_data_nomedia.json asset is added for Desktop to demonstrate the added features. -* Known issue: keyboard action sometimes causes exception mentioned in Flutter's issue #106475 ([Windows] Keyboard shortcuts stop working after modifier key repeat flutter/flutter#106475). -* Know issue: user needs to click the editor to get focus before toolbar is able to display. + +- Changed translation country code (zh_HK -> zh_hk) to lower case, which is required for i18n_extension used in flutter_quill. +- Add localization in example's main to demonstrate translation. +- Issue [Windows] selection's copy / paste tool bar not shown #861: add selection's copy / paste toolbar, escape to hide toolbar, mouse right click to show toolbar, ctrl-Y / ctrl-Z to undo / redo. +- Image and video displayed in Windows platform caused screen flickering while selecting text, a sample_data_nomedia.json asset is added for Desktop to demonstrate the added features. +- Known issue: keyboard action sometimes causes exception mentioned in Flutter's issue #106475 ([Windows] Keyboard shortcuts stop working after modifier key repeat flutter/flutter#106475). +- Know issue: user needs to click the editor to get focus before toolbar is able to display. # [6.0.0] BREAKING CHANGE -* Removed embed (image, video & formula) blocks from the package to reduce app size. + +- Removed embed (image, video & formula) blocks from the package to reduce app size. These blocks have been moved to the package `flutter_quill_extensions`, migrate by filling the `embedBuilders` and `embedButtons` parameters as follows: @@ -200,699 +298,929 @@ QuillToolbar.basic( ); ``` - # [5.4.2] -* Upgrade i18n_extension. + +- Upgrade i18n_extension. # [5.4.1] -* Update German Translation. + +- Update German Translation. # [5.4.0] -* Added Formula Button (for maths support). + +- Added Formula Button (for maths support). # [5.3.2] -* Add more font family. + +- Add more font family. # [5.3.1] -* Enable search when text is not empty. + +- Enable search when text is not empty. # [5.3.0] -* Added search function. + +- Added search function. # [5.2.11] -* Remove default small color. + +- Remove default small color. # [5.2.10] -* Don't wrap the QuillEditor's child in the EditorTextSelectionGestureDetector if selection is disabled. + +- Don't wrap the QuillEditor's child in the EditorTextSelectionGestureDetector if selection is disabled. # [5.2.9] -* Added option to modify SelectHeaderStyleButton options. -* Added option to click again on h1, h2, h3 button to go back to normal. + +- Added option to modify SelectHeaderStyleButton options. +- Added option to click again on h1, h2, h3 button to go back to normal. # [5.2.8] -* Remove tooltip for LinkStyleButton. -* Make link match regex case insensitive. + +- Remove tooltip for LinkStyleButton. +- Make link match regex case insensitive. # [5.2.7] -* Add locale to QuillEditor.basic. + +- Add locale to QuillEditor.basic. # [5.2.6] -* Fix keyboard pops up when resizing the image. + +- Fix keyboard pops up when resizing the image. # [5.2.5] -* Upgrade youtube_player_flutter_quill to 8.2.2. + +- Upgrade youtube_player_flutter_quill to 8.2.2. # [5.2.4] -* Upgrade youtube_player_flutter_quill to 8.2.1. + +- Upgrade youtube_player_flutter_quill to 8.2.1. # [5.2.3] -* Flutter Quill Doesn't Work On iOS 16 or Xcode 14 Betas (Stored properties cannot be marked potentially unavailable with '@available'). + +- Flutter Quill Doesn't Work On iOS 16 or Xcode 14 Betas (Stored properties cannot be marked potentially unavailable with '@available'). # [5.2.2] -* Fix Web Unsupported operation: Platform._operatingSystem error. + +- Fix Web Unsupported operation: Platform.\_operatingSystem error. # [5.2.1] -* Rename QuillCustomIcon to QuillCustomButton. + +- Rename QuillCustomIcon to QuillCustomButton. # [5.2.0] -* Support font family selection. + +- Support font family selection. # [5.1.1] -* Update README. + +- Update README. # [5.1.0] -* Added CustomBlockEmbed and customElementsEmbedBuilder. + +- Added CustomBlockEmbed and customElementsEmbedBuilder. # [5.0.5] -* Upgrade device_info_plus to 4.0.0. + +- Upgrade device_info_plus to 4.0.0. # [5.0.4] -* Added onVideoInit callback for video documents. + +- Added onVideoInit callback for video documents. # [5.0.3] -* Update dependencies. + +- Update dependencies. # [5.0.2] -* Keep cursor position on checkbox tap. + +- Keep cursor position on checkbox tap. # [5.0.1] -* Fix static analysis errors. + +- Fix static analysis errors. # [5.0.0] -* Flutter 3.0.0 support. + +- Flutter 3.0.0 support. # [4.2.3] -* Ignore color:inherit and convert double to int for level. + +- Ignore color:inherit and convert double to int for level. # [4.2.2] -* Add clear option to font size dropdown. + +- Add clear option to font size dropdown. # [4.2.1] -* Refactor font size dropdown. + +- Refactor font size dropdown. # [4.2.0] -* Ensure selectionOverlay is available for showToolbar. + +- Ensure selectionOverlay is available for showToolbar. # [4.1.9] -* Using properly iconTheme colors. + +- Using properly iconTheme colors. # [4.1.8] -* Update font size dropdown. + +- Update font size dropdown. # [4.1.7] -* Convert FontSize to a Map to allow for named Font Size. + +- Convert FontSize to a Map to allow for named Font Size. # [4.1.6] -* Update quill_dropdown_button.dart. + +- Update quill_dropdown_button.dart. # [4.1.5] -* Add Font Size dropdown to the toolbar. + +- Add Font Size dropdown to the toolbar. # [4.1.4] -* New borderRadius for iconTheme. + +- New borderRadius for iconTheme. # [4.1.3] -* Fix selection handles show/hide after paste, backspace, copy. + +- Fix selection handles show/hide after paste, backspace, copy. # [4.1.2] -* Add full support for hardware keyboards (Chromebook, Android tablets, etc) that don't alter screen UI. + +- Add full support for hardware keyboards (Chromebook, Android tablets, etc) that don't alter screen UI. # [4.1.1] -* Added textSelectionControls field in QuillEditor. + +- Added textSelectionControls field in QuillEditor. # [4.1.0] -* Added Node to linkActionPickerDelegate. + +- Added Node to linkActionPickerDelegate. # [4.0.12] -* Add Persian(fa) language. + +- Add Persian(fa) language. # [4.0.11] -* Fix cut selection error in multi-node line. + +- Fix cut selection error in multi-node line. # [4.0.10] -* Fix vertical caret position bug. + +- Fix vertical caret position bug. # [4.0.9] -* Request keyboard focus when no child is found. + +- Request keyboard focus when no child is found. # [4.0.8] -* Fix blank lines do not display when --web-renderer=html. + +- Fix blank lines do not display when --web-renderer=html. # [4.0.7] -* Refactor getPlainText (better handling of blank lines and lines with multiple markups. + +- Refactor getPlainText (better handling of blank lines and lines with multiple markups. # [4.0.6] -* Bug fix for copying text with new lines. + +- Bug fix for copying text with new lines. # [4.0.5] -* Fixed casting null to Tuple2 when link dialog is dismissed without any input (e.g. barrier dismissed). + +- Fixed casting null to Tuple2 when link dialog is dismissed without any input (e.g. barrier dismissed). # [4.0.4] -* Bug fix for text direction rtl. + +- Bug fix for text direction rtl. # [4.0.3] -* Support text direction rtl. + +- Support text direction rtl. # [4.0.2] -* Clear toggled style on selection change. + +- Clear toggled style on selection change. # [4.0.1] -* Fix copy/cut/paste/selectAll not working. + +- Fix copy/cut/paste/selectAll not working. # [4.0.0] -* Upgrade for Flutter 2.10. + +- Upgrade for Flutter 2.10. # [3.9.11] -* Added Indonesian translation. + +- Added Indonesian translation. # [3.9.10] -* Fix for undoing a modification ending with an indented line. + +- Fix for undoing a modification ending with an indented line. # [3.9.9] -* iOS: Save image whose filename does not end with image file extension. + +- iOS: Save image whose filename does not end with image file extension. # [3.9.8] -* Added Urdu translation. + +- Added Urdu translation. # [3.9.7] -* Fix for clicking on the Link button without any text on a new line crashes. + +- Fix for clicking on the Link button without any text on a new line crashes. # [3.9.6] -* Apply locale to QuillEditor(contents). + +- Apply locale to QuillEditor(contents). # [3.9.5] -* Fix image pasting. + +- Fix image pasting. # [3.9.4] -* Hiding dialog after selecting action for image. + +- Hiding dialog after selecting action for image. # [3.9.3] -* Update ImageResizer for Android. + +- Update ImageResizer for Android. # [3.9.2] -* Copy image with its style. + +- Copy image with its style. # [3.9.1] -* Support resizing image. + +- Support resizing image. # [3.9.0] -* Image menu options for copy/remove. + +- Image menu options for copy/remove. # [3.8.8] -* Update set textEditingValue. + +- Update set textEditingValue. # [3.8.7] -* Fix checkbox not toggled correctly in toolbar button. + +- Fix checkbox not toggled correctly in toolbar button. # [3.8.6] -* Fix cursor position changes when checking/unchecking the checkbox. + +- Fix cursor position changes when checking/unchecking the checkbox. # [3.8.5] -* Fix _handleDragUpdate in _TextSelectionHandleOverlayState. + +- Fix \_handleDragUpdate in \_TextSelectionHandleOverlayState. # [3.8.4] -* Fix link dialog layout. + +- Fix link dialog layout. # [3.8.3] -* Fix for errors on a non scrollable editor. + +- Fix for errors on a non scrollable editor. # [3.8.2] -* Fix certain keys not working on web when editor is a child of a scroll view. + +- Fix certain keys not working on web when editor is a child of a scroll view. # [3.8.1] -* Refactor _QuillEditorState to QuillEditorState. + +- Refactor \_QuillEditorState to QuillEditorState. # [3.8.0] -* Support pasting with format. + +- Support pasting with format. # [3.7.3] -* Fix selection overlay for collapsed selection. + +- Fix selection overlay for collapsed selection. # [3.7.2] -* Reverted Embed toPlainText change. + +- Reverted Embed toPlainText change. # [3.7.1] -* Change Embed toPlainText to be empty string. + +- Change Embed toPlainText to be empty string. # [3.7.0] -* Replace Toolbar showHistory group with individual showRedo and showUndo. + +- Replace Toolbar showHistory group with individual showRedo and showUndo. # [3.6.5] -* Update Link dialogue for image/video. + +- Update Link dialogue for image/video. # [3.6.4] -* Link dialogue TextInputType.multiline. + +- Link dialogue TextInputType.multiline. # [3.6.3] -* Bug fix for link button text selection. + +- Bug fix for link button text selection. # [3.6.2] -* Improve link button. + +- Improve link button. # [3.6.1] -* Remove SnackBar 'What is entered is not a link'. + +- Remove SnackBar 'What is entered is not a link'. # [3.6.0] -* Allow link button to enter text. + +- Allow link button to enter text. # [3.5.3] -* Change link button behavior. + +- Change link button behavior. # [3.5.2] -* Bug fix for embed. + +- Bug fix for embed. # [3.5.1] -* Bug fix for platform util. + +- Bug fix for platform util. # [3.5.0] -* Removed redundant classes. + +- Removed redundant classes. # [3.4.4] -* Add more translations. + +- Add more translations. # [3.4.3] -* Preset link from attributes. + +- Preset link from attributes. # [3.4.2] -* Fix launch link edit mode. + +- Fix launch link edit mode. # [3.4.1] -* Placeholder effective in scrollable. + +- Placeholder effective in scrollable. # [3.4.0] -* Option to save image in read-only mode. + +- Option to save image in read-only mode. # [3.3.1] -* Pass any specified key in QuillEditor constructor to super. + +- Pass any specified key in QuillEditor constructor to super. # [3.3.0] -* Fixed Style toggle issue. + +- Fixed Style toggle issue. # [3.2.1] -* Added new translations. + +- Added new translations. # [3.2.0] -* Support multiple links insertion on the go. + +- Support multiple links insertion on the go. # [3.1.1] -* Add selection completed callback. + +- Add selection completed callback. # [3.1.0] -* Fixed image ontap functionality. + +- Fixed image ontap functionality. # [3.0.4] -* Add maxContentWidth constraint to editor. + +- Add maxContentWidth constraint to editor. # [3.0.3] -* Do not show caret on screen when the editor is not focused. + +- Do not show caret on screen when the editor is not focused. # [3.0.2] -* Fix launch link for read-only mode. + +- Fix launch link for read-only mode. # [3.0.1] -* Handle null value of Attribute.link. + +- Handle null value of Attribute.link. # [3.0.0] -* Launch link improvements. -* Removed QuillSimpleViewer. + +- Launch link improvements. +- Removed QuillSimpleViewer. # [2.5.2] -* Skip image when pasting. + +- Skip image when pasting. # [2.5.1] -* Bug fix for Desktop `Shift` + `Click` support. + +- Bug fix for Desktop `Shift` + `Click` support. # [2.5.0] -* Update checkbox list. + +- Update checkbox list. # [2.4.1] -* Desktop selection improvements. + +- Desktop selection improvements. # [2.4.0] -* Improve inline code style. + +- Improve inline code style. # [2.3.3] -* Improves selection rects to have consistent height regardless of individual segment text styles. + +- Improves selection rects to have consistent height regardless of individual segment text styles. # [2.3.2] -* Allow disabling floating cursor. + +- Allow disabling floating cursor. # [2.3.1] -* Preserve last newline character on delete. + +- Preserve last newline character on delete. # [2.3.0] -* Massive changes to support flutter 2.8. + +- Massive changes to support flutter 2.8. # [2.2.2] -* iOS - floating cursor. + +- iOS - floating cursor. # [2.2.1] -* Bug fix for imports supporting flutter 2.8. + +- Bug fix for imports supporting flutter 2.8. # [2.2.0] -* Support flutter 2.8. + +- Support flutter 2.8. # [2.1.1] -* Add methods of clearing editor and moving cursor. + +- Add methods of clearing editor and moving cursor. # [2.1.0] -* Add delete handler. + +- Add delete handler. # [2.0.23] -* Support custom replaceText handler. + +- Support custom replaceText handler. # [2.0.22] -* Fix attribute compare and fix font size parsing. + +- Fix attribute compare and fix font size parsing. # [2.0.21] -* Handle click on embed object. + +- Handle click on embed object. # [2.0.20] -* Improved UX/UI of Image widget. + +- Improved UX/UI of Image widget. # [2.0.19] -* When uploading a video, applying indicator. + +- When uploading a video, applying indicator. # [2.0.18] -* Make toolbar dividers optional. + +- Make toolbar dividers optional. # [2.0.17] -* Allow alignment of the toolbar icons to match WrapAlignment. + +- Allow alignment of the toolbar icons to match WrapAlignment. # [2.0.16] -* Add hide / show alignment buttons. + +- Add hide / show alignment buttons. # [2.0.15] -* Implement change cursor to SystemMouseCursors.click when hovering a link styled text. + +- Implement change cursor to SystemMouseCursors.click when hovering a link styled text. # [2.0.14] -* Enable customize the checkbox widget using DefaultListBlockStyle style. + +- Enable customize the checkbox widget using DefaultListBlockStyle style. # [2.0.13] -* Improve the scrolling performance by reducing the repaint areas. + +- Improve the scrolling performance by reducing the repaint areas. # [2.0.12] -* Fix the selection effect can't be seen as the textLine with background color. + +- Fix the selection effect can't be seen as the textLine with background color. # [2.0.11] -* Fix visibility of text selection handlers on scroll. + +- Fix visibility of text selection handlers on scroll. # [2.0.10] -* cursorConnt.color notify the text_line to repaint if it was disposed. + +- cursorConnt.color notify the text_line to repaint if it was disposed. # [2.0.9] -* Improve UX when trying to add a link. + +- Improve UX when trying to add a link. # [2.0.8] -* Adding translations to the toolbar. + +- Adding translations to the toolbar. # [2.0.7] -* Added theming options for toolbar icons and LinkDialog. + +- Added theming options for toolbar icons and LinkDialog. # [2.0.6] -* Avoid runtime error when placed inside TabBarView. + +- Avoid runtime error when placed inside TabBarView. # [2.0.5] -* Support inline code formatting. + +- Support inline code formatting. # [2.0.4] -* Enable history shortcuts for desktop. + +- Enable history shortcuts for desktop. # [2.0.3] -* Fix cursor when line contains image. + +- Fix cursor when line contains image. # [2.0.2] -* Address KeyboardListener class name conflict. + +- Address KeyboardListener class name conflict. # [2.0.1] -* Upgrade flutter_colorpicker to 0.5.0. + +- Upgrade flutter_colorpicker to 0.5.0. # [2.0.0] -* Text Alignment functions + Block Format standards. + +- Text Alignment functions + Block Format standards. # [1.9.6] -* Support putting QuillEditor inside a Scrollable view. + +- Support putting QuillEditor inside a Scrollable view. # [1.9.5] -* Skip image when pasting. + +- Skip image when pasting. # [1.9.4] -* Bug fix for cursor position when tapping at the end of line with image(s). + +- Bug fix for cursor position when tapping at the end of line with image(s). # [1.9.3] -* Bug fix when line only contains one image. + +- Bug fix when line only contains one image. # [1.9.2] -* Support for building custom inline styles. + +- Support for building custom inline styles. # [1.9.1] -* Cursor jumps to the most appropriate offset to display selection. + +- Cursor jumps to the most appropriate offset to display selection. # [1.9.0] -* Support inline image. + +- Support inline image. # [1.8.3] -* Updated quill_delta. + +- Updated quill_delta. # [1.8.2] -* Support mobile image alignment. + +- Support mobile image alignment. # [1.8.1] -* Support mobile custom size image. + +- Support mobile custom size image. # [1.8.0] -* Support entering link for image/video. + +- Support entering link for image/video. # [1.7.3] -* Bumps photo_view version. + +- Bumps photo_view version. # [1.7.2] -* Fix static analysis error. + +- Fix static analysis error. # [1.7.1] -* Support Youtube video. + +- Support Youtube video. # [1.7.0] -* Support video. + +- Support video. # [1.6.4] -* Bug fix for clear format button. + +- Bug fix for clear format button. # [1.6.3] -* Fixed dragging right handle scrolling issue. + +- Fixed dragging right handle scrolling issue. # [1.6.2] -* Fixed the position of the selection status drag handle. + +- Fixed the position of the selection status drag handle. # [1.6.1] -* Upgrade image_picker and flutter_colorpicker. + +- Upgrade image_picker and flutter_colorpicker. # [1.6.0] -* Support Multi Row Toolbar. + +- Support Multi Row Toolbar. # [1.5.0] -* Remove file_picker dependency. + +- Remove file_picker dependency. # [1.4.1] -* Remove filesystem_picker dependency. + +- Remove filesystem_picker dependency. # [1.4.0] -* Remove path_provider dependency. + +- Remove path_provider dependency. # [1.3.4] -* Add option to paintCursorAboveText. + +- Add option to paintCursorAboveText. # [1.3.3] -* Upgrade file_picker version. + +- Upgrade file_picker version. # [1.3.2] -* Fix copy/paste bug. + +- Fix copy/paste bug. # [1.3.1] -* New logo. + +- New logo. # [1.3.0] -* Support flutter 2.2.0. + +- Support flutter 2.2.0. # [1.2.2] -* Checkbox supports tapping. + +- Checkbox supports tapping. # [1.2.1] -* Indented position not holding while editing. + +- Indented position not holding while editing. # [1.2.0] -* Fix image button cancel causes crash. + +- Fix image button cancel causes crash. # [1.1.8] -* Fix height of empty line bug. + +- Fix height of empty line bug. # [1.1.7] -* Fix text selection in read-only mode. + +- Fix text selection in read-only mode. # [1.1.6] -* Remove universal_html dependency. + +- Remove universal_html dependency. # [1.1.5] -* Enable "Select", "Select All" and "Copy" in read-only mode. + +- Enable "Select", "Select All" and "Copy" in read-only mode. # [1.1.4] -* Fix text selection issue. + +- Fix text selection issue. # [1.1.3] -* Update example folder. + +- Update example folder. # [1.1.2] -* Add pedantic. + +- Add pedantic. # [1.1.1] -* Base64 image support. + +- Base64 image support. # [1.1.0] -* Support null safety. + +- Support null safety. # [1.0.9] -* Web support for raw editor and keyboard listener. + +- Web support for raw editor and keyboard listener. # [1.0.8] -* Support token attribute. + +- Support token attribute. # [1.0.7] -* Fix crash on web (dart:io). + +- Fix crash on web (dart:io). # [1.0.6] -* Add desktop support - WINDOWS, MACOS and LINUX. + +- Add desktop support - WINDOWS, MACOS and LINUX. # [1.0.5] -* Bug fix: Can not insert newline when Bold is toggled ON. + +- Bug fix: Can not insert newline when Bold is toggled ON. # [1.0.4] -* Upgrade photo_view to ^0.11.0. + +- Upgrade photo_view to ^0.11.0. # [1.0.3] -* Fix issue that text is not displayed while typing [WEB]. + +- Fix issue that text is not displayed while typing [WEB]. # [1.0.2] -* Update toolbar in sample home page. + +- Update toolbar in sample home page. # [1.0.1] -* Fix static analysis errors. + +- Fix static analysis errors. # [1.0.0] -* Support flutter 2.0. + +- Support flutter 2.0. # [1.0.0-dev.2] -* Improve link handling for tel, mailto and etc. + +- Improve link handling for tel, mailto and etc. # [1.0.0-dev.1] -* Upgrade prerelease SDK & Bump for master. + +- Upgrade prerelease SDK & Bump for master. # [0.3.5] -* Fix for cursor focus issues when keyboard is on. + +- Fix for cursor focus issues when keyboard is on. # [0.3.4] -* Improve link handling for tel, mailto and etc. + +- Improve link handling for tel, mailto and etc. # [0.3.3] -* More fix on cursor focus issue when keyboard is on. + +- More fix on cursor focus issue when keyboard is on. # [0.3.2] -* Fix cursor focus issue when keyboard is on. + +- Fix cursor focus issue when keyboard is on. # [0.3.1] -* cursor focus when keyboard is on. + +- cursor focus when keyboard is on. # [0.3.0] -* Line Height calculated based on font size. + +- Line Height calculated based on font size. # [0.2.12] -* Support placeholder. + +- Support placeholder. # [0.2.11] -* Fix static analysis error. + +- Fix static analysis error. # [0.2.10] -* Update TextInputConfiguration autocorrect to true in stable branch. + +- Update TextInputConfiguration autocorrect to true in stable branch. # [0.2.9] -* Update TextInputConfiguration autocorrect to true. + +- Update TextInputConfiguration autocorrect to true. # [0.2.8] -* Support display local image besides network image in stable branch. + +- Support display local image besides network image in stable branch. # [0.2.7] -* Support display local image besides network image. + +- Support display local image besides network image. # [0.2.6] -* Fix cursor after pasting. + +- Fix cursor after pasting. # [0.2.5] -* Toggle text/background color button in toolbar. + +- Toggle text/background color button in toolbar. # [0.2.4] -* Support the use of custom icon size in toolbar. + +- Support the use of custom icon size in toolbar. # [0.2.3] -* Support custom styles and image on local device storage without uploading. + +- Support custom styles and image on local device storage without uploading. # [0.2.2] -* Update git repo. + +- Update git repo. # [0.2.1] -* Fix static analysis error. + +- Fix static analysis error. # [0.2.0] -* Add checked/unchecked list button in toolbar. + +- Add checked/unchecked list button in toolbar. # [0.1.8] -* Support font and size attributes. + +- Support font and size attributes. # [0.1.7] -* Support checked/unchecked list. + +- Support checked/unchecked list. # [0.1.6] -* Fix getExtentEndpointForSelection. + +- Fix getExtentEndpointForSelection. # [0.1.5] -* Support text alignment. + +- Support text alignment. # [0.1.4] -* Handle url with trailing spaces. + +- Handle url with trailing spaces. # [0.1.3] -* Handle cursor position change when undo/redo. + +- Handle cursor position change when undo/redo. # [0.1.2] -* Handle more text colors. + +- Handle more text colors. # [0.1.1] -* Fix cursor issue when undo. + +- Fix cursor issue when undo. # [0.1.0] -* Fix insert image. + +- Fix insert image. # [0.0.9] -* Handle rgba color. + +- Handle rgba color. # [0.0.8] -* Fix launching url. + +- Fix launching url. # [0.0.7] -* Handle multiple image inserts. + +- Handle multiple image inserts. # [0.0.6] -* More toolbar functionality. + +- More toolbar functionality. # [0.0.5] -* Update example. + +- Update example. # [0.0.4] -* Update example. + +- Update example. # [0.0.3] -* Update home page meta data. + +- Update home page meta data. # [0.0.2] -* Support image upload and launch url in read-only mode. + +- Support image upload and launch url in read-only mode. # [0.0.1] -* Rich text editor based on Quill Delta. + +- Rich text editor based on Quill Delta. diff --git a/flutter_quill_extensions/CHANGELOG.md b/flutter_quill_extensions/CHANGELOG.md index 442fb43d..b741b21a 100644 --- a/flutter_quill_extensions/CHANGELOG.md +++ b/flutter_quill_extensions/CHANGELOG.md @@ -1,3 +1,12 @@ +## 0.3.1 +* Image embedding tweaks + * Add MediaButton which is intened to superseed the ImageButton and VideoButton. Only image selection is working. + * Implement image insert for web (image as base64) + +## 0.3.0 + +* Added support for adding custom tooltips to toolbar buttons + ## 0.2.0 * Allow widgets to override widget span properties [b7951b0](https://github.com/singerdmx/flutter-quill/commit/b7951b02c9086ea42e7aad6d78e6c9b0297562e5) diff --git a/flutter_quill_extensions/lib/embeds/builders.dart b/flutter_quill_extensions/lib/embeds/builders.dart index 4888da9f..cf04a462 100644 --- a/flutter_quill_extensions/lib/embeds/builders.dart +++ b/flutter_quill_extensions/lib/embeds/builders.dart @@ -7,7 +7,10 @@ import 'package:flutter_quill/flutter_quill.dart' hide Text; import 'package:flutter_quill/translations.dart'; import 'package:gallery_saver/gallery_saver.dart'; import 'package:math_keyboard/math_keyboard.dart'; +import 'package:universal_html/html.dart' as html; +import '../shims/dart_ui_fake.dart' + if (dart.library.html) '../shims/dart_ui_real.dart' as ui; import 'utils.dart'; import 'widgets/image.dart'; import 'widgets/image_resizer.dart'; @@ -145,6 +148,41 @@ class ImageEmbedBuilder extends EmbedBuilder { } } +class ImageEmbedBuilderWeb extends EmbedBuilder { + ImageEmbedBuilderWeb({this.constraints}) + : assert(kIsWeb, 'ImageEmbedBuilderWeb is only for web platform'); + + final BoxConstraints? constraints; + + @override + String get key => BlockEmbed.imageType; + + @override + Widget build( + BuildContext context, + QuillController controller, + Embed node, + bool readOnly, + bool inline, + ) { + final imageUrl = node.value.data; + + ui.platformViewRegistry.registerViewFactory(imageUrl, (viewId) { + return html.ImageElement() + ..src = imageUrl + ..style.height = 'auto' + ..style.width = 'auto'; + }); + + return ConstrainedBox( + constraints: constraints ?? BoxConstraints.loose(const Size(200, 200)), + child: HtmlElementView( + viewType: imageUrl, + ), + ); + } +} + class VideoEmbedBuilder extends EmbedBuilder { VideoEmbedBuilder({this.onVideoInit}); diff --git a/flutter_quill_extensions/lib/embeds/embed_types.dart b/flutter_quill_extensions/lib/embeds/embed_types.dart index 814b77b6..6a48f066 100644 --- a/flutter_quill_extensions/lib/embeds/embed_types.dart +++ b/flutter_quill_extensions/lib/embeds/embed_types.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; @@ -18,3 +19,28 @@ enum MediaPickSetting { Camera, Video, } + +typedef MediaFileUrl = String; +typedef MediaFilePicker = Future Function(QuillMediaType mediaType); +typedef MediaPickedCallback = Future Function(QuillFile file); + +enum QuillMediaType { image, video } + +extension QuillMediaTypeX on QuillMediaType { + bool get isImage => this == QuillMediaType.image; + bool get isVideo => this == QuillMediaType.video; +} + +/// Represents a file data which returned by file picker. +class QuillFile { + QuillFile({ + required this.name, + this.path = '', + Uint8List? bytes, + }) : assert(name.isNotEmpty), + bytes = bytes ?? Uint8List(0); + + final String name; + final String path; + final Uint8List bytes; +} diff --git a/flutter_quill_extensions/lib/embeds/toolbar/media_button.dart b/flutter_quill_extensions/lib/embeds/toolbar/media_button.dart new file mode 100644 index 00000000..837ca825 --- /dev/null +++ b/flutter_quill_extensions/lib/embeds/toolbar/media_button.dart @@ -0,0 +1,452 @@ +//import 'dart:io'; +import 'dart:math' as math; +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_quill/extensions.dart'; +import 'package:flutter_quill/flutter_quill.dart' hide Text; +import 'package:flutter_quill/translations.dart'; +import 'package:image_picker/image_picker.dart'; + +import '../embed_types.dart'; + +/// Widget which combines [ImageButton] and [VideButton] widgets. This widget +/// has more customization and uses dialog similar to one which is used +/// on [http://quilljs.com]. +class MediaButton extends StatelessWidget { + const MediaButton({ + required this.controller, + required this.icon, + this.type = QuillMediaType.image, + this.iconSize = kDefaultIconSize, + this.fillColor, + this.mediaFilePicker = _defaultMediaPicker, + this.onMediaPickedCallback, + this.iconTheme, + this.dialogTheme, + this.tooltip, + this.childrenSpacing = 16.0, + this.labelText, + this.hintText, + this.submitButtonText, + this.submitButtonSize, + this.galleryButtonText, + this.linkButtonText, + this.autovalidateMode = AutovalidateMode.disabled, + Key? key, + this.validationMessage, + }) : assert(type == QuillMediaType.image, + 'Video selection is not supported yet'), + super(key: key); + + final QuillController controller; + final IconData icon; + final double iconSize; + final Color? fillColor; + final QuillMediaType type; + final QuillIconTheme? iconTheme; + final QuillDialogTheme? dialogTheme; + final String? tooltip; + final MediaFilePicker mediaFilePicker; + final MediaPickedCallback? onMediaPickedCallback; + + /// The margin between child widgets in the dialog. + final double childrenSpacing; + + /// The text of label in link add mode. + final String? labelText; + + /// The hint text for link [TextField]. + final String? hintText; + + /// The text of the submit button. + final String? submitButtonText; + + /// The size of dialog buttons. + final Size? submitButtonSize; + + /// The text of the gallery button [MediaSourceSelectorDialog]. + final String? galleryButtonText; + + /// The text of the link button [MediaSourceSelectorDialog]. + final String? linkButtonText; + + final AutovalidateMode autovalidateMode; + final String? validationMessage; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final iconColor = iconTheme?.iconUnselectedColor ?? theme.iconTheme.color; + final iconFillColor = + iconTheme?.iconUnselectedFillColor ?? fillColor ?? theme.canvasColor; + + return QuillIconButton( + icon: Icon(icon, size: iconSize, color: iconColor), + tooltip: tooltip, + highlightElevation: 0, + hoverElevation: 0, + size: iconSize * 1.77, + fillColor: iconFillColor, + borderRadius: iconTheme?.borderRadius ?? 2, + onPressed: () => _onPressedHandler(context), + ); + } + + Future _onPressedHandler(BuildContext context) async { + if (onMediaPickedCallback != null) { + final mediaSource = await showDialog( + context: context, + builder: (_) => MediaSourceSelectorDialog( + dialogTheme: dialogTheme, + galleryButtonText: galleryButtonText, + linkButtonText: linkButtonText, + ), + ); + if (mediaSource != null) { + if (mediaSource == MediaPickSetting.Gallery) { + await _pickImage(); + } else { + _inputLink(context); + } + } + } else { + _inputLink(context); + } + } + + Future _pickImage() async { + if (!(kIsWeb || isMobile() || isDesktop())) { + throw UnsupportedError( + 'Unsupported target platform: ${defaultTargetPlatform.name}'); + } + + final mediaFileUrl = await _pickMediaFileUrl(); + + if (mediaFileUrl != null) { + final index = controller.selection.baseOffset; + final length = controller.selection.extentOffset - index; + controller.replaceText( + index, length, BlockEmbed.image(mediaFileUrl), null); + } + } + + Future _pickMediaFileUrl() async { + final mediaFile = await mediaFilePicker(type); + return mediaFile != null ? onMediaPickedCallback?.call(mediaFile) : null; + } + + void _inputLink(BuildContext context) { + showDialog( + context: context, + builder: (_) => MediaLinkDialog( + dialogTheme: dialogTheme, + labelText: labelText, + hintText: hintText, + buttonText: submitButtonText, + buttonSize: submitButtonSize, + childrenSpacing: childrenSpacing, + autovalidateMode: autovalidateMode, + validationMessage: validationMessage, + ), + ).then(_linkSubmitted); + } + + void _linkSubmitted(String? value) { + if (value != null && value.isNotEmpty) { + final index = controller.selection.baseOffset; + final length = controller.selection.extentOffset - index; + final data = + type.isImage ? BlockEmbed.image(value) : BlockEmbed.video(value); + controller.replaceText(index, length, data, null); + } + } +} + +/// Provides a dialog for input link to media resource. +class MediaLinkDialog extends StatefulWidget { + const MediaLinkDialog({ + Key? key, + this.link, + this.dialogTheme, + this.childrenSpacing = 16.0, + this.labelText, + this.hintText, + this.buttonText, + this.buttonSize, + this.autovalidateMode = AutovalidateMode.disabled, + this.validationMessage, + }) : assert(childrenSpacing > 0), + super(key: key); + + final String? link; + final QuillDialogTheme? dialogTheme; + + /// The margin between child widgets in the dialog. + final double childrenSpacing; + + /// The text of label in link add mode. + final String? labelText; + + /// The hint text for link [TextField]. + final String? hintText; + + /// The text of the submit button. + final String? buttonText; + + /// The size of dialog buttons. + final Size? buttonSize; + + final AutovalidateMode autovalidateMode; + final String? validationMessage; + + @override + State createState() => _MediaLinkDialogState(); +} + +class _MediaLinkDialogState extends State { + final _linkFocus = FocusNode(); + final _linkController = TextEditingController(); + + @override + void dispose() { + _linkFocus.dispose(); + _linkController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final constraints = widget.dialogTheme?.linkDialogConstraints ?? + () { + final mediaQuery = MediaQuery.of(context); + final maxWidth = + kIsWeb ? mediaQuery.size.width / 4 : mediaQuery.size.width - 80; + return BoxConstraints(maxWidth: maxWidth, maxHeight: 80); + }(); + + final buttonStyle = widget.buttonSize != null + ? Theme.of(context) + .elevatedButtonTheme + .style + ?.copyWith(fixedSize: MaterialStatePropertyAll(widget.buttonSize)) + : widget.dialogTheme?.buttonStyle; + + final isWrappable = widget.dialogTheme?.isWrappable ?? false; + + final children = [ + Text(widget.labelText ?? 'Enter media'.i18n), + UtilityWidgets.maybeWidget( + enabled: !isWrappable, + wrapper: (child) => Expanded( + child: child, + ), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: widget.childrenSpacing), + child: TextFormField( + controller: _linkController, + focusNode: _linkFocus, + style: widget.dialogTheme?.inputTextStyle, + keyboardType: TextInputType.url, + textInputAction: TextInputAction.done, + decoration: InputDecoration( + labelStyle: widget.dialogTheme?.labelTextStyle, + hintText: widget.hintText, + ), + autofocus: true, + autovalidateMode: widget.autovalidateMode, + validator: _validateLink, + onChanged: _linkChanged, + ), + ), + ), + ElevatedButton( + onPressed: _canPress() ? _submitLink : null, + style: buttonStyle, + child: Text(widget.buttonText ?? 'Ok'.i18n), + ), + ]; + + return Dialog( + backgroundColor: widget.dialogTheme?.dialogBackgroundColor, + shape: widget.dialogTheme?.shape ?? + DialogTheme.of(context).shape ?? + RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + child: ConstrainedBox( + constraints: constraints, + child: Padding( + padding: + widget.dialogTheme?.linkDialogPadding ?? const EdgeInsets.all(16), + child: isWrappable + ? Wrap( + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + runSpacing: widget.dialogTheme?.runSpacing ?? 0.0, + children: children, + ) + : Row( + children: children, + ), + ), + ), + ); + } + + bool _canPress() => _validateLink(_linkController.text) == null; + + void _linkChanged(String value) { + setState(() { + _linkController.text = value; + }); + } + + void _submitLink() => Navigator.pop(context, _linkController.text); + + String? _validateLink(String? value) { + if ((value?.isEmpty ?? false) || + !AutoFormatMultipleLinksRule.linkRegExp.hasMatch(value!)) { + return widget.validationMessage ?? 'That is not a valid URL'; + } + + return null; + } +} + +/// Media souce selector. +class MediaSourceSelectorDialog extends StatelessWidget { + const MediaSourceSelectorDialog({ + Key? key, + this.dialogTheme, + this.galleryButtonText, + this.linkButtonText, + }) : super(key: key); + + final QuillDialogTheme? dialogTheme; + + /// The text of the gallery button [MediaSourceSelectorDialog]. + final String? galleryButtonText; + + /// The text of the link button [MediaSourceSelectorDialog]. + final String? linkButtonText; + + @override + Widget build(BuildContext context) { + final constraints = dialogTheme?.mediaSelectorDialogConstraints ?? + () { + final mediaQuery = MediaQuery.of(context); + double maxWidth, maxHeight; + if (kIsWeb) { + maxWidth = mediaQuery.size.width / 7; + maxHeight = mediaQuery.size.height / 7; + } else { + maxWidth = mediaQuery.size.width - 80; + maxHeight = maxWidth / 2; + } + return BoxConstraints(maxWidth: maxWidth, maxHeight: maxHeight); + }(); + + final shape = dialogTheme?.shape ?? + DialogTheme.of(context).shape ?? + RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)); + + return Dialog( + backgroundColor: dialogTheme?.dialogBackgroundColor, + shape: shape, + child: ConstrainedBox( + constraints: constraints, + child: Padding( + padding: dialogTheme?.mediaSelectorDialogPadding ?? + const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: TextButtonWithIcon( + icon: Icons.collections, + label: galleryButtonText ?? 'Gallery'.i18n, + onPressed: () => + Navigator.pop(context, MediaPickSetting.Gallery), + ), + ), + const SizedBox(width: 10), + Expanded( + child: TextButtonWithIcon( + icon: Icons.link, + label: linkButtonText ?? 'Link'.i18n, + onPressed: () => + Navigator.pop(context, MediaPickSetting.Link), + ), + ) + ], + ), + ), + ), + ); + } +} + +class TextButtonWithIcon extends StatelessWidget { + const TextButtonWithIcon({ + required this.label, + required this.icon, + required this.onPressed, + this.textStyle, + Key? key, + }) : super(key: key); + + final String label; + final IconData icon; + final VoidCallback onPressed; + final TextStyle? textStyle; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scale = MediaQuery.maybeOf(context)?.textScaleFactor ?? 1; + final gap = scale <= 1 ? 8.0 : lerpDouble(8, 4, math.min(scale - 1, 1))!; + final buttonStyle = TextButtonTheme.of(context).style; + final shape = buttonStyle?.shape?.resolve({}) ?? + const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(4))); + return Material( + shape: shape, + textStyle: textStyle ?? + theme.textButtonTheme.style?.textStyle?.resolve({}) ?? + theme.textTheme.labelLarge, + elevation: buttonStyle?.elevation?.resolve({}) ?? 0, + child: InkWell( + customBorder: shape, + onTap: onPressed, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon), + SizedBox(height: gap), + Flexible(child: Text(label)), + ], + ), + ), + ), + ); + } +} + +/// Default file picker. +Future _defaultMediaPicker(QuillMediaType mediaType) async { + final pickedFile = mediaType.isImage + ? await ImagePicker().pickImage(source: ImageSource.gallery) + : await ImagePicker().pickVideo(source: ImageSource.gallery); + + if (pickedFile != null) { + return QuillFile( + name: pickedFile.name, + path: pickedFile.path, + bytes: await pickedFile.readAsBytes(), + ); + } + + return null; +} diff --git a/flutter_quill_extensions/lib/embeds/widgets/image.dart b/flutter_quill_extensions/lib/embeds/widgets/image.dart index d4df2a4c..658c5050 100644 --- a/flutter_quill_extensions/lib/embeds/widgets/image.dart +++ b/flutter_quill_extensions/lib/embeds/widgets/image.dart @@ -21,7 +21,7 @@ String getImageStyleString(QuillController controller) { final String? s = controller .getAllSelectionStyles() .firstWhere((s) => s.attributes.containsKey(Attribute.style.key), - orElse: () => Style()) + orElse: Style.new) .attributes[Attribute.style.key] ?.value; return s ?? ''; diff --git a/flutter_quill_extensions/lib/flutter_quill_extensions.dart b/flutter_quill_extensions/lib/flutter_quill_extensions.dart index 12b3bd82..fdbc54b2 100644 --- a/flutter_quill_extensions/lib/flutter_quill_extensions.dart +++ b/flutter_quill_extensions/lib/flutter_quill_extensions.dart @@ -15,18 +15,24 @@ export 'embeds/toolbar/camera_button.dart'; export 'embeds/toolbar/formula_button.dart'; export 'embeds/toolbar/image_button.dart'; export 'embeds/toolbar/image_video_utils.dart'; +export 'embeds/toolbar/media_button.dart'; export 'embeds/toolbar/video_button.dart'; export 'embeds/utils.dart'; class FlutterQuillEmbeds { - static List builders( - {void Function(GlobalKey videoContainerKey)? onVideoInit}) => + static List builders({ + void Function(GlobalKey videoContainerKey)? onVideoInit, + }) => [ ImageEmbedBuilder(), VideoEmbedBuilder(onVideoInit: onVideoInit), FormulaEmbedBuilder(), ]; + static List webBuilders() => [ + ImageEmbedBuilderWeb(), + ]; + static List buttons({ bool showImageButton = true, bool showVideoButton = true, @@ -43,58 +49,58 @@ class FlutterQuillEmbeds { FilePickImpl? filePickImpl, WebImagePickImpl? webImagePickImpl, WebVideoPickImpl? webVideoPickImpl, - }) { - return [ - if (showImageButton) - (controller, toolbarIconSize, iconTheme, dialogTheme) => ImageButton( - icon: Icons.image, - iconSize: toolbarIconSize, - tooltip: imageButtonTooltip, - controller: controller, - onImagePickCallback: onImagePickCallback, - filePickImpl: filePickImpl, - webImagePickImpl: webImagePickImpl, - mediaPickSettingSelector: mediaPickSettingSelector, - iconTheme: iconTheme, - dialogTheme: dialogTheme, - ), - if (showVideoButton) - (controller, toolbarIconSize, iconTheme, dialogTheme) => VideoButton( - icon: Icons.movie_creation, - iconSize: toolbarIconSize, - tooltip: videoButtonTooltip, - controller: controller, - onVideoPickCallback: onVideoPickCallback, - filePickImpl: filePickImpl, - webVideoPickImpl: webImagePickImpl, - mediaPickSettingSelector: mediaPickSettingSelector, - iconTheme: iconTheme, - dialogTheme: dialogTheme, - ), - if ((onImagePickCallback != null || onVideoPickCallback != null) && - showCameraButton) - (controller, toolbarIconSize, iconTheme, dialogTheme) => CameraButton( - icon: Icons.photo_camera, - iconSize: toolbarIconSize, - tooltip: cameraButtonTooltip, - controller: controller, - onImagePickCallback: onImagePickCallback, - onVideoPickCallback: onVideoPickCallback, - filePickImpl: filePickImpl, - webImagePickImpl: webImagePickImpl, - webVideoPickImpl: webVideoPickImpl, - cameraPickSettingSelector: cameraPickSettingSelector, - iconTheme: iconTheme, - ), - if (showFormulaButton) - (controller, toolbarIconSize, iconTheme, dialogTheme) => FormulaButton( - icon: Icons.functions, - iconSize: toolbarIconSize, - tooltip: formulaButtonTooltip, - controller: controller, - iconTheme: iconTheme, - dialogTheme: dialogTheme, - ) - ]; - } + }) => + [ + if (showImageButton) + (controller, toolbarIconSize, iconTheme, dialogTheme) => ImageButton( + icon: Icons.image, + iconSize: toolbarIconSize, + tooltip: imageButtonTooltip, + controller: controller, + onImagePickCallback: onImagePickCallback, + filePickImpl: filePickImpl, + webImagePickImpl: webImagePickImpl, + mediaPickSettingSelector: mediaPickSettingSelector, + iconTheme: iconTheme, + dialogTheme: dialogTheme, + ), + if (showVideoButton) + (controller, toolbarIconSize, iconTheme, dialogTheme) => VideoButton( + icon: Icons.movie_creation, + iconSize: toolbarIconSize, + tooltip: videoButtonTooltip, + controller: controller, + onVideoPickCallback: onVideoPickCallback, + filePickImpl: filePickImpl, + webVideoPickImpl: webImagePickImpl, + mediaPickSettingSelector: mediaPickSettingSelector, + iconTheme: iconTheme, + dialogTheme: dialogTheme, + ), + if ((onImagePickCallback != null || onVideoPickCallback != null) && + showCameraButton) + (controller, toolbarIconSize, iconTheme, dialogTheme) => CameraButton( + icon: Icons.photo_camera, + iconSize: toolbarIconSize, + tooltip: cameraButtonTooltip, + controller: controller, + onImagePickCallback: onImagePickCallback, + onVideoPickCallback: onVideoPickCallback, + filePickImpl: filePickImpl, + webImagePickImpl: webImagePickImpl, + webVideoPickImpl: webVideoPickImpl, + cameraPickSettingSelector: cameraPickSettingSelector, + iconTheme: iconTheme, + ), + if (showFormulaButton) + (controller, toolbarIconSize, iconTheme, dialogTheme) => + FormulaButton( + icon: Icons.functions, + iconSize: toolbarIconSize, + tooltip: formulaButtonTooltip, + controller: controller, + iconTheme: iconTheme, + dialogTheme: dialogTheme, + ) + ]; } diff --git a/flutter_quill_extensions/lib/shims/dart_ui_fake.dart b/flutter_quill_extensions/lib/shims/dart_ui_fake.dart new file mode 100644 index 00000000..baaf9ebd --- /dev/null +++ b/flutter_quill_extensions/lib/shims/dart_ui_fake.dart @@ -0,0 +1,23 @@ +// ignore_for_file: avoid_classes_with_only_static_members, camel_case_types, lines_longer_than_80_chars + +import 'package:universal_html/html.dart' as html; + +// 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. + +typedef PlatroformViewFactory = html.Element Function(int viewId); + +/// Shim for web_ui engine.PlatformViewRegistry +/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L62 +class platformViewRegistry { + /// Shim for registerViewFactory + /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L72 + static dynamic registerViewFactory( + String viewTypeId, PlatroformViewFactory viewFactory) {} +} + +/// Shim for web_ui engine.AssetManager +/// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L12 +class webOnlyAssetManager { + static dynamic getAssetUrl(String asset) {} +} diff --git a/flutter_quill_extensions/lib/shims/dart_ui_real.dart b/flutter_quill_extensions/lib/shims/dart_ui_real.dart new file mode 100644 index 00000000..69c06ee2 --- /dev/null +++ b/flutter_quill_extensions/lib/shims/dart_ui_real.dart @@ -0,0 +1 @@ +export 'dart:ui'; diff --git a/flutter_quill_extensions/pubspec.yaml b/flutter_quill_extensions/pubspec.yaml index 2bb092ce..12ab6b52 100644 --- a/flutter_quill_extensions/pubspec.yaml +++ b/flutter_quill_extensions/pubspec.yaml @@ -1,18 +1,18 @@ name: flutter_quill_extensions description: Embed extensions for flutter_quill including image, video, formula and etc. -version: 0.2.1 +version: 0.3.1 homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_extensions environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.17.0 <3.0.0" flutter: ">=3.0.0" dependencies: flutter: sdk: flutter - flutter_quill: ^7.1.7 + flutter_quill: ^7.1.12 image_picker: ^0.8.5+3 photo_view: ^0.14.0 @@ -21,6 +21,7 @@ dependencies: gallery_saver: ^2.3.2 math_keyboard: ^0.1.8 string_validator: ^1.0.0 + universal_html: ^2.2.1 url_launcher: ^6.1.9 dev_dependencies: diff --git a/lib/extensions.dart b/lib/extensions.dart index 12e961c2..b7c2b0af 100644 --- a/lib/extensions.dart +++ b/lib/extensions.dart @@ -4,3 +4,4 @@ export 'src/models/documents/nodes/leaf.dart' hide Text; export 'src/models/rules/insert.dart'; export 'src/utils/platform.dart'; export 'src/utils/string.dart'; +export 'src/utils/widgets.dart'; diff --git a/lib/src/models/documents/attribute.dart b/lib/src/models/documents/attribute.dart index 2ec7d7f2..6c48e59e 100644 --- a/lib/src/models/documents/attribute.dart +++ b/lib/src/models/documents/attribute.dart @@ -19,6 +19,8 @@ class Attribute { static final Map _registry = LinkedHashMap.of({ Attribute.bold.key: Attribute.bold, + Attribute.subscript.key: Attribute.subscript, + Attribute.superscript.key: Attribute.superscript, Attribute.italic.key: Attribute.italic, Attribute.small.key: Attribute.small, Attribute.underline.key: Attribute.underline, @@ -42,10 +44,18 @@ class Attribute { Attribute.style.key: Attribute.style, Attribute.token.key: Attribute.token, Attribute.script.key: Attribute.script, + Attribute.image.key: Attribute.image, + Attribute.video.key: Attribute.video, }); static const BoldAttribute bold = BoldAttribute(); + static final ScriptAttribute subscript = + ScriptAttribute(ScriptAttributes.sub); + + static final ScriptAttribute superscript = + ScriptAttribute(ScriptAttributes.sup); + static const ItalicAttribute italic = ItalicAttribute(); static const SmallAttribute small = SmallAttribute(); @@ -90,7 +100,7 @@ class Attribute { static const TokenAttribute token = TokenAttribute(''); - static const ScriptAttribute script = ScriptAttribute(''); + static final ScriptAttribute script = ScriptAttribute(null); static const String mobileWidth = 'mobileWidth'; @@ -100,8 +110,14 @@ class Attribute { static const String mobileAlignment = 'mobileAlignment'; + static const ImageAttribute image = ImageAttribute(null); + + static const VideoAttribute video = VideoAttribute(null); + static final Set inlineKeys = { Attribute.bold.key, + Attribute.subscript.key, + Attribute.superscript.key, Attribute.italic.key, Attribute.small.key, Attribute.underline.key, @@ -138,6 +154,11 @@ class Attribute { Attribute.blockQuote.key, }); + static final Set embedKeys = { + Attribute.image.key, + Attribute.video.key, + }; + static const Attribute h1 = HeaderAttribute(level: 1); static const Attribute h2 = HeaderAttribute(level: 2); @@ -345,8 +366,26 @@ class TokenAttribute extends Attribute { const TokenAttribute(String val) : super('token', AttributeScope.IGNORE, val); } -// `script` is supposed to be inline attribute but it is not supported yet -class ScriptAttribute extends Attribute { - const ScriptAttribute(String val) - : super('script', AttributeScope.IGNORE, val); +class ScriptAttribute extends Attribute { + ScriptAttribute(ScriptAttributes? val) + : super('script', AttributeScope.INLINE, val?.value); +} + +enum ScriptAttributes { + sup('super'), + sub('sup'); + + const ScriptAttributes(this.value); + + final String value; +} + +class ImageAttribute extends Attribute { + const ImageAttribute(String? url) + : super('image', AttributeScope.EMBEDS, url); +} + +class VideoAttribute extends Attribute { + const VideoAttribute(String? url) + : super('video', AttributeScope.EMBEDS, url); } diff --git a/lib/src/models/documents/document.dart b/lib/src/models/documents/document.dart index 1b4d5aef..597d9025 100644 --- a/lib/src/models/documents/document.dart +++ b/lib/src/models/documents/document.dart @@ -170,6 +170,12 @@ class Document { return (res.node as Line).collectAllStyles(res.offset, len); } + /// Returns all styles for any character within the specified text range. + List> collectAllStylesWithOffset(int index, int len) { + final res = queryChild(index); + return (res.node as Line).collectAllStylesWithOffsets(res.offset, len); + } + /// Returns plain text within the specified text range. String getPlainText(int index, int len) { final res = queryChild(index); diff --git a/lib/src/models/documents/nodes/line.dart b/lib/src/models/documents/nodes/line.dart index a8cca75c..42306edb 100644 --- a/lib/src/models/documents/nodes/line.dart +++ b/lib/src/models/documents/nodes/line.dart @@ -458,6 +458,44 @@ class Line extends Container { return result; } + /// Returns all styles for any character within the specified text range. + List> collectAllStylesWithOffsets( + int offset, + int len, { + int beg = 0, + }) { + final local = math.min(length - offset, len); + final result = >[]; + + final data = queryChild(offset, true); + var node = data.node as Leaf?; + if (node != null) { + var pos = 0; + pos = node.length - data.offset; + result.add(OffsetValue(node.documentOffset, node.style, node.length)); + while (!node!.isLast && pos < local) { + node = node.next as Leaf; + result.add(OffsetValue(node.documentOffset, node.style, node.length)); + pos += node.length; + } + } + + result.add(OffsetValue(documentOffset, style, length)); + if (parent is Block) { + final block = parent as Block; + result.add(OffsetValue(block.documentOffset, block.style, block.length)); + } + + final remaining = len - local; + if (remaining > 0 && nextLine != null) { + final rest = + nextLine!.collectAllStylesWithOffsets(0, remaining, beg: local); + result.addAll(rest); + } + + return result; + } + /// Returns plain text within the specified text range. String getPlainText(int offset, int len) { final plainText = StringBuffer(); diff --git a/lib/src/models/structs/offset_value.dart b/lib/src/models/structs/offset_value.dart index 0f9e558b..58275458 100644 --- a/lib/src/models/structs/offset_value.dart +++ b/lib/src/models/structs/offset_value.dart @@ -1,5 +1,6 @@ class OffsetValue { - OffsetValue(this.offset, this.value); + OffsetValue(this.offset, this.value, [this.length]); final int offset; + final int? length; final T value; } diff --git a/lib/src/models/themes/quill_dialog_theme.dart b/lib/src/models/themes/quill_dialog_theme.dart index 795d35d5..1552c5f5 100644 --- a/lib/src/models/themes/quill_dialog_theme.dart +++ b/lib/src/models/themes/quill_dialog_theme.dart @@ -1,8 +1,21 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -class QuillDialogTheme { - QuillDialogTheme( - {this.labelTextStyle, this.inputTextStyle, this.dialogBackgroundColor}); +/// Used to configure the dialog's look and feel. +class QuillDialogTheme with Diagnosticable { + const QuillDialogTheme({ + this.labelTextStyle, + this.inputTextStyle, + this.dialogBackgroundColor, + this.shape, + this.buttonStyle, + this.linkDialogConstraints, + this.linkDialogPadding = const EdgeInsets.all(16), + this.mediaSelectorDialogConstraints, + this.mediaSelectorDialogPadding = const EdgeInsets.all(16), + this.isWrappable = false, + this.runSpacing = 8.0, + }) : assert(runSpacing >= 0); ///The text style to use for the label shown in the link-input dialog final TextStyle? labelTextStyle; @@ -10,6 +23,105 @@ class QuillDialogTheme { ///The text style to use for the input text shown in the link-input dialog final TextStyle? inputTextStyle; - ///The background color for the [LinkDialog()] + ///The background color for the Quill dialog final Color? dialogBackgroundColor; + + /// The shape of this dialog's border. + /// + /// Defines the dialog's [Material.shape]. + /// + /// The default shape is a [RoundedRectangleBorder] with a radius of 4.0 + final ShapeBorder? shape; + + /// Constrains for [LinkStyleDialog]. + final BoxConstraints? linkDialogConstraints; + + /// The padding for content of [LinkStyleDialog]. + final EdgeInsetsGeometry linkDialogPadding; + + /// Constrains for [MediaSourceSelectorDialog]. + final BoxConstraints? mediaSelectorDialogConstraints; + + /// The padding for content of [MediaSourceSelectorDialog]. + final EdgeInsetsGeometry mediaSelectorDialogPadding; + + /// Customizes this button's appearance. + final ButtonStyle? buttonStyle; + + /// Whether dialog's children are wrappred with [Wrap] instead of [Row]. + final bool isWrappable; + + /// How much space to place between the runs themselves in the cross axis. + /// + /// Make sense if [isWrappable] is `true`. + /// + /// Defaults to 0.0. + final double runSpacing; + + QuillDialogTheme copyWith({ + TextStyle? labelTextStyle, + TextStyle? inputTextStyle, + Color? dialogBackgroundColor, + ShapeBorder? shape, + ButtonStyle? buttonStyle, + BoxConstraints? linkDialogConstraints, + EdgeInsetsGeometry? linkDialogPadding, + BoxConstraints? imageDialogConstraints, + EdgeInsetsGeometry? mediaDialogPadding, + bool? isWrappable, + double? runSpacing, + }) { + return QuillDialogTheme( + labelTextStyle: labelTextStyle ?? this.labelTextStyle, + inputTextStyle: inputTextStyle ?? this.inputTextStyle, + dialogBackgroundColor: + dialogBackgroundColor ?? this.dialogBackgroundColor, + shape: shape ?? this.shape, + buttonStyle: buttonStyle ?? this.buttonStyle, + linkDialogConstraints: + linkDialogConstraints ?? this.linkDialogConstraints, + linkDialogPadding: linkDialogPadding ?? this.linkDialogPadding, + mediaSelectorDialogConstraints: + imageDialogConstraints ?? mediaSelectorDialogConstraints, + mediaSelectorDialogPadding: + mediaDialogPadding ?? mediaSelectorDialogPadding, + isWrappable: isWrappable ?? this.isWrappable, + runSpacing: runSpacing ?? this.runSpacing, + ); + } + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is QuillDialogTheme && + other.labelTextStyle == labelTextStyle && + other.inputTextStyle == inputTextStyle && + other.dialogBackgroundColor == dialogBackgroundColor && + other.shape == shape && + other.buttonStyle == buttonStyle && + other.linkDialogConstraints == linkDialogConstraints && + other.linkDialogPadding == linkDialogPadding && + other.mediaSelectorDialogConstraints == + mediaSelectorDialogConstraints && + other.mediaSelectorDialogPadding == mediaSelectorDialogPadding && + other.isWrappable == isWrappable && + other.runSpacing == runSpacing; + } + + @override + int get hashCode => Object.hash( + labelTextStyle, + inputTextStyle, + dialogBackgroundColor, + shape, + buttonStyle, + linkDialogConstraints, + linkDialogPadding, + mediaSelectorDialogConstraints, + mediaSelectorDialogPadding, + isWrappable, + runSpacing, + ); } diff --git a/lib/src/translations/toolbar.i18n.dart b/lib/src/translations/toolbar.i18n.dart index cb6c7853..5eb2b344 100644 --- a/lib/src/translations/toolbar.i18n.dart +++ b/lib/src/translations/toolbar.i18n.dart @@ -40,6 +40,8 @@ extension Localization on String { 'Font family': 'Font family', 'Font size': 'Font size', 'Bold': 'Bold', + 'Subscript': 'Subscript', + 'Superscript': 'Superscript', 'Italic': 'Italic', 'Underline': 'Underline', 'Strike through': 'Strike through', @@ -61,6 +63,11 @@ extension Localization on String { 'Increase indent': 'Increase indent', 'Decrease indent': 'Decrease indent', 'Insert URL': 'Insert URL', + 'Visit link': 'Visit link', + 'Enter link': 'Enter link', + 'Enter media': 'Enter media', + 'Edit': 'Edit', + 'Apply': 'Apply', }, 'en_us': { 'Paste a link': 'Paste a link', @@ -99,6 +106,8 @@ extension Localization on String { 'Font family': 'Font family', 'Font size': 'Font size', 'Bold': 'Bold', + 'Subscript': 'Subscript', + 'Superscript': 'Superscript', 'Italic': 'Italic', 'Underline': 'Underline', 'Strike through': 'Strike through', @@ -120,6 +129,11 @@ extension Localization on String { 'Increase indent': 'Increase indent', 'Decrease indent': 'Decrease indent', 'Insert URL': 'Insert URL', + 'Visit link': 'Visit link', + 'Enter link': 'Enter link', + 'Enter media': 'Enter media', + 'Edit': 'Edit', + 'Apply': 'Apply', }, 'ar': { 'Paste a link': 'نسخ الرابط', @@ -153,6 +167,39 @@ extension Localization on String { 'Next': 'Next', 'Camera': 'Camera', 'Video': 'Video', + 'Undo': 'Undo', + 'Redo': 'Redo', + 'Font family': 'Font family', + 'Font size': 'Font size', + 'Bold': 'Bold', + 'Subscript': 'Subscript', + 'Superscript': 'Superscript', + 'Italic': 'Italic', + 'Underline': 'Underline', + 'Strike through': 'Strike through', + 'Inline code': 'Inline code', + 'Font color': 'Font color', + 'Background color': 'Background color', + 'Clear format': 'Clear format', + 'Align left': 'Align left', + 'Align center': 'Align center', + 'Align right': 'Align right', + 'Justify win width': 'Justify win width', + 'Text direction': 'Text direction', + 'Header style': 'Header style', + 'Numbered list': 'Numbered list', + 'Bullet list': 'Bullet list', + 'Checked list': 'Checked list', + 'Code block': 'Code block', + 'Quote': 'Quote', + 'Increase indent': 'Increase indent', + 'Decrease indent': 'Decrease indent', + 'Insert URL': 'Insert URL', + 'Visit link': 'Visit link', + 'Enter link': 'Enter link', + 'Enter media': 'Enter media', + 'Edit': 'Edit', + 'Apply': 'Apply', }, 'da': { 'Paste a link': 'Indsæt link', @@ -186,6 +233,39 @@ extension Localization on String { 'Next': 'Next', 'Camera': 'Camera', 'Video': 'Video', + 'Undo': 'Undo', + 'Redo': 'Redo', + 'Font family': 'Font family', + 'Font size': 'Font size', + 'Bold': 'Bold', + 'Subscript': 'Subscript', + 'Superscript': 'Superscript', + 'Italic': 'Italic', + 'Underline': 'Underline', + 'Strike through': 'Strike through', + 'Inline code': 'Inline code', + 'Font color': 'Font color', + 'Background color': 'Background color', + 'Clear format': 'Clear format', + 'Align left': 'Align left', + 'Align center': 'Align center', + 'Align right': 'Align right', + 'Justify win width': 'Justify win width', + 'Text direction': 'Text direction', + 'Header style': 'Header style', + 'Numbered list': 'Numbered list', + 'Bullet list': 'Bullet list', + 'Checked list': 'Checked list', + 'Code block': 'Code block', + 'Quote': 'Quote', + 'Increase indent': 'Increase indent', + 'Decrease indent': 'Decrease indent', + 'Insert URL': 'Insert URL', + 'Visit link': 'Visit link', + 'Enter link': 'Enter link', + 'Enter media': 'Enter media', + 'Edit': 'Edit', + 'Apply': 'Apply', }, 'de': { 'Paste a link': 'Link hinzufügen', @@ -220,6 +300,39 @@ extension Localization on String { 'Next': 'Nächster', 'Camera': 'Camera', 'Video': 'Video', + 'Undo': 'Undo', + 'Redo': 'Redo', + 'Font family': 'Font family', + 'Font size': 'Font size', + 'Bold': 'Bold', + 'Subscript': 'Subscript', + 'Superscript': 'Superscript', + 'Italic': 'Italic', + 'Underline': 'Underline', + 'Strike through': 'Strike through', + 'Inline code': 'Inline code', + 'Font color': 'Font color', + 'Background color': 'Background color', + 'Clear format': 'Clear format', + 'Align left': 'Align left', + 'Align center': 'Align center', + 'Align right': 'Align right', + 'Justify win width': 'Justify win width', + 'Text direction': 'Text direction', + 'Header style': 'Header style', + 'Numbered list': 'Numbered list', + 'Bullet list': 'Bullet list', + 'Checked list': 'Checked list', + 'Code block': 'Code block', + 'Quote': 'Quote', + 'Increase indent': 'Increase indent', + 'Decrease indent': 'Decrease indent', + 'Insert URL': 'Insert URL', + 'Visit link': 'Visit link', + 'Enter link': 'Enter link', + 'Enter media': 'Enter media', + 'Edit': 'Edit', + 'Apply': 'Apply', }, 'fr': { 'Paste a link': 'Coller un lien', @@ -253,6 +366,39 @@ extension Localization on String { 'Next': 'Suivant', 'Camera': 'Caméra', 'Video': 'Vidéo', + 'Undo': 'Undo', + 'Redo': 'Redo', + 'Font family': 'Font family', + 'Font size': 'Font size', + 'Bold': 'Bold', + 'Subscript': 'Subscript', + 'Superscript': 'Superscript', + 'Italic': 'Italic', + 'Underline': 'Underline', + 'Strike through': 'Strike through', + 'Inline code': 'Inline code', + 'Font color': 'Font color', + 'Background color': 'Background color', + 'Clear format': 'Clear format', + 'Align left': 'Align left', + 'Align center': 'Align center', + 'Align right': 'Align right', + 'Justify win width': 'Justify win width', + 'Text direction': 'Text direction', + 'Header style': 'Header style', + 'Numbered list': 'Numbered list', + 'Bullet list': 'Bullet list', + 'Checked list': 'Checked list', + 'Code block': 'Code block', + 'Quote': 'Quote', + 'Increase indent': 'Increase indent', + 'Decrease indent': 'Decrease indent', + 'Insert URL': 'Insert URL', + 'Visit link': 'Visit link', + 'Enter link': 'Enter link', + 'Enter media': 'Enter media', + 'Edit': 'Edit', + 'Apply': 'Apply', }, 'zh_cn': { 'Paste a link': '粘贴链接', @@ -286,6 +432,105 @@ extension Localization on String { 'Next': '下一个', 'Camera': '拍照', 'Video': '录像', + 'Undo': '撤销', + 'Redo': '重做', + 'Font family': '字体', + 'Font size': '字号', + 'Bold': '粗体', + 'Subscript': '下标', + 'Superscript': '上标', + 'Italic': '斜体', + 'Underline': '下划线', + 'Strike through': '删除线', + 'Inline code': '内联代码', + 'Font color': '字体颜色', + 'Background color': '背景颜色', + 'Clear format': '清除格式', + 'Align left': '左对齐', + 'Align center': '居中对齐', + 'Align right': '右对齐', + 'Justify win width': '两端对齐', + 'Text direction': '文本方向', + 'Header style': '标题样式', + 'Numbered list': '有序列表', + 'Bullet list': '无序列表', + 'Checked list': '任务列表', + 'Code block': '代码块', + 'Quote': '引言', + 'Increase indent': '增加缩进', + 'Decrease indent': '减少缩进', + 'Insert URL': '插入链接', + 'Visit link': '访问链接', + 'Enter link': '输入链接', + 'Enter media': '输入媒体', + 'Edit': '编辑', + 'Apply': '应用', + }, + 'zh_hk': { + 'Paste a link': '貼上連結', + 'Ok': '確定', + 'Select Color': '選擇顏色', + 'Gallery': '圖片庫', + 'Link': '連結', + 'Please first select some text to transform into a link.': + '請選擇文字以轉換為連結。', + 'Open': '開啓', + 'Copy': '複製', + 'Remove': '移除', + 'Save': '儲存', + 'Zoom': '放大', + 'Saved': '已儲存', + 'Text': '文字', + 'What is entered is not a link': '輸入的不是連結', + 'Resize': '變更大小', + 'Width': '寛', + 'Height': '高', + 'Size': '大小', + 'Small': '小', + 'Large': '大', + 'Huge': '超大', + 'Clear': '清除', + 'Font': '字型', + 'Search': '搜尋', + 'matches': '符合', + 'showing match': '顯示符合', + 'Prev': '上一個', + 'Next': '下一個', + 'Camera': '相機', + 'Video': '錄影', + 'Undo': '撤銷', + 'Redo': '重做', + 'Font family': '字體', + 'Font size': '字號', + 'Bold': '粗體', + 'Subscript': '下標', + 'Superscript': '上標', + 'Italic': '斜體', + 'Underline': '下劃線', + 'Strike through': '刪除線', + 'Inline code': '內聯代碼', + 'Font color': '字體顏色', + 'Background color': '背景顏色', + 'Clear format': '清除格式', + 'Align left': '左對齊', + 'Align center': '居中對齊', + 'Align right': '右對齊', + 'Justify win width': '兩端對齊', + 'Text direction': '文本方向', + 'Header style': '標題樣式', + 'Numbered list': '有序列表', + 'Bullet list': '無序列表', + 'Checked list': '任務列表', + 'Code block': '代碼塊', + 'Quote': '引言', + 'Increase indent': '增加縮進', + 'Decrease indent': '減少縮進', + 'Insert URL': '插入鏈接', + 'Visit link': '訪問鏈接', + 'Enter link': '輸入鏈接', + 'Enter media': '輸入媒體', + 'Edit': '編輯', + 'Apply': '應用', }, 'jp': { 'Paste a link': 'リンクをペースト', @@ -319,6 +564,39 @@ extension Localization on String { 'Next': '次へ', 'Camera': 'カメラ', 'Video': 'ビデオ', + 'Undo': '取り消し', + 'Redo': 'やり直し', + 'Font family': 'フォントファミリー', + 'Font size': 'フォントサイズ', + 'Bold': '太字', + 'Subscript': '下付き', + 'Superscript': '上付き', + 'Italic': '斜体', + 'Underline': '下線', + 'Strike through': '取り消し線', + 'Inline code': 'インラインコード', + 'Font color': 'フォントカラー', + 'Background color': 'ベースカラー', + 'Clear format': 'クリアフォーマット', + 'Align left': '左揃え', + 'Align center': 'センターアライメント', + 'Align right': '右揃え', + 'Justify win width': '両端揃え', + 'Text direction': '文字方向', + 'Header style': 'タイトルスタイル', + 'Numbered list': '順序付きリスト', + 'Bullet list': '順序無しリスト', + 'Checked list': 'チェックボックス', + 'Code block': 'コード', + 'Quote': '引用', + 'Increase indent': 'インデントを増やす', + 'Decrease indent': 'インデントを減らす', + 'Insert URL': 'ハイパーリンクを挿入', + 'Visit link': 'ハイパーリンクを訪問', + 'Enter link': 'ハイパーリンクを輸入', + 'Enter media': 'ミディアムを輸入', + 'Edit': '編集', + 'Apply': '応用', }, 'ko': { 'Paste a link': '링크를 붙여넣어 주세요.', @@ -352,6 +630,39 @@ extension Localization on String { 'Next': '다음', 'Camera': '카메라', 'Video': '비디오', + 'Undo': 'Undo', + 'Redo': 'Redo', + 'Font family': 'Font family', + 'Font size': 'Font size', + 'Bold': 'Bold', + 'Subscript': 'Subscript', + 'Superscript': 'Superscript', + 'Italic': 'Italic', + 'Underline': 'Underline', + 'Strike through': 'Strike through', + 'Inline code': 'Inline code', + 'Font color': 'Font color', + 'Background color': 'Background color', + 'Clear format': 'Clear format', + 'Align left': 'Align left', + 'Align center': 'Align center', + 'Align right': 'Align right', + 'Justify win width': 'Justify win width', + 'Text direction': 'Text direction', + 'Header style': 'Header style', + 'Numbered list': 'Numbered list', + 'Bullet list': 'Bullet list', + 'Checked list': 'Checked list', + 'Code block': 'Code block', + 'Quote': 'Quote', + 'Increase indent': 'Increase indent', + 'Decrease indent': 'Decrease indent', + 'Insert URL': 'Insert URL', + 'Visit link': 'Visit link', + 'Enter link': 'Enter link', + 'Enter media': 'Enter media', + 'Edit': 'Edit', + 'Apply': 'Apply', }, 'ru': { 'Paste a link': 'Вставить ссылку', @@ -385,6 +696,39 @@ extension Localization on String { 'Next': 'Next', 'Camera': 'Camera', 'Video': 'Video', + 'Undo': 'Undo', + 'Redo': 'Redo', + 'Font family': 'Font family', + 'Font size': 'Font size', + 'Bold': 'Bold', + 'Subscript': 'Subscript', + 'Superscript': 'Superscript', + 'Italic': 'Italic', + 'Underline': 'Underline', + 'Strike through': 'Strike through', + 'Inline code': 'Inline code', + 'Font color': 'Font color', + 'Background color': 'Background color', + 'Clear format': 'Clear format', + 'Align left': 'Align left', + 'Align center': 'Align center', + 'Align right': 'Align right', + 'Justify win width': 'Justify win width', + 'Text direction': 'Text direction', + 'Header style': 'Header style', + 'Numbered list': 'Numbered list', + 'Bullet list': 'Bullet list', + 'Checked list': 'Checked list', + 'Code block': 'Code block', + 'Quote': 'Quote', + 'Increase indent': 'Increase indent', + 'Decrease indent': 'Decrease indent', + 'Insert URL': 'Insert URL', + 'Visit link': 'Visit link', + 'Enter link': 'Enter link', + 'Enter media': 'Enter media', + 'Edit': 'Edit', + 'Apply': 'Apply', }, 'es': { 'Paste a link': 'Pega un enlace', @@ -419,6 +763,39 @@ extension Localization on String { 'Next': 'Siguiente', 'Camera': 'Cámara', 'Video': 'Vídeo', + 'Undo': 'Undo', + 'Redo': 'Redo', + 'Font family': 'Font family', + 'Font size': 'Font size', + 'Bold': 'Bold', + 'Subscript': 'Subscript', + 'Superscript': 'Superscript', + 'Italic': 'Italic', + 'Underline': 'Underline', + 'Strike through': 'Strike through', + 'Inline code': 'Inline code', + 'Font color': 'Font color', + 'Background color': 'Background color', + 'Clear format': 'Clear format', + 'Align left': 'Align left', + 'Align center': 'Align center', + 'Align right': 'Align right', + 'Justify win width': 'Justify win width', + 'Text direction': 'Text direction', + 'Header style': 'Header style', + 'Numbered list': 'Numbered list', + 'Bullet list': 'Bullet list', + 'Checked list': 'Checked list', + 'Code block': 'Code block', + 'Quote': 'Quote', + 'Increase indent': 'Increase indent', + 'Decrease indent': 'Decrease indent', + 'Insert URL': 'Insert URL', + 'Visit link': 'Visit link', + 'Enter link': 'Enter link', + 'Enter media': 'Enter media', + 'Edit': 'Edit', + 'Apply': 'Apply', }, 'tr': { 'Paste a link': 'Bağlantıyı Yapıştır', @@ -452,6 +829,39 @@ extension Localization on String { 'Next': 'Devam', 'Camera': 'Kamera', 'Video': 'Video', + 'Undo': 'Undo', + 'Redo': 'Redo', + 'Font family': 'Font family', + 'Font size': 'Font size', + 'Bold': 'Bold', + 'Subscript': 'Subscript', + 'Superscript': 'Superscript', + 'Italic': 'Italic', + 'Underline': 'Underline', + 'Strike through': 'Strike through', + 'Inline code': 'Inline code', + 'Font color': 'Font color', + 'Background color': 'Background color', + 'Clear format': 'Clear format', + 'Align left': 'Align left', + 'Align center': 'Align center', + 'Align right': 'Align right', + 'Justify win width': 'Justify win width', + 'Text direction': 'Text direction', + 'Header style': 'Header style', + 'Numbered list': 'Numbered list', + 'Bullet list': 'Bullet list', + 'Checked list': 'Checked list', + 'Code block': 'Code block', + 'Quote': 'Quote', + 'Increase indent': 'Increase indent', + 'Decrease indent': 'Decrease indent', + 'Insert URL': 'Insert URL', + 'Visit link': 'Visit link', + 'Enter link': 'Enter link', + 'Enter media': 'Enter media', + 'Edit': 'Edit', + 'Apply': 'Apply', }, 'uk': { 'Paste a link': 'Вставити посилання', @@ -485,21 +895,54 @@ extension Localization on String { 'Next': 'Next', 'Camera': 'Camera', 'Video': 'Video', - }, - 'pt': { - 'Paste a link': 'Colar um link', - 'Ok': 'Ok', - 'Select Color': 'Selecionar uma cor', - 'Gallery': 'Galeria', - 'Link': 'Link', - 'Please first select some text to transform into a link.': - 'Por favor selecione primeiro um texto para ' - 'transformá-lo em um link', - 'Open': 'Abra', - 'Copy': 'Copiar', - 'Remove': 'Remover', - 'Save': 'Salvar', - 'Zoom': 'Zoom', + 'Undo': 'Undo', + 'Redo': 'Redo', + 'Font family': 'Font family', + 'Font size': 'Font size', + 'Bold': 'Bold', + 'Subscript': 'Subscript', + 'Superscript': 'Superscript', + 'Italic': 'Italic', + 'Underline': 'Underline', + 'Strike through': 'Strike through', + 'Inline code': 'Inline code', + 'Font color': 'Font color', + 'Background color': 'Background color', + 'Clear format': 'Clear format', + 'Align left': 'Align left', + 'Align center': 'Align center', + 'Align right': 'Align right', + 'Justify win width': 'Justify win width', + 'Text direction': 'Text direction', + 'Header style': 'Header style', + 'Numbered list': 'Numbered list', + 'Bullet list': 'Bullet list', + 'Checked list': 'Checked list', + 'Code block': 'Code block', + 'Quote': 'Quote', + 'Increase indent': 'Increase indent', + 'Decrease indent': 'Decrease indent', + 'Insert URL': 'Insert URL', + 'Visit link': 'Visit link', + 'Enter link': 'Enter link', + 'Enter media': 'Enter media', + 'Edit': 'Edit', + 'Apply': 'Apply', + }, + 'pt': { + 'Paste a link': 'Colar um link', + 'Ok': 'Ok', + 'Select Color': 'Selecionar uma cor', + 'Gallery': 'Galeria', + 'Link': 'Link', + 'Please first select some text to transform into a link.': + 'Por favor selecione primeiro um texto para ' + 'transformá-lo em um link', + 'Open': 'Abra', + 'Copy': 'Copiar', + 'Remove': 'Remover', + 'Save': 'Salvar', + 'Zoom': 'Zoom', 'Saved': 'Salvo', 'Text': 'Texto', 'What is entered is not a link': 'O link inserido não é válido', @@ -519,6 +962,39 @@ extension Localization on String { 'Next': 'Next', 'Camera': 'Camera', 'Video': 'Video', + 'Undo': 'Undo', + 'Redo': 'Redo', + 'Font family': 'Font family', + 'Font size': 'Font size', + 'Bold': 'Bold', + 'Subscript': 'Subscript', + 'Superscript': 'Superscript', + 'Italic': 'Italic', + 'Underline': 'Underline', + 'Strike through': 'Strike through', + 'Inline code': 'Inline code', + 'Font color': 'Font color', + 'Background color': 'Background color', + 'Clear format': 'Clear format', + 'Align left': 'Align left', + 'Align center': 'Align center', + 'Align right': 'Align right', + 'Justify win width': 'Justify win width', + 'Text direction': 'Text direction', + 'Header style': 'Header style', + 'Numbered list': 'Numbered list', + 'Bullet list': 'Bullet list', + 'Checked list': 'Checked list', + 'Code block': 'Code block', + 'Quote': 'Quote', + 'Increase indent': 'Increase indent', + 'Decrease indent': 'Decrease indent', + 'Insert URL': 'Insert URL', + 'Visit link': 'Visit link', + 'Enter link': 'Enter link', + 'Enter media': 'Enter media', + 'Edit': 'Edit', + 'Apply': 'Apply', }, 'pt_br': { 'Paste a link': 'Colar um link', @@ -553,6 +1029,39 @@ extension Localization on String { 'Next': 'Próximo', 'Camera': 'Camera', 'Video': 'Vídeo', + 'Undo': 'Undo', + 'Redo': 'Redo', + 'Font family': 'Font family', + 'Font size': 'Font size', + 'Bold': 'Bold', + 'Subscript': 'Subscript', + 'Superscript': 'Superscript', + 'Italic': 'Italic', + 'Underline': 'Underline', + 'Strike through': 'Strike through', + 'Inline code': 'Inline code', + 'Font color': 'Font color', + 'Background color': 'Background color', + 'Clear format': 'Clear format', + 'Align left': 'Align left', + 'Align center': 'Align center', + 'Align right': 'Align right', + 'Justify win width': 'Justify win width', + 'Text direction': 'Text direction', + 'Header style': 'Header style', + 'Numbered list': 'Numbered list', + 'Bullet list': 'Bullet list', + 'Checked list': 'Checked list', + 'Code block': 'Code block', + 'Quote': 'Quote', + 'Increase indent': 'Increase indent', + 'Decrease indent': 'Decrease indent', + 'Insert URL': 'Insert URL', + 'Visit link': 'Visit link', + 'Enter link': 'Enter link', + 'Enter media': 'Enter media', + 'Edit': 'Edit', + 'Apply': 'Apply', }, 'pl': { 'Paste a link': 'Wklej link', @@ -587,6 +1096,39 @@ extension Localization on String { 'Next': 'Next', 'Camera': 'Camera', 'Video': 'Video', + 'Undo': 'Undo', + 'Redo': 'Redo', + 'Font family': 'Font family', + 'Font size': 'Font size', + 'Bold': 'Bold', + 'Subscript': 'Subscript', + 'Superscript': 'Superscript', + 'Italic': 'Italic', + 'Underline': 'Underline', + 'Strike through': 'Strike through', + 'Inline code': 'Inline code', + 'Font color': 'Font color', + 'Background color': 'Background color', + 'Clear format': 'Clear format', + 'Align left': 'Align left', + 'Align center': 'Align center', + 'Align right': 'Align right', + 'Justify win width': 'Justify win width', + 'Text direction': 'Text direction', + 'Header style': 'Header style', + 'Numbered list': 'Numbered list', + 'Bullet list': 'Bullet list', + 'Checked list': 'Checked list', + 'Code block': 'Code block', + 'Quote': 'Quote', + 'Increase indent': 'Increase indent', + 'Decrease indent': 'Decrease indent', + 'Insert URL': 'Insert URL', + 'Visit link': 'Visit link', + 'Enter link': 'Enter link', + 'Enter media': 'Enter media', + 'Edit': 'Edit', + 'Apply': 'Apply', }, 'vi': { 'Paste a link': 'Chèn liên kết', @@ -621,6 +1163,39 @@ extension Localization on String { 'Next': 'Next', 'Camera': 'Camera', 'Video': 'Video', + 'Undo': 'Undo', + 'Redo': 'Redo', + 'Font family': 'Font family', + 'Font size': 'Font size', + 'Bold': 'Bold', + 'Subscript': 'Subscript', + 'Superscript': 'Superscript', + 'Italic': 'Italic', + 'Underline': 'Underline', + 'Strike through': 'Strike through', + 'Inline code': 'Inline code', + 'Font color': 'Font color', + 'Background color': 'Background color', + 'Clear format': 'Clear format', + 'Align left': 'Align left', + 'Align center': 'Align center', + 'Align right': 'Align right', + 'Justify win width': 'Justify win width', + 'Text direction': 'Text direction', + 'Header style': 'Header style', + 'Numbered list': 'Numbered list', + 'Bullet list': 'Bullet list', + 'Checked list': 'Checked list', + 'Code block': 'Code block', + 'Quote': 'Quote', + 'Increase indent': 'Increase indent', + 'Decrease indent': 'Decrease indent', + 'Insert URL': 'Insert URL', + 'Visit link': 'Visit link', + 'Enter link': 'Enter link', + 'Enter media': 'Enter media', + 'Edit': 'Edit', + 'Apply': 'Apply', }, 'ur': { 'Paste a link': 'لنک پیسٹ کریں', @@ -654,6 +1229,39 @@ extension Localization on String { 'Next': 'Next', 'Camera': 'Camera', 'Video': 'Video', + 'Undo': 'Undo', + 'Redo': 'Redo', + 'Font family': 'Font family', + 'Font size': 'Font size', + 'Bold': 'Bold', + 'Subscript': 'Subscript', + 'Superscript': 'Superscript', + 'Italic': 'Italic', + 'Underline': 'Underline', + 'Strike through': 'Strike through', + 'Inline code': 'Inline code', + 'Font color': 'Font color', + 'Background color': 'Background color', + 'Clear format': 'Clear format', + 'Align left': 'Align left', + 'Align center': 'Align center', + 'Align right': 'Align right', + 'Justify win width': 'Justify win width', + 'Text direction': 'Text direction', + 'Header style': 'Header style', + 'Numbered list': 'Numbered list', + 'Bullet list': 'Bullet list', + 'Checked list': 'Checked list', + 'Code block': 'Code block', + 'Quote': 'Quote', + 'Increase indent': 'Increase indent', + 'Decrease indent': 'Decrease indent', + 'Insert URL': 'Insert URL', + 'Visit link': 'Visit link', + 'Enter link': 'Enter link', + 'Enter media': 'Enter media', + 'Edit': 'Edit', + 'Apply': 'Apply', }, 'id': { 'Paste a link': 'Tempel tautan', @@ -687,6 +1295,39 @@ extension Localization on String { 'Next': 'Next', 'Camera': 'Camera', 'Video': 'Video', + 'Undo': 'Undo', + 'Redo': 'Redo', + 'Font family': 'Font family', + 'Font size': 'Font size', + 'Bold': 'Bold', + 'Subscript': 'Subscript', + 'Superscript': 'Superscript', + 'Italic': 'Italic', + 'Underline': 'Underline', + 'Strike through': 'Strike through', + 'Inline code': 'Inline code', + 'Font color': 'Font color', + 'Background color': 'Background color', + 'Clear format': 'Clear format', + 'Align left': 'Align left', + 'Align center': 'Align center', + 'Align right': 'Align right', + 'Justify win width': 'Justify win width', + 'Text direction': 'Text direction', + 'Header style': 'Header style', + 'Numbered list': 'Numbered list', + 'Bullet list': 'Bullet list', + 'Checked list': 'Checked list', + 'Code block': 'Code block', + 'Quote': 'Quote', + 'Increase indent': 'Increase indent', + 'Decrease indent': 'Decrease indent', + 'Insert URL': 'Insert URL', + 'Visit link': 'Visit link', + 'Enter link': 'Enter link', + 'Enter media': 'Enter media', + 'Edit': 'Edit', + 'Apply': 'Apply', }, 'no': { 'Paste a link': 'Lim inn lenke', @@ -720,6 +1361,39 @@ extension Localization on String { 'Next': 'Next', 'Camera': 'Camera', 'Video': 'Video', + 'Undo': 'Undo', + 'Redo': 'Redo', + 'Font family': 'Font family', + 'Font size': 'Font size', + 'Bold': 'Bold', + 'Subscript': 'Subscript', + 'Superscript': 'Superscript', + 'Italic': 'Italic', + 'Underline': 'Underline', + 'Strike through': 'Strike through', + 'Inline code': 'Inline code', + 'Font color': 'Font color', + 'Background color': 'Background color', + 'Clear format': 'Clear format', + 'Align left': 'Align left', + 'Align center': 'Align center', + 'Align right': 'Align right', + 'Justify win width': 'Justify win width', + 'Text direction': 'Text direction', + 'Header style': 'Header style', + 'Numbered list': 'Numbered list', + 'Bullet list': 'Bullet list', + 'Checked list': 'Checked list', + 'Code block': 'Code block', + 'Quote': 'Quote', + 'Increase indent': 'Increase indent', + 'Decrease indent': 'Decrease indent', + 'Insert URL': 'Insert URL', + 'Visit link': 'Visit link', + 'Enter link': 'Enter link', + 'Enter media': 'Enter media', + 'Edit': 'Edit', + 'Apply': 'Apply', }, 'fa': { 'Paste a link': 'جایگذاری لینک', @@ -753,6 +1427,39 @@ extension Localization on String { 'Next': 'بعدی', 'Camera': 'دوربین', 'Video': 'ویدیو', + 'Undo': 'Undo', + 'Redo': 'Redo', + 'Font family': 'Font family', + 'Font size': 'Font size', + 'Bold': 'Bold', + 'Subscript': 'Subscript', + 'Superscript': 'Superscript', + 'Italic': 'Italic', + 'Underline': 'Underline', + 'Strike through': 'Strike through', + 'Inline code': 'Inline code', + 'Font color': 'Font color', + 'Background color': 'Background color', + 'Clear format': 'Clear format', + 'Align left': 'Align left', + 'Align center': 'Align center', + 'Align right': 'Align right', + 'Justify win width': 'Justify win width', + 'Text direction': 'Text direction', + 'Header style': 'Header style', + 'Numbered list': 'Numbered list', + 'Bullet list': 'Bullet list', + 'Checked list': 'Checked list', + 'Code block': 'Code block', + 'Quote': 'Quote', + 'Increase indent': 'Increase indent', + 'Decrease indent': 'Decrease indent', + 'Insert URL': 'Insert URL', + 'Visit link': 'Visit link', + 'Enter link': 'Enter link', + 'Enter media': 'Enter media', + 'Edit': 'Edit', + 'Apply': 'Apply', }, 'hi': { 'Paste a link': 'लिंक पेस्ट करें', @@ -786,6 +1493,39 @@ extension Localization on String { 'Next': 'Next', 'Camera': 'Camera', 'Video': 'Video', + 'Undo': 'Undo', + 'Redo': 'Redo', + 'Font family': 'Font family', + 'Font size': 'Font size', + 'Bold': 'Bold', + 'Subscript': 'Subscript', + 'Superscript': 'Superscript', + 'Italic': 'Italic', + 'Underline': 'Underline', + 'Strike through': 'Strike through', + 'Inline code': 'Inline code', + 'Font color': 'Font color', + 'Background color': 'Background color', + 'Clear format': 'Clear format', + 'Align left': 'Align left', + 'Align center': 'Align center', + 'Align right': 'Align right', + 'Justify win width': 'Justify win width', + 'Text direction': 'Text direction', + 'Header style': 'Header style', + 'Numbered list': 'Numbered list', + 'Bullet list': 'Bullet list', + 'Checked list': 'Checked list', + 'Code block': 'Code block', + 'Quote': 'Quote', + 'Increase indent': 'Increase indent', + 'Decrease indent': 'Decrease indent', + 'Insert URL': 'Insert URL', + 'Visit link': 'Visit link', + 'Enter link': 'Enter link', + 'Enter media': 'Enter media', + 'Edit': 'Edit', + 'Apply': 'Apply', }, 'nl': { 'Paste a link': 'Plak een link', @@ -819,39 +1559,39 @@ extension Localization on String { 'Next': 'Next', 'Camera': 'Camera', 'Video': 'Video', - }, - 'zh_hk': { - 'Paste a link': '貼上連結', - 'Ok': '確定', - 'Select Color': '選擇顏色', - 'Gallery': '圖片庫', - 'Link': '連結', - 'Please first select some text to transform into a link.': - '請選擇文字以轉換為連結。', - 'Open': '開啓', - 'Copy': '複製', - 'Remove': '移除', - 'Save': '儲存', - 'Zoom': '放大', - 'Saved': '已儲存', - 'Text': '文字', - 'What is entered is not a link': '輸入的不是連結', - 'Resize': '變更大小', - 'Width': '寛', - 'Height': '高', - 'Size': '大小', - 'Small': '小', - 'Large': '大', - 'Huge': '超大', - 'Clear': '清除', - 'Font': '字型', - 'Search': '搜尋', - 'matches': '符合', - 'showing match': '顯示符合', - 'Prev': '上一個', - 'Next': '下一個', - 'Camera': '相機', - 'Video': '錄影', + 'Undo': 'Undo', + 'Redo': 'Redo', + 'Font family': 'Font family', + 'Font size': 'Font size', + 'Bold': 'Bold', + 'Subscript': 'Subscript', + 'Superscript': 'Superscript', + 'Italic': 'Italic', + 'Underline': 'Underline', + 'Strike through': 'Strike through', + 'Inline code': 'Inline code', + 'Font color': 'Font color', + 'Background color': 'Background color', + 'Clear format': 'Clear format', + 'Align left': 'Align left', + 'Align center': 'Align center', + 'Align right': 'Align right', + 'Justify win width': 'Justify win width', + 'Text direction': 'Text direction', + 'Header style': 'Header style', + 'Numbered list': 'Numbered list', + 'Bullet list': 'Bullet list', + 'Checked list': 'Checked list', + 'Code block': 'Code block', + 'Quote': 'Quote', + 'Increase indent': 'Increase indent', + 'Decrease indent': 'Decrease indent', + 'Insert URL': 'Insert URL', + 'Visit link': 'Visit link', + 'Enter link': 'Enter link', + 'Enter media': 'Enter media', + 'Edit': 'Edit', + 'Apply': 'Apply', }, 'sr': { 'Paste a link': 'Nalepi vezu', @@ -885,6 +1625,39 @@ extension Localization on String { 'Next': 'Next', 'Camera': 'Camera', 'Video': 'Video', + 'Undo': 'Undo', + 'Redo': 'Redo', + 'Font family': 'Font family', + 'Font size': 'Font size', + 'Bold': 'Bold', + 'Subscript': 'Subscript', + 'Superscript': 'Superscript', + 'Italic': 'Italic', + 'Underline': 'Underline', + 'Strike through': 'Strike through', + 'Inline code': 'Inline code', + 'Font color': 'Font color', + 'Background color': 'Background color', + 'Clear format': 'Clear format', + 'Align left': 'Align left', + 'Align center': 'Align center', + 'Align right': 'Align right', + 'Justify win width': 'Justify win width', + 'Text direction': 'Text direction', + 'Header style': 'Header style', + 'Numbered list': 'Numbered list', + 'Bullet list': 'Bullet list', + 'Checked list': 'Checked list', + 'Code block': 'Code block', + 'Quote': 'Quote', + 'Increase indent': 'Increase indent', + 'Decrease indent': 'Decrease indent', + 'Insert URL': 'Insert URL', + 'Visit link': 'Visit link', + 'Enter link': 'Enter link', + 'Enter media': 'Enter media', + 'Edit': 'Edit', + 'Apply': 'Apply', }, 'cs': { 'Paste a link': 'Vložte odkaz', @@ -918,6 +1691,39 @@ extension Localization on String { 'Next': 'Další', 'Camera': 'Kamera', 'Video': 'Video', + 'Undo': 'Undo', + 'Redo': 'Redo', + 'Font family': 'Font family', + 'Font size': 'Font size', + 'Bold': 'Bold', + 'Subscript': 'Subscript', + 'Superscript': 'Superscript', + 'Italic': 'Italic', + 'Underline': 'Underline', + 'Strike through': 'Strike through', + 'Inline code': 'Inline code', + 'Font color': 'Font color', + 'Background color': 'Background color', + 'Clear format': 'Clear format', + 'Align left': 'Align left', + 'Align center': 'Align center', + 'Align right': 'Align right', + 'Justify win width': 'Justify win width', + 'Text direction': 'Text direction', + 'Header style': 'Header style', + 'Numbered list': 'Numbered list', + 'Bullet list': 'Bullet list', + 'Checked list': 'Checked list', + 'Code block': 'Code block', + 'Quote': 'Quote', + 'Increase indent': 'Increase indent', + 'Decrease indent': 'Decrease indent', + 'Insert URL': 'Insert URL', + 'Visit link': 'Visit link', + 'Enter link': 'Enter link', + 'Enter media': 'Enter media', + 'Edit': 'Edit', + 'Apply': 'Apply', }, 'he': { 'Paste a link': 'הדבק את הלינק', @@ -951,6 +1757,39 @@ extension Localization on String { 'Next': 'הבא', 'Camera': 'מצלמה', 'Video': 'וידאו', + 'Undo': 'Undo', + 'Redo': 'Redo', + 'Font family': 'Font family', + 'Font size': 'Font size', + 'Bold': 'Bold', + 'Subscript': 'Subscript', + 'Superscript': 'Superscript', + 'Italic': 'Italic', + 'Underline': 'Underline', + 'Strike through': 'Strike through', + 'Inline code': 'Inline code', + 'Font color': 'Font color', + 'Background color': 'Background color', + 'Clear format': 'Clear format', + 'Align left': 'Align left', + 'Align center': 'Align center', + 'Align right': 'Align right', + 'Justify win width': 'Justify win width', + 'Text direction': 'Text direction', + 'Header style': 'Header style', + 'Numbered list': 'Numbered list', + 'Bullet list': 'Bullet list', + 'Checked list': 'Checked list', + 'Code block': 'Code block', + 'Quote': 'Quote', + 'Increase indent': 'Increase indent', + 'Decrease indent': 'Decrease indent', + 'Insert URL': 'Insert URL', + 'Visit link': 'Visit link', + 'Enter link': 'Enter link', + 'Enter media': 'Enter media', + 'Edit': 'Edit', + 'Apply': 'Apply', }, 'ms': { 'Paste a link': 'Tampal Pautan', @@ -985,6 +1824,39 @@ extension Localization on String { 'Next': 'Seterusnya', 'Camera': 'Kamera', 'Video': 'Video', + 'Undo': 'Undo', + 'Redo': 'Redo', + 'Font family': 'Font family', + 'Font size': 'Font size', + 'Bold': 'Bold', + 'Subscript': 'Subscript', + 'Superscript': 'Superscript', + 'Italic': 'Italic', + 'Underline': 'Underline', + 'Strike through': 'Strike through', + 'Inline code': 'Inline code', + 'Font color': 'Font color', + 'Background color': 'Background color', + 'Clear format': 'Clear format', + 'Align left': 'Align left', + 'Align center': 'Align center', + 'Align right': 'Align right', + 'Justify win width': 'Justify win width', + 'Text direction': 'Text direction', + 'Header style': 'Header style', + 'Numbered list': 'Numbered list', + 'Bullet list': 'Bullet list', + 'Checked list': 'Checked list', + 'Code block': 'Code block', + 'Quote': 'Quote', + 'Increase indent': 'Increase indent', + 'Decrease indent': 'Decrease indent', + 'Insert URL': 'Insert URL', + 'Visit link': 'Visit link', + 'Enter link': 'Enter link', + 'Enter media': 'Enter media', + 'Edit': 'Edit', + 'Apply': 'Apply', }, 'it': { 'Paste a link': 'Incolla un collegamento', @@ -1019,6 +1891,39 @@ extension Localization on String { 'Next': 'Succ', 'Camera': 'Camera', 'Video': 'Video', + 'Undo': 'Undo', + 'Redo': 'Redo', + 'Font family': 'Font family', + 'Font size': 'Font size', + 'Bold': 'Bold', + 'Subscript': 'Subscript', + 'Superscript': 'Superscript', + 'Italic': 'Italic', + 'Underline': 'Underline', + 'Strike through': 'Strike through', + 'Inline code': 'Inline code', + 'Font color': 'Font color', + 'Background color': 'Background color', + 'Clear format': 'Clear format', + 'Align left': 'Align left', + 'Align center': 'Align center', + 'Align right': 'Align right', + 'Justify win width': 'Justify win width', + 'Text direction': 'Text direction', + 'Header style': 'Header style', + 'Numbered list': 'Numbered list', + 'Bullet list': 'Bullet list', + 'Checked list': 'Checked list', + 'Code block': 'Code block', + 'Quote': 'Quote', + 'Increase indent': 'Increase indent', + 'Decrease indent': 'Decrease indent', + 'Insert URL': 'Insert URL', + 'Visit link': 'Visit link', + 'Enter link': 'Enter link', + 'Enter media': 'Enter media', + 'Edit': 'Edit', + 'Apply': 'Apply', }, }; diff --git a/lib/src/widgets/controller.dart b/lib/src/widgets/controller.dart index 30396377..48bf136d 100644 --- a/lib/src/widgets/controller.dart +++ b/lib/src/widgets/controller.dart @@ -101,6 +101,14 @@ class QuillController extends ChangeNotifier { // Increases or decreases the indent of the current selection by 1. void indentSelection(bool isIncrease) { + if (selection.isCollapsed) { + _indentSelectionFormat(isIncrease); + } else { + _indentSelectionEachLine(isIncrease); + } + } + + void _indentSelectionFormat(bool isIncrease) { final indent = getSelectionStyle().attributes[Attribute.indent.key]; if (indent == null) { if (isIncrease) { @@ -119,6 +127,38 @@ class QuillController extends ChangeNotifier { formatSelection(Attribute.getIndentLevel(indent.value - 1)); } + void _indentSelectionEachLine(bool isIncrease) { + final styles = document.collectAllStylesWithOffset( + selection.start, + selection.end - selection.start, + ); + for (final style in styles) { + final indent = style.value.attributes[Attribute.indent.key]; + final formatIndex = math.max(style.offset, selection.start); + final formatLength = math.min( + style.offset + (style.length ?? 0), + selection.end, + ) - + style.offset; + Attribute? formatAttribute; + if (indent == null) { + if (isIncrease) { + formatAttribute = Attribute.indentL1; + } + } else if (indent.value == 1 && !isIncrease) { + formatAttribute = Attribute.clone(Attribute.indentL1, null); + } else if (isIncrease) { + formatAttribute = Attribute.getIndentLevel(indent.value + 1); + } else { + formatAttribute = Attribute.getIndentLevel(indent.value - 1); + } + if (formatAttribute != null) { + document.format(formatIndex, formatLength, formatAttribute); + } + } + notifyListeners(); + } + /// Returns all styles for each node within selection List> getAllIndividualSelectionStyles() { final styles = document.collectAllIndividualStyles( diff --git a/lib/src/widgets/default_styles.dart b/lib/src/widgets/default_styles.dart index f9fef5d0..c2d6e878 100644 --- a/lib/src/widgets/default_styles.dart +++ b/lib/src/widgets/default_styles.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:flutter/material.dart'; import '../models/documents/attribute.dart'; @@ -141,6 +143,8 @@ class DefaultStyles { this.h3, this.paragraph, this.bold, + this.subscript, + this.superscript, this.italic, this.small, this.underline, @@ -165,6 +169,8 @@ class DefaultStyles { final DefaultTextBlockStyle? h3; final DefaultTextBlockStyle? paragraph; final TextStyle? bold; + final TextStyle? subscript; + final TextStyle? superscript; final TextStyle? italic; final TextStyle? small; final TextStyle? underline; @@ -244,6 +250,9 @@ class DefaultStyles { paragraph: DefaultTextBlockStyle(baseStyle, const VerticalSpacing(0, 0), const VerticalSpacing(0, 0), null), bold: const TextStyle(fontWeight: FontWeight.bold), + subscript: const TextStyle(fontFeatures: [FontFeature.subscripts()]), + superscript: + const TextStyle(fontFeatures: [FontFeature.superscripts()]), italic: const TextStyle(fontStyle: FontStyle.italic), small: const TextStyle(fontSize: 12), underline: const TextStyle(decoration: TextDecoration.underline), @@ -317,6 +326,8 @@ class DefaultStyles { h3: other.h3 ?? h3, paragraph: other.paragraph ?? paragraph, bold: other.bold ?? bold, + subscript: other.subscript ?? subscript, + superscript: other.superscript ?? superscript, italic: other.italic ?? italic, small: other.small ?? small, underline: other.underline ?? underline, diff --git a/lib/src/widgets/delegate.dart b/lib/src/widgets/delegate.dart index fa5dbe1f..de92d8eb 100644 --- a/lib/src/widgets/delegate.dart +++ b/lib/src/widgets/delegate.dart @@ -14,6 +14,9 @@ typedef EmbedsBuilder = EmbedBuilder Function(Embed node); typedef CustomStyleBuilder = TextStyle Function(Attribute attribute); +typedef CustomRecognizerBuilder = GestureRecognizer? Function( + Attribute attribute, Leaf leaf); + /// Delegate interface for the [EditorTextSelectionGestureDetectorBuilder]. /// /// The interface is usually implemented by textfield implementations wrapping diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index 2a1bc5e3..906b67fb 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -15,6 +15,7 @@ import '../models/documents/nodes/container.dart' as container_node; import '../models/documents/nodes/leaf.dart'; import '../models/documents/style.dart'; import '../models/structs/offset_value.dart'; +import '../models/themes/quill_dialog_theme.dart'; import '../utils/platform.dart'; import 'box.dart'; import 'controller.dart'; @@ -143,49 +144,51 @@ abstract class RenderAbstractEditor implements TextLayoutMetrics { } class QuillEditor extends StatefulWidget { - const QuillEditor( - {required this.controller, - required this.focusNode, - required this.scrollController, - required this.scrollable, - required this.padding, - required this.autoFocus, - required this.readOnly, - required this.expands, - this.showCursor, - this.paintCursorAboveText, - this.placeholder, - this.enableInteractiveSelection = true, - this.enableSelectionToolbar = true, - this.scrollBottomInset = 0, - this.minHeight, - this.maxHeight, - this.maxContentWidth, - this.customStyles, - this.textCapitalization = TextCapitalization.sentences, - this.keyboardAppearance = Brightness.light, - this.scrollPhysics, - this.onLaunchUrl, - this.onTapDown, - this.onTapUp, - this.onSingleLongTapStart, - this.onSingleLongTapMoveUpdate, - this.onSingleLongTapEnd, - this.embedBuilders, - this.unknownEmbedBuilder, - this.linkActionPickerDelegate = defaultLinkActionPickerDelegate, - this.customStyleBuilder, - this.locale, - this.floatingCursorDisabled = false, - this.textSelectionControls, - this.onImagePaste, - this.customShortcuts, - this.customActions, - this.detectWordBoundary = true, - this.enableUnfocusOnTapOutside = true, - this.customLinkPrefixes = const [], - Key? key}) - : super(key: key); + const QuillEditor({ + required this.controller, + required this.focusNode, + required this.scrollController, + required this.scrollable, + required this.padding, + required this.autoFocus, + required this.readOnly, + required this.expands, + this.showCursor, + this.paintCursorAboveText, + this.placeholder, + this.enableInteractiveSelection = true, + this.enableSelectionToolbar = true, + this.scrollBottomInset = 0, + this.minHeight, + this.maxHeight, + this.maxContentWidth, + this.customStyles, + this.textCapitalization = TextCapitalization.sentences, + this.keyboardAppearance = Brightness.light, + this.scrollPhysics, + this.onLaunchUrl, + this.onTapDown, + this.onTapUp, + this.onSingleLongTapStart, + this.onSingleLongTapMoveUpdate, + this.onSingleLongTapEnd, + this.embedBuilders, + this.unknownEmbedBuilder, + this.linkActionPickerDelegate = defaultLinkActionPickerDelegate, + this.customStyleBuilder, + this.customRecognizerBuilder, + this.locale, + this.floatingCursorDisabled = false, + this.textSelectionControls, + this.onImagePaste, + this.customShortcuts, + this.customActions, + this.detectWordBoundary = true, + this.enableUnfocusOnTapOutside = true, + this.customLinkPrefixes = const [], + this.dialogTheme, + Key? key, + }) : super(key: key); factory QuillEditor.basic({ required QuillController controller, @@ -302,6 +305,7 @@ class QuillEditor extends StatefulWidget { /// horizontally centered. This is mostly useful on devices with wide screens. final double? maxContentWidth; + /// Allows to override [DefaultStyles]. final DefaultStyles? customStyles; /// Whether this editor's height will be sized to fill its parent. @@ -369,6 +373,7 @@ class QuillEditor extends StatefulWidget { final Iterable? embedBuilders; final EmbedBuilder? unknownEmbedBuilder; final CustomStyleBuilder? customStyleBuilder; + final CustomRecognizerBuilder? customRecognizerBuilder; /// The locale to use for the editor toolbar, defaults to system locale /// More https://github.com/singerdmx/flutter-quill#translation @@ -401,7 +406,14 @@ class QuillEditor extends StatefulWidget { /// Returns the url of the image if the image should be inserted. final Future Function(Uint8List imageBytes)? onImagePaste; - final Map? customShortcuts; + /// Contains user-defined shortcuts map. + /// + /// [https://docs.flutter.dev/development/ui/advanced/actions-and-shortcuts#shortcuts] + final Map? customShortcuts; + + /// Contains user-defined actions. + /// + /// [https://docs.flutter.dev/development/ui/advanced/actions-and-shortcuts#actions] final Map>? customActions; final bool detectWordBoundary; @@ -412,6 +424,9 @@ class QuillEditor extends StatefulWidget { /// Useful for deeplinks final List customLinkPrefixes; + /// Configures the dialog theme. + final QuillDialogTheme? dialogTheme; + @override QuillEditorState createState() => QuillEditorState(); } @@ -505,12 +520,14 @@ class QuillEditorState extends State embedBuilder: _getEmbedBuilder, linkActionPickerDelegate: widget.linkActionPickerDelegate, customStyleBuilder: widget.customStyleBuilder, + customRecognizerBuilder: widget.customRecognizerBuilder, floatingCursorDisabled: widget.floatingCursorDisabled, onImagePaste: widget.onImagePaste, customShortcuts: widget.customShortcuts, customActions: widget.customActions, customLinkPrefixes: widget.customLinkPrefixes, enableUnfocusOnTapOutside: widget.enableUnfocusOnTapOutside, + dialogTheme: widget.dialogTheme, ); final editor = I18n( diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 64059983..89831d96 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -2,10 +2,9 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math' as math; -// ignore: unnecessary_import -import 'dart:typed_data'; import 'dart:ui' as ui hide TextStyle; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -24,6 +23,7 @@ import '../models/documents/nodes/node.dart'; import '../models/documents/style.dart'; import '../models/structs/offset_value.dart'; import '../models/structs/vertical_spacing.dart'; +import '../models/themes/quill_dialog_theme.dart'; import '../utils/cast.dart'; import '../utils/delta.dart'; import '../utils/embeds.dart'; @@ -42,46 +42,49 @@ import 'raw_editor/raw_editor_state_text_input_client_mixin.dart'; import 'text_block.dart'; import 'text_line.dart'; import 'text_selection.dart'; +import 'toolbar/link_style_button2.dart'; import 'toolbar/search_dialog.dart'; class RawEditor extends StatefulWidget { - const RawEditor( - {required this.controller, - required this.focusNode, - required this.scrollController, - required this.scrollBottomInset, - required this.cursorStyle, - required this.selectionColor, - required this.selectionCtrls, - required this.embedBuilder, - Key? key, - this.scrollable = true, - this.padding = EdgeInsets.zero, - this.readOnly = false, - this.placeholder, - this.onLaunchUrl, - this.contextMenuBuilder = defaultContextMenuBuilder, - this.showSelectionHandles = false, - bool? showCursor, - this.textCapitalization = TextCapitalization.none, - this.maxHeight, - this.minHeight, - this.maxContentWidth, - this.customStyles, - this.customShortcuts, - this.customActions, - this.expands = false, - this.autoFocus = false, - this.enableUnfocusOnTapOutside = true, - this.keyboardAppearance = Brightness.light, - this.enableInteractiveSelection = true, - this.scrollPhysics, - this.linkActionPickerDelegate = defaultLinkActionPickerDelegate, - this.customStyleBuilder, - this.floatingCursorDisabled = false, - this.onImagePaste, - this.customLinkPrefixes = const []}) - : assert(maxHeight == null || maxHeight > 0, 'maxHeight cannot be null'), + const RawEditor({ + required this.controller, + required this.focusNode, + required this.scrollController, + required this.scrollBottomInset, + required this.cursorStyle, + required this.selectionColor, + required this.selectionCtrls, + required this.embedBuilder, + Key? key, + this.scrollable = true, + this.padding = EdgeInsets.zero, + this.readOnly = false, + this.placeholder, + this.onLaunchUrl, + this.contextMenuBuilder = defaultContextMenuBuilder, + this.showSelectionHandles = false, + bool? showCursor, + this.textCapitalization = TextCapitalization.none, + this.maxHeight, + this.minHeight, + this.maxContentWidth, + this.customStyles, + this.customShortcuts, + this.customActions, + this.expands = false, + this.autoFocus = false, + this.enableUnfocusOnTapOutside = true, + this.keyboardAppearance = Brightness.light, + this.enableInteractiveSelection = true, + this.scrollPhysics, + this.linkActionPickerDelegate = defaultLinkActionPickerDelegate, + this.customStyleBuilder, + this.customRecognizerBuilder, + this.floatingCursorDisabled = false, + this.onImagePaste, + this.customLinkPrefixes = const [], + this.dialogTheme, + }) : assert(maxHeight == null || maxHeight > 0, 'maxHeight cannot be null'), assert(minHeight == null || minHeight >= 0, 'minHeight cannot be null'), assert(maxHeight == null || minHeight == null || maxHeight >= minHeight, 'maxHeight cannot be null'), @@ -190,6 +193,7 @@ class RawEditor extends StatefulWidget { /// horizontally centered. This is mostly useful on devices with wide screens. final double? maxContentWidth; + /// Allows to override [DefaultStyles]. final DefaultStyles? customStyles; /// Whether this widget's height will be sized to fill its parent. @@ -245,16 +249,27 @@ class RawEditor extends StatefulWidget { final Future Function(Uint8List imageBytes)? onImagePaste; - final Map? customShortcuts; + /// Contains user-defined shortcuts map. + /// + /// [https://docs.flutter.dev/development/ui/advanced/actions-and-shortcuts#shortcuts] + final Map? customShortcuts; + + /// Contains user-defined actions. + /// + /// [https://docs.flutter.dev/development/ui/advanced/actions-and-shortcuts#actions] final Map>? customActions; /// Builder function for embeddable objects. final EmbedsBuilder embedBuilder; final LinkActionPickerDelegate linkActionPickerDelegate; final CustomStyleBuilder? customStyleBuilder; + final CustomRecognizerBuilder? customRecognizerBuilder; final bool floatingCursorDisabled; final List customLinkPrefixes; + /// Configures the dialog theme. + final QuillDialogTheme? dialogTheme; + @override State createState() => RawEditorState(); } @@ -495,78 +510,148 @@ class RawEditorState extends EditorState minHeight: widget.minHeight ?? 0.0, maxHeight: widget.maxHeight ?? double.infinity); + final isMacOS = Theme.of(context).platform == TargetPlatform.macOS; + return TextFieldTapRegion( enabled: widget.enableUnfocusOnTapOutside, onTapOutside: _defaultOnTapOutside, child: QuillStyles( data: _styles!, child: Shortcuts( - shortcuts: { + shortcuts: mergeMaps({ // shortcuts added for Desktop platforms. - LogicalKeySet(LogicalKeyboardKey.escape): - const HideSelectionToolbarIntent(), - LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyZ): - const UndoTextIntent(SelectionChangedCause.keyboard), - LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyY): - const RedoTextIntent(SelectionChangedCause.keyboard), + const SingleActivator( + LogicalKeyboardKey.escape, + ): const HideSelectionToolbarIntent(), + SingleActivator( + LogicalKeyboardKey.keyZ, + control: !isMacOS, + meta: isMacOS, + ): const UndoTextIntent(SelectionChangedCause.keyboard), + SingleActivator( + LogicalKeyboardKey.keyY, + control: !isMacOS, + meta: isMacOS, + ): const RedoTextIntent(SelectionChangedCause.keyboard), // Selection formatting. - LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyB): - const ToggleTextStyleIntent(Attribute.bold), - LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyU): - const ToggleTextStyleIntent(Attribute.underline), - LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyI): - const ToggleTextStyleIntent(Attribute.italic), - LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift, - LogicalKeyboardKey.keyS): - const ToggleTextStyleIntent(Attribute.strikeThrough), - LogicalKeySet( - LogicalKeyboardKey.control, LogicalKeyboardKey.backquote): - const ToggleTextStyleIntent(Attribute.inlineCode), - LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyL): - const ToggleTextStyleIntent(Attribute.ul), - LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyO): - const ToggleTextStyleIntent(Attribute.ol), - LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift, - LogicalKeyboardKey.keyB): - const ToggleTextStyleIntent(Attribute.blockQuote), - LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift, - LogicalKeyboardKey.tilde): - const ToggleTextStyleIntent(Attribute.codeBlock), - // Indent - LogicalKeySet(LogicalKeyboardKey.control, - LogicalKeyboardKey.bracketRight): - const IndentSelectionIntent(true), - LogicalKeySet( - LogicalKeyboardKey.control, LogicalKeyboardKey.bracketLeft): - const IndentSelectionIntent(false), - - LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyF): - const OpenSearchIntent(), - - LogicalKeySet( - LogicalKeyboardKey.control, LogicalKeyboardKey.digit1): - const ApplyHeaderIntent(Attribute.h1), - LogicalKeySet( - LogicalKeyboardKey.control, LogicalKeyboardKey.digit2): - const ApplyHeaderIntent(Attribute.h2), - LogicalKeySet( - LogicalKeyboardKey.control, LogicalKeyboardKey.digit3): - const ApplyHeaderIntent(Attribute.h3), - LogicalKeySet( - LogicalKeyboardKey.control, LogicalKeyboardKey.digit0): - const ApplyHeaderIntent(Attribute.header), - - LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift, - LogicalKeyboardKey.keyL): const ApplyCheckListIntent(), - - if (widget.customShortcuts != null) ...widget.customShortcuts!, - }, + SingleActivator( + LogicalKeyboardKey.keyB, + control: !isMacOS, + meta: isMacOS, + ): const ToggleTextStyleIntent(Attribute.bold), + SingleActivator( + LogicalKeyboardKey.keyU, + control: !isMacOS, + meta: isMacOS, + ): const ToggleTextStyleIntent(Attribute.underline), + SingleActivator( + LogicalKeyboardKey.keyI, + control: !isMacOS, + meta: isMacOS, + ): const ToggleTextStyleIntent(Attribute.italic), + SingleActivator( + LogicalKeyboardKey.keyS, + control: !isMacOS, + meta: isMacOS, + shift: true, + ): const ToggleTextStyleIntent(Attribute.strikeThrough), + SingleActivator( + LogicalKeyboardKey.backquote, + control: !isMacOS, + meta: isMacOS, + ): const ToggleTextStyleIntent(Attribute.inlineCode), + SingleActivator( + LogicalKeyboardKey.tilde, + control: !isMacOS, + meta: isMacOS, + shift: true, + ): const ToggleTextStyleIntent(Attribute.codeBlock), + SingleActivator( + LogicalKeyboardKey.keyB, + control: !isMacOS, + meta: isMacOS, + shift: true, + ): const ToggleTextStyleIntent(Attribute.blockQuote), + SingleActivator( + LogicalKeyboardKey.keyK, + control: !isMacOS, + meta: isMacOS, + ): const ApplyLinkIntent(), + + // Lists + SingleActivator( + LogicalKeyboardKey.keyL, + control: !isMacOS, + meta: isMacOS, + shift: true, + ): const ToggleTextStyleIntent(Attribute.ul), + SingleActivator( + LogicalKeyboardKey.keyO, + control: !isMacOS, + meta: isMacOS, + shift: true, + ): const ToggleTextStyleIntent(Attribute.ol), + SingleActivator( + LogicalKeyboardKey.keyC, + control: !isMacOS, + meta: isMacOS, + shift: true, + ): const ApplyCheckListIntent(), + + // Indents + SingleActivator( + LogicalKeyboardKey.keyM, + control: !isMacOS, + meta: isMacOS, + ): const IndentSelectionIntent(true), + SingleActivator( + LogicalKeyboardKey.keyM, + control: !isMacOS, + meta: isMacOS, + shift: true, + ): const IndentSelectionIntent(false), + + // Headers + SingleActivator( + LogicalKeyboardKey.digit1, + control: !isMacOS, + meta: isMacOS, + ): const ApplyHeaderIntent(Attribute.h1), + SingleActivator( + LogicalKeyboardKey.digit2, + control: !isMacOS, + meta: isMacOS, + ): const ApplyHeaderIntent(Attribute.h2), + SingleActivator( + LogicalKeyboardKey.digit3, + control: !isMacOS, + meta: isMacOS, + ): const ApplyHeaderIntent(Attribute.h3), + SingleActivator( + LogicalKeyboardKey.digit0, + control: !isMacOS, + meta: isMacOS, + ): const ApplyHeaderIntent(Attribute.header), + + SingleActivator( + LogicalKeyboardKey.keyG, + control: !isMacOS, + meta: isMacOS, + ): const InsertEmbedIntent(Attribute.image), + + SingleActivator( + LogicalKeyboardKey.keyF, + control: !isMacOS, + meta: isMacOS, + ): const OpenSearchIntent(), + }, { + ...?widget.customShortcuts + }), child: Actions( - actions: { - ..._actions, - if (widget.customActions != null) ...widget.customActions!, - }, + actions: mergeMaps>(_actions, { + ...?widget.customActions, + }), child: Focus( focusNode: widget.focusNode, onKey: _onKey, @@ -592,17 +677,16 @@ class RawEditorState extends EditorState if (event is! RawKeyDownEvent) { return KeyEventResult.ignored; } + // Handle indenting blocks when pressing the tab key. + if (event.logicalKey == LogicalKeyboardKey.tab) { + return _handleTabKey(event); + } // Don't handle key if there is an active selection. if (controller.selection.baseOffset != controller.selection.extentOffset) { return KeyEventResult.ignored; } - // Handle indenting blocks when pressing the tab key. - if (event.logicalKey == LogicalKeyboardKey.tab) { - return _handleTabKey(event); - } - // Handle inserting lists when space is pressed following // a list initiating phrase. if (event.logicalKey == LogicalKeyboardKey.space) { @@ -653,6 +737,19 @@ class RawEditorState extends EditorState return KeyEventResult.handled; } + if (controller.selection.baseOffset != controller.selection.extentOffset) { + if (child.node == null || child.node!.parent == null) { + return KeyEventResult.handled; + } + final parentBlock = child.node!.parent!; + if (parentBlock.style.containsKey(Attribute.ol.key) || + parentBlock.style.containsKey(Attribute.ul.key) || + parentBlock.style.containsKey(Attribute.checked.key)) { + controller.indentSelection(!event.isShiftPressed); + } + return KeyEventResult.handled; + } + if (child.node == null) { return insertTabCharacter(); } @@ -830,6 +927,7 @@ class RawEditorState extends EditorState textDirection: _textDirection, embedBuilder: widget.embedBuilder, customStyleBuilder: widget.customStyleBuilder, + customRecognizerBuilder: widget.customRecognizerBuilder, styles: _styles!, readOnly: widget.readOnly, controller: controller, @@ -1570,11 +1668,13 @@ class RawEditorState extends EditorState RedoTextIntent: _makeOverridable(_RedoKeyboardAction(this)), OpenSearchIntent: _openSearchAction, + // Selection Formatting ToggleTextStyleIntent: _formatSelectionAction, IndentSelectionIntent: _indentSelectionAction, ApplyHeaderIntent: _applyHeaderAction, ApplyCheckListIntent: _applyCheckListAction, + ApplyLinkIntent: ApplyLinkAction(this) }; @override @@ -2490,6 +2590,43 @@ class _ApplyCheckListAction extends Action { bool get isActionEnabled => true; } +class ApplyLinkIntent extends Intent { + const ApplyLinkIntent(); +} + +class ApplyLinkAction extends Action { + ApplyLinkAction(this.state); + + final RawEditorState state; + + @override + Object? invoke(ApplyLinkIntent intent) async { + final initialTextLink = QuillTextLink.prepare(state.controller); + + final textLink = await showDialog( + context: state.context, + builder: (context) { + return LinkStyleDialog( + text: initialTextLink.text, + link: initialTextLink.link, + dialogTheme: state.widget.dialogTheme, + ); + }, + ); + + if (textLink != null) { + textLink.submit(state.controller); + } + return null; + } +} + +class InsertEmbedIntent extends Intent { + const InsertEmbedIntent(this.type); + + final Attribute type; +} + /// Signature for a widget builder that builds a context menu for the given /// [RawEditorState]. /// diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index 22623f4d..f4dbc899 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -41,6 +41,7 @@ class TextLine extends StatefulWidget { required this.linkActionPicker, this.textDirection, this.customStyleBuilder, + this.customRecognizerBuilder, this.customLinkPrefixes = const [], Key? key, }) : super(key: key); @@ -52,6 +53,7 @@ class TextLine extends StatefulWidget { final bool readOnly; final QuillController controller; final CustomStyleBuilder? customStyleBuilder; + final CustomRecognizerBuilder? customRecognizerBuilder; final ValueChanged? onLaunchUrl; final LinkActionPicker linkActionPicker; final List customLinkPrefixes; @@ -313,12 +315,14 @@ class _TextLineState extends State { final isLink = nodeStyle.containsKey(Attribute.link.key) && nodeStyle.attributes[Attribute.link.key]!.value != null; + final recognizer = _getRecognizer(node, isLink); + return TextSpan( text: textNode.value, style: _getInlineTextStyle( textNode, defaultStyles, nodeStyle, lineStyle, isLink), - recognizer: isLink && canLaunchLinks ? _getRecognizer(node) : null, - mouseCursor: isLink && canLaunchLinks ? SystemMouseCursors.click : null, + recognizer: recognizer, + mouseCursor: (recognizer != null) ? SystemMouseCursors.click : null, ); } @@ -352,6 +356,14 @@ class _TextLineState extends State { } }); + if (nodeStyle.containsKey(Attribute.script.key)) { + if (nodeStyle.attributes.values.contains(Attribute.subscript)) { + res = _merge(res, defaultStyles.subscript!); + } else if (nodeStyle.attributes.values.contains(Attribute.superscript)) { + res = _merge(res, defaultStyles.superscript!); + } + } + if (nodeStyle.containsKey(Attribute.inlineCode.key)) { res = _merge(res, defaultStyles.inlineCode!.styleFor(lineStyle)); } @@ -398,19 +410,38 @@ class _TextLineState extends State { return res; } - GestureRecognizer _getRecognizer(Node segment) { + GestureRecognizer? _getRecognizer(Node segment, bool isLink) { if (_linkRecognizers.containsKey(segment)) { return _linkRecognizers[segment]!; } - if (isDesktop() || widget.readOnly) { - _linkRecognizers[segment] = TapGestureRecognizer() - ..onTap = () => _tapNodeLink(segment); - } else { - _linkRecognizers[segment] = LongPressGestureRecognizer() - ..onLongPress = () => _longPressLink(segment); + if (widget.customRecognizerBuilder != null) { + final textNode = segment as leaf.Text; + final nodeStyle = textNode.style; + + nodeStyle.attributes.forEach((key, value) { + final recognizer = widget.customRecognizerBuilder!.call(value, segment); + if (recognizer != null) { + _linkRecognizers[segment] = recognizer; + return; + } + }); + } + + if (_linkRecognizers.containsKey(segment)) { + return _linkRecognizers[segment]!; + } + + if (isLink && canLaunchLinks) { + if (isDesktop() || widget.readOnly) { + _linkRecognizers[segment] = TapGestureRecognizer() + ..onTap = () => _tapNodeLink(segment); + } else { + _linkRecognizers[segment] = LongPressGestureRecognizer() + ..onLongPress = () => _longPressLink(segment); + } } - return _linkRecognizers[segment]!; + return _linkRecognizers[segment]; } Future _launchUrl(String url) async { diff --git a/lib/src/widgets/toolbar.dart b/lib/src/widgets/toolbar.dart index d5a5c93a..e4da8149 100644 --- a/lib/src/widgets/toolbar.dart +++ b/lib/src/widgets/toolbar.dart @@ -29,6 +29,7 @@ export 'toolbar/color_button.dart'; export 'toolbar/history_button.dart'; export 'toolbar/indent_button.dart'; export 'toolbar/link_style_button.dart'; +export 'toolbar/link_style_button2.dart'; export 'toolbar/quill_font_family_button.dart'; export 'toolbar/quill_font_size_button.dart'; export 'toolbar/quill_icon_button.dart'; @@ -102,6 +103,8 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { bool showRedo = true, bool showDirection = false, bool showSearchButton = true, + bool showSubscript = true, + bool showSuperscript = true, List customButtons = const [], ///Map of font sizes in string @@ -207,6 +210,8 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { ToolbarButtons.fontFamily: 'Font family'.i18n, ToolbarButtons.fontSize: 'Font size'.i18n, ToolbarButtons.bold: 'Bold'.i18n, + ToolbarButtons.subscript: 'Subscript'.i18n, + ToolbarButtons.superscript: 'Superscript'.i18n, ToolbarButtons.italic: 'Italic'.i18n, ToolbarButtons.small: 'Small'.i18n, ToolbarButtons.underline: 'Underline'.i18n, @@ -295,6 +300,26 @@ class QuillToolbar extends StatelessWidget implements PreferredSizeWidget { iconTheme: iconTheme, afterButtonPressed: afterButtonPressed, ), + if (showSubscript) + ToggleStyleButton( + attribute: Attribute.subscript, + icon: Icons.subscript, + iconSize: toolbarIconSize, + tooltip: buttonTooltips[ToolbarButtons.subscript], + controller: controller, + iconTheme: iconTheme, + afterButtonPressed: afterButtonPressed, + ), + if (showSuperscript) + ToggleStyleButton( + attribute: Attribute.superscript, + icon: Icons.superscript, + iconSize: toolbarIconSize, + tooltip: buttonTooltips[ToolbarButtons.superscript], + controller: controller, + iconTheme: iconTheme, + afterButtonPressed: afterButtonPressed, + ), if (showItalicButton) ToggleStyleButton( attribute: Attribute.italic, diff --git a/lib/src/widgets/toolbar/enum.dart b/lib/src/widgets/toolbar/enum.dart index 197bea56..e6719e50 100644 --- a/lib/src/widgets/toolbar/enum.dart +++ b/lib/src/widgets/toolbar/enum.dart @@ -4,6 +4,8 @@ enum ToolbarButtons { fontFamily, fontSize, bold, + subscript, + superscript, italic, small, underline, diff --git a/lib/src/widgets/toolbar/link_style_button2.dart b/lib/src/widgets/toolbar/link_style_button2.dart new file mode 100644 index 00000000..42aaff11 --- /dev/null +++ b/lib/src/widgets/toolbar/link_style_button2.dart @@ -0,0 +1,446 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:url_launcher/link.dart'; + +import '../../../extensions.dart'; +import '../../../translations.dart'; +import '../../models/documents/attribute.dart'; +import '../../models/themes/quill_dialog_theme.dart'; +import '../../models/themes/quill_icon_theme.dart'; +import '../controller.dart'; +import '../link.dart'; +import '../toolbar.dart'; + +/// Alternative version of [LinkStyleButton]. This widget has more customization +/// and uses dialog similar to one which is used on [http://quilljs.com]. +class LinkStyleButton2 extends StatefulWidget { + const LinkStyleButton2({ + required this.controller, + this.icon, + this.iconSize = kDefaultIconSize, + this.iconTheme, + this.dialogTheme, + this.afterButtonPressed, + this.tooltip, + this.constraints, + this.addLinkLabel, + this.editLinkLabel, + this.linkColor, + this.childrenSpacing = 16.0, + this.autovalidateMode = AutovalidateMode.disabled, + this.validationMessage, + this.buttonSize, + Key? key, + }) : assert(addLinkLabel == null || addLinkLabel.length > 0), + assert(editLinkLabel == null || editLinkLabel.length > 0), + assert(childrenSpacing > 0), + assert(validationMessage == null || validationMessage.length > 0), + super(key: key); + + final QuillController controller; + final IconData? icon; + final double iconSize; + final QuillIconTheme? iconTheme; + final QuillDialogTheme? dialogTheme; + final VoidCallback? afterButtonPressed; + final String? tooltip; + + /// The constrains for dialog. + final BoxConstraints? constraints; + + /// The text of label in link add mode. + final String? addLinkLabel; + + /// The text of label in link edit mode. + final String? editLinkLabel; + + /// The color of URL. + final Color? linkColor; + + /// The margin between child widgets in the dialog. + final double childrenSpacing; + + final AutovalidateMode autovalidateMode; + final String? validationMessage; + + /// The size of dialog buttons. + final Size? buttonSize; + + @override + State createState() => _LinkStyleButton2State(); +} + +class _LinkStyleButton2State extends State { + @override + void dispose() { + super.dispose(); + widget.controller.removeListener(_didChangeSelection); + } + + @override + void initState() { + super.initState(); + widget.controller.addListener(_didChangeSelection); + } + + @override + void didUpdateWidget(covariant LinkStyleButton2 oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.controller != widget.controller) { + oldWidget.controller.removeListener(_didChangeSelection); + widget.controller.addListener(_didChangeSelection); + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isToggled = _getLinkAttributeValue() != null; + return QuillIconButton( + tooltip: widget.tooltip, + highlightElevation: 0, + hoverElevation: 0, + size: widget.iconSize * kIconButtonFactor, + icon: Icon( + widget.icon ?? Icons.link, + size: widget.iconSize, + color: isToggled + ? (widget.iconTheme?.iconSelectedColor ?? + theme.primaryIconTheme.color) + : (widget.iconTheme?.iconUnselectedColor ?? theme.iconTheme.color), + ), + fillColor: isToggled + ? (widget.iconTheme?.iconSelectedFillColor ?? + Theme.of(context).primaryColor) + : (widget.iconTheme?.iconUnselectedFillColor ?? theme.canvasColor), + borderRadius: widget.iconTheme?.borderRadius ?? 2, + onPressed: _openLinkDialog, + afterPressed: widget.afterButtonPressed, + ); + } + + Future _openLinkDialog() async { + final initialTextLink = QuillTextLink.prepare(widget.controller); + + final textLink = await showDialog( + context: context, + builder: (_) => LinkStyleDialog( + dialogTheme: widget.dialogTheme, + text: initialTextLink.text, + link: initialTextLink.link, + constraints: widget.constraints, + addLinkLabel: widget.addLinkLabel, + editLinkLabel: widget.editLinkLabel, + linkColor: widget.linkColor, + childrenSpacing: widget.childrenSpacing, + autovalidateMode: widget.autovalidateMode, + validationMessage: widget.validationMessage, + buttonSize: widget.buttonSize, + ), + ); + + if (textLink != null) { + textLink.submit(widget.controller); + } + } + + String? _getLinkAttributeValue() { + return widget.controller + .getSelectionStyle() + .attributes[Attribute.link.key] + ?.value; + } + + void _didChangeSelection() { + setState(() {}); + } +} + +class LinkStyleDialog extends StatefulWidget { + const LinkStyleDialog({ + Key? key, + this.text, + this.link, + this.dialogTheme, + this.constraints, + this.contentPadding = + const EdgeInsets.symmetric(vertical: 16, horizontal: 16), + this.addLinkLabel, + this.editLinkLabel, + this.linkColor, + this.childrenSpacing = 16.0, + this.autovalidateMode = AutovalidateMode.disabled, + this.validationMessage, + this.buttonSize, + }) : assert(addLinkLabel == null || addLinkLabel.length > 0), + assert(editLinkLabel == null || editLinkLabel.length > 0), + assert(childrenSpacing > 0), + assert(validationMessage == null || validationMessage.length > 0), + super(key: key); + + final String? text; + final String? link; + final QuillDialogTheme? dialogTheme; + + /// The constrains for dialog. + final BoxConstraints? constraints; + + /// The padding for content of dialog. + final EdgeInsetsGeometry contentPadding; + + /// The text of label in link add mode. + final String? addLinkLabel; + + /// The text of label in link edit mode. + final String? editLinkLabel; + + /// The color of URL. + final Color? linkColor; + + /// The margin between child widgets in the dialog. + final double childrenSpacing; + + final AutovalidateMode autovalidateMode; + final String? validationMessage; + + /// The size of dialog buttons. + final Size? buttonSize; + + @override + State createState() => _LinkStyleDialogState(); +} + +class _LinkStyleDialogState extends State { + late final TextEditingController _linkController; + + late String _link; + late String _text; + + late bool _isEditMode; + + @override + void dispose() { + _linkController.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + _link = widget.link ?? ''; + _text = widget.text ?? ''; + _isEditMode = _link.isNotEmpty; + _linkController = TextEditingController.fromValue( + TextEditingValue( + text: _isEditMode ? _link : '', + selection: _isEditMode + ? TextSelection(baseOffset: 0, extentOffset: _link.length) + : const TextSelection.collapsed(offset: 0), + ), + ); + } + + @override + Widget build(BuildContext context) { + final constraints = widget.constraints ?? + widget.dialogTheme?.linkDialogConstraints ?? + () { + final mediaQuery = MediaQuery.of(context); + final maxWidth = + kIsWeb ? mediaQuery.size.width / 4 : mediaQuery.size.width - 80; + return BoxConstraints(maxWidth: maxWidth, maxHeight: 80); + }(); + + final buttonStyle = widget.buttonSize != null + ? Theme.of(context) + .elevatedButtonTheme + .style + ?.copyWith(fixedSize: MaterialStatePropertyAll(widget.buttonSize)) + : widget.dialogTheme?.buttonStyle; + + final isWrappable = widget.dialogTheme?.isWrappable ?? false; + + final children = _isEditMode + ? [ + Text(widget.editLinkLabel ?? 'Visit link'.i18n), + UtilityWidgets.maybeWidget( + enabled: !isWrappable, + wrapper: (child) => Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: child, + ), + ), + child: Padding( + padding: + EdgeInsets.symmetric(horizontal: widget.childrenSpacing), + child: Link( + uri: Uri.parse(_linkController.text), + builder: (context, followLink) { + return TextButton( + onPressed: followLink, + style: TextButton.styleFrom( + backgroundColor: Colors.transparent, + ), + child: Text( + widget.link!, + textAlign: TextAlign.left, + overflow: TextOverflow.ellipsis, + style: widget.dialogTheme?.inputTextStyle?.copyWith( + color: widget.linkColor ?? Colors.blue, + decoration: TextDecoration.underline, + ), + ), + ); + }, + ), + ), + ), + ElevatedButton( + onPressed: () { + setState(() { + _isEditMode = !_isEditMode; + }); + }, + style: buttonStyle, + child: Text('Edit'.i18n), + ), + Padding( + padding: EdgeInsets.only(left: widget.childrenSpacing), + child: ElevatedButton( + onPressed: _removeLink, + style: buttonStyle, + child: Text('Remove'.i18n), + ), + ), + ] + : [ + Text(widget.addLinkLabel ?? 'Enter link'.i18n), + UtilityWidgets.maybeWidget( + enabled: !isWrappable, + wrapper: (child) => Expanded( + child: child, + ), + child: Padding( + padding: + EdgeInsets.symmetric(horizontal: widget.childrenSpacing), + child: TextFormField( + controller: _linkController, + style: widget.dialogTheme?.inputTextStyle, + keyboardType: TextInputType.url, + textInputAction: TextInputAction.done, + decoration: InputDecoration( + labelStyle: widget.dialogTheme?.labelTextStyle, + ), + autofocus: true, + autovalidateMode: widget.autovalidateMode, + validator: _validateLink, + onChanged: _linkChanged, + ), + ), + ), + ElevatedButton( + onPressed: _canPress() ? _applyLink : null, + style: buttonStyle, + child: Text('Apply'.i18n), + ), + ]; + + return Dialog( + backgroundColor: widget.dialogTheme?.dialogBackgroundColor, + shape: widget.dialogTheme?.shape ?? + DialogTheme.of(context).shape ?? + RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + child: ConstrainedBox( + constraints: constraints, + child: Padding( + padding: widget.contentPadding, + child: isWrappable + ? Wrap( + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + runSpacing: widget.dialogTheme?.runSpacing ?? 0.0, + children: children, + ) + : Row( + children: children, + ), + ), + ), + ); + } + + void _linkChanged(String value) { + setState(() { + _link = value; + }); + } + + bool _canPress() => _validateLink(_link) == null; + + String? _validateLink(String? value) { + if ((value?.isEmpty ?? false) || + !AutoFormatMultipleLinksRule.linkRegExp.hasMatch(value!)) { + return widget.validationMessage ?? 'That is not a valid URL'; + } + + return null; + } + + void _applyLink() => + Navigator.pop(context, QuillTextLink(_text.trim(), _link.trim())); + + void _removeLink() => + Navigator.pop(context, QuillTextLink(_text.trim(), null)); +} + +/// Contains information about text URL. +class QuillTextLink { + QuillTextLink( + this.text, + this.link, + ); + + final String text; + final String? link; + + static QuillTextLink prepare(QuillController controller) { + final link = + controller.getSelectionStyle().attributes[Attribute.link.key]?.value; + final index = controller.selection.start; + + var text; + if (link != null) { + // text should be the link's corresponding text, not selection + final leaf = controller.document.querySegmentLeafNode(index).leaf; + if (leaf != null) { + text = leaf.toPlainText(); + } + } + + final len = controller.selection.end - index; + text ??= len == 0 ? '' : controller.document.getPlainText(index, len); + + return QuillTextLink(text, link); + } + + void submit(QuillController controller) { + var index = controller.selection.start; + var length = controller.selection.end - index; + final linkValue = + controller.getSelectionStyle().attributes[Attribute.link.key]?.value; + + if (linkValue != null) { + // text should be the link's corresponding text, not selection + final leaf = controller.document.querySegmentLeafNode(index).leaf; + if (leaf != null) { + final range = getLinkRange(leaf); + index = range.start; + length = range.end - range.start; + } + } + controller + ..replaceText(index, length, text, null) + ..formatText(index, text.length, LinkAttribute(link)); + } +} diff --git a/lib/src/widgets/toolbar/toggle_style_button.dart b/lib/src/widgets/toolbar/toggle_style_button.dart index 1eb0eb57..53099743 100644 --- a/lib/src/widgets/toolbar/toggle_style_button.dart +++ b/lib/src/widgets/toolbar/toggle_style_button.dart @@ -105,7 +105,8 @@ class _ToggleStyleButtonState extends State { } bool _getIsToggled(Map attrs) { - if (widget.attribute.key == Attribute.list.key) { + if (widget.attribute.key == Attribute.list.key || + widget.attribute.key == Attribute.script.key) { final attribute = attrs[widget.attribute.key]; if (attribute == null) { return false; diff --git a/pubspec.yaml b/pubspec.yaml index 38a3d504..43a4ec74 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,12 +1,12 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 7.1.8 +version: 7.1.14 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.17.0 <3.0.0" flutter: ">=3.0.0" dependencies: