├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── code_editor.iml ├── example └── main.dart ├── lib ├── EditorModel.dart ├── EditorModelStyleOptions.dart ├── FileEditor.dart ├── Theme.dart ├── ToolButton.dart ├── code_editor.dart └── formatters │ └── html.dart ├── pubspec.lock ├── pubspec.yaml └── test ├── html.dart └── test_api.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | .vscode/ 12 | 13 | # IntelliJ related 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | build/ 32 | 33 | # Android related 34 | **/android/**/gradle-wrapper.jar 35 | **/android/.gradle 36 | **/android/captures/ 37 | **/android/gradlew 38 | **/android/gradlew.bat 39 | **/android/local.properties 40 | **/android/**/GeneratedPluginRegistrant.java 41 | 42 | # iOS/XCode related 43 | **/ios/**/*.mode1v3 44 | **/ios/**/*.mode2v3 45 | **/ios/**/*.moved-aside 46 | **/ios/**/*.pbxuser 47 | **/ios/**/*.perspectivev3 48 | **/ios/**/*sync/ 49 | **/ios/**/.sconsign.dblite 50 | **/ios/**/.tags* 51 | **/ios/**/.vagrant/ 52 | **/ios/**/DerivedData/ 53 | **/ios/**/Icon? 54 | **/ios/**/Pods/ 55 | **/ios/**/.symlinks/ 56 | **/ios/**/profile 57 | **/ios/**/xcuserdata 58 | **/ios/.generated/ 59 | **/ios/Flutter/App.framework 60 | **/ios/Flutter/Flutter.framework 61 | **/ios/Flutter/Flutter.podspec 62 | **/ios/Flutter/Generated.xcconfig 63 | **/ios/Flutter/app.flx 64 | **/ios/Flutter/app.zip 65 | **/ios/Flutter/flutter_assets/ 66 | **/ios/Flutter/flutter_export_environment.sh 67 | **/ios/ServiceDefinitions.json 68 | **/ios/Runner/GeneratedPluginRegistrant.* 69 | 70 | # Exceptions to above rules. 71 | !**/ios/**/default.mode1v3 72 | !**/ios/**/default.mode2v3 73 | !**/ios/**/default.pbxuser 74 | !**/ios/**/default.perspectivev3 75 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 76 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 8af6b2f038c1172e61d418869363a28dffec3cb4 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.1.0] - August 7, 2023 2 | 3 | + Fix `reverseEditAndUndoRedoButtons` so that the "undo" button is always at the right of the "redo" button. 4 | + Fix the example of the package and improve the README so that users don't make the mistake of declaring the instance of `EditorModel` in the `build` function in `Stateful` widgets. 5 | + Add a new option in `EditorModelStyleOptions` named "removeFocusOfTextFieldOnTapOutside", whose default value is `true`. 6 | 7 | ## [2.0.2] - July 31, 2023 8 | 9 | + Fix a lot of typo 10 | + Fix a glitch (the text field would not have the same value as the current file) 11 | + Add important note on README 12 | 13 | ## [2.0.0] - July 31, 2023 14 | 15 | + Huge refactoring of the code base 16 | + Add a lot of **breaking changes** 17 | + Add a new way to set individual files as `readonly` 18 | + Add undo/redo buttons 19 | + Add formatters (the editor is able to auto-format HTML code when the user saves modifications) 20 | + Add custom formatters (the dev can manipulate the new value of the TextField before saving it) 21 | + Change signature of `onSubmit` callback. 22 | + Improve comments for a better documentation in the dev's code directly 23 | + Set as compatible with dart 3.0 24 | + Now works with SDK `>=2.12.0` up to `<4.0.0` 25 | + Improve null safety 26 | + Removed ability to pass in custom `TextEditingController` to `CodeEditor` 27 | 28 | ## [1.3.2] - July 24, 2022 29 | 30 | + Update dependencies 31 | + Fix dart format 32 | + Fix example 33 | 34 | ## [1.3.1] - August 28, 2021 35 | 36 | + Adds the ability to move the "edit" button at the top 37 | 38 | ## [1.3.0] - June 26, 2021 39 | 40 | + Ability to pass in custom `TextEditingController` to the `CodeEditor`. 41 | 42 | ## [1.2.0] - June 15, 2021 43 | 44 | + null-safety thanks to contributors 45 | + Updates the dependencies to their latest version. 46 | 47 | ## [1.1.1] - April 24, 2021 48 | 49 | + dartfmt 50 | + See [https://dart.dev/tools/dart-format]() 51 | 52 | ## [1.1.0] - April 24, 2021 53 | 54 | + Bug fixes. 55 | + Fix typo. 56 | + Better compatibility with the latest version of Flutter (the deprecated widgets have been removed). 57 | + Reorganisation of `EditorModelStyleOptions.dart`. 58 | + Updates the dependencies to their latest version. 59 | 60 | ## [1.0.1] - Aug 10, 2020 61 | 62 | Fix typo. 63 | 64 | ## [1.0.0] - Aug 10, 2020 65 | 66 | This is a big version ! 67 | Here are all the updates : 68 | 69 | + There was a bug before : you could'nt change the content of a file outside the CodeEditor Widget, I mean in a SetState() function. Now, it's possible. 70 | 71 | + You can choose to disable the navigation bar with the new parameter : disableNavigationbar (by default set to false). If you hide the navigation bar, only the first file will be displayed. 72 | 73 | + WARNING : if you move your project to this version, you will need to modify a little thing : EditorModel has no positional arguments anymore, so to define your files, you will have to name the parameter to "files". Check the Readme section. 74 | 75 | + Now, there is no required parameters anymore. Everything has a default value to bring more flexibility to developers. Check the Readme section for more info. 76 | 77 | + code_editor does not use Provider package anymore. 78 | 79 | + The Readme section is more readable and more accurate. 80 | 81 | Enjoy Coding ! 82 | 83 | ## [0.2.0] - Aug 7, 2020 84 | 85 | From now on, when the code overflows the editor, the scroll is better. 86 | 87 | + New parameter inside EditorModelStyleOptions : TextStyle textStyleOfTextField, the style of the text inside the text field. 88 | 89 | code_editor is much better since its initial release :) 90 | 91 | ## [0.1.3] - Aug 7, 2020 92 | 93 | The CodeEditor Widget doesn't have margin property anymore. 94 | 95 | ## [0.1.2] - Aug 6, 2020 96 | 97 | onSubmit parameter is not required anynome inside the CodeEditor Widget. 98 | 99 | + A new parameter : bool edit, by default its value is true. Set it to false if you want to disable file editing. Use it inside the CodeEditor Widget. 100 | 101 | + New parameter inside EditorModelStyleOptions : double fontSizeOfFilename, the font size of the files' names. 102 | 103 | + The documentation in the source code is much better ! 104 | 105 | ## [0.1.1] - Aug 1, 2020 106 | 107 | Minor changes 108 | 109 | ## [0.1.0] - Aug 1, 2020 110 | 111 | Complete change of the files implementation method : you can now define the name of the files in the navbar, define which language you want among dozens and define the content of each file. Moreover, you are no longer limited by a maximum number of files, you can put as many as you want! 112 | 113 | + Minor bug fixes. 114 | 115 | See all the available languages at : 116 | https://github.com/git-touch/highlight/tree/master/highlight/lib/languages 117 | 118 | ## [0.0.1] - Aug 1, 2020 119 | 120 | Initial release 121 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License (MIT) 2 | 3 | Copyright © 2022 ScienceSky.fr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # code_editor 2 | 3 | A code editor (dart, js, html, ...) for Flutter with syntax highlighting and custom theme. 4 | 5 | _This package is specially designed to make it easier to write code on mobile. It should work on other platforms as well, but that's not the goal. If you plan to build something just for the web/desktop, your users might suffer from the lack of features specific to those platforms._ 6 | 7 | List of supported languages: https://github.com/git-touch/highlight.dart/tree/master/highlight/lib/languages 8 | 9 | ## Description 10 | 11 | The editor displays the contents of fictitious "files" that correspond to instances of `FileEditor`. Each file has properties: its name, its content and the language this file uses. 12 | 13 | In other words, with this code editor, you can edit files which contain code. You can switch between the files in the navigation bar to edit their content with tools that make writing easier on phones. Once editing is complete, the code is highlighted according to the imposed theme (by default a custom one). 14 | You can choose your theme or create your own by checking at `import 'package:flutter_highlight/themes/github.dart';` 15 | 16 | ![example-1](https://learnweb.sciencesky.fr/code_editor_example-1.png) 17 | ![example-2](https://learnweb.sciencesky.fr/code_editor_example-2.png) 18 | ![example-3](https://learnweb.sciencesky.fr/code_editor_example-3.png) 19 | 20 | ## Installation 21 | 22 | It's very easy to install : 23 | 24 | * Add in the pubspec.yaml file 25 | 26 | ```yaml 27 | dependencies: 28 | code_editor: ^2.1.0 29 | ``` 30 | 31 | * Don't forget to update the modifications of the pubspec.yaml file 32 | 33 | ``` 34 | $ flutter pub get 35 | ``` 36 | 37 | * Finally, use code_editor in your flutter project 38 | 39 | ```dart 40 | import 'package:code_editor/code_editor.dart'; 41 | ``` 42 | 43 | ## Usage 44 | 45 | After importing the package into your project, you can initialize an `EditorModel` to control the editor. If you use a `Stateless` widget, then declare this code in the `build()` function: 46 | 47 | ```dart 48 | // example of a easier way to write code 49 | // instead of writing it in a single string 50 | List contentOfPage1 = [ 51 | "", 52 | "", 53 | "\t", 54 | "\t\tgo to page 2", 55 | "\t", 56 | "", 57 | ]; 58 | 59 | // The files displayed in the navigation bar of the editor. 60 | // There is no limit. 61 | // By default: 62 | // [name] = "file.${language ?? 'txt'}" 63 | // [language] = "text" 64 | // [code] = "" 65 | // [readonly] = false 66 | List files = [ 67 | FileEditor( 68 | name: "page1.html", 69 | language: "html", 70 | code: contentOfPage1.join("\n"), // [code] needs a string 71 | ), 72 | FileEditor( 73 | name: "page2.html", 74 | language: "html", 75 | code: "go back", 76 | readonly: true, // this file won't be editable 77 | ), 78 | FileEditor( 79 | name: "style.css", 80 | language: "css", 81 | code: "a { color: red; }", 82 | ), 83 | ]; 84 | 85 | // The model used by the CodeEditor widget, 86 | // you need it in order to control it. 87 | // But you can use `CodeEditor.empty()` if you don't want to use a model. 88 | EditorModel model = EditorModel( 89 | files: files, // the files created above 90 | // you can customize the editor as you want 91 | styleOptions: EditorModelStyleOptions( 92 | fontSize: 13, 93 | ), 94 | ); 95 | 96 | // /!\ important to use a `SingleChildScrollView` 97 | // because of the telephone keypad which might cause a 98 | // "RenderFlex overflowed by x pixels on the bottom" error 99 | return SingleChildScrollView( 100 | child: CodeEditor( 101 | model: model, // the model created above 102 | disableNavigationbar: false, // hide the navigation bar ? default is `false` 103 | // when the user confirms changes in one of the files: 104 | onSubmit: (String language, String value) { 105 | print("A file was changed."); 106 | }, 107 | // the html code will be auto-formatted 108 | // after any modification to an HTML file 109 | formatters: const ["html"], 110 | textModifier: (String language, String content) { 111 | print("A file is about to change"); 112 | 113 | // transform the code before it is saved 114 | // if you need to perform some operations on it 115 | // like your own auto-formatting for example 116 | return content; 117 | } 118 | ), 119 | ); 120 | ``` 121 | 122 | **However**, if you are using a `Stateful` widget, declaring the `model` in the `build()` function would cause the entire editor to go back to its initial state as soon as you update the state of something else in your widget. As a consequence, when using the `CodeEditor` in a `Stateful` widget, you want to declare the `model` in `initState()`: 123 | 124 | ```dart 125 | 126 | class _HomePageState extends State { 127 | late EditorModel model; 128 | 129 | @override 130 | void initState() { 131 | super.initState(); 132 | List contentOfPage1 = [ 133 | "", 134 | "", 135 | "\t", 136 | "\t\tgo to page 2", 137 | "\t", 138 | "", 139 | ]; 140 | 141 | List files = [ 142 | FileEditor( 143 | name: "page1.html", 144 | language: "html", 145 | code: contentOfPage1.join("\n"), 146 | ), 147 | ]; 148 | 149 | model = EditorModel( 150 | files: files, 151 | ); 152 | } 153 | 154 | @override 155 | Widget build(BuildContext context) { 156 | return Scaffold( 157 | appBar: AppBar(title: const Text("code_editor example")), 158 | // /!\ The SingleChildScrollView is important because of the phone's keypad which causes a "RenderFlex overflowed by x pixels on the bottom" error 159 | body: SingleChildScrollView( 160 | child: Column( 161 | children: [ 162 | CodeEditor( 163 | model: model, 164 | formatters: const ["html"], 165 | ), 166 | 167 | // Some code below that updates the state of the entire widget 168 | // ... 169 | ] 170 | ), 171 | ), 172 | ); 173 | } 174 | } 175 | ``` 176 | 177 | For the style options, you have a lot of possibilites : 178 | 179 | ```dart 180 | // The class, to show you what you can change. 181 | // To know what those options do exactly, 182 | // please refer to the comments in the code itself. 183 | class EditorModelStyleOptions { 184 | final EdgeInsets padding; 185 | final double heightOfContainer; 186 | final Map theme; 187 | final bool showUndoRedoButtons; 188 | final String fontFamily; 189 | final double? letterSpacing; 190 | final double fontSize; 191 | final double lineHeight; 192 | final int tabSize; 193 | final Color editorColor; 194 | final Color editorBorderColor; 195 | final Color editorFilenameColor; 196 | final Color editorToolButtonColor; 197 | final Color editorToolButtonTextColor; 198 | final double? fontSizeOfFilename; 199 | final TextStyle textStyleOfTextField; 200 | final Color editButtonBackgroundColor; 201 | final Color editButtonTextColor; 202 | final String editButtonName; 203 | final bool reverseEditAndUndoRedoButtons; 204 | final ToolbarOptions toolbarOptions; 205 | final bool placeCursorAtTheEndOnEdit; 206 | final bool removeFocusOfTextFieldOnTapOutside; 207 | } 208 | ``` 209 | 210 | Change the position of the edit button ("Edit") with : 211 | 212 | ```dart 213 | // inside EditorModelStyleOptions: 214 | styles.defineEditButtonPosition(bottom: 10, right: 15) // default values 215 | ``` 216 | 217 | Chain the calls like this if needed instead of creating a temporary variable: 218 | 219 | ```dart 220 | EditorModel model = EditorModel( 221 | files: files, 222 | styleOptions: EditorModelStyleOptions( 223 | showUndoRedoButtons: true, 224 | reverseEditAndUndoRedoButtons: true, 225 | )..defineEditButtonPosition( // yes with 2 dots 226 | bottom: 10, 227 | left: 15, 228 | ), 229 | ); 230 | ``` 231 | 232 | ## Internal dependencies 233 | 234 | code_editor uses the following dependencies to work: 235 | 1. flutter_highlight 236 | 2. font_awesome_flutter 237 | 238 | ## Contributing 239 | 240 | Do not hesitate to contribute to the project on GitHub :) 241 | 242 | ## License 243 | 244 | MIT License 245 | -------------------------------------------------------------------------------- /code_editor.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /example/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:code_editor/code_editor.dart'; 3 | 4 | void main() => runApp(const MyApp()); 5 | 6 | class MyApp extends StatelessWidget { 7 | const MyApp({Key? key}) : super(key: key); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return MaterialApp( 12 | title: 'Flutter Demo', 13 | theme: ThemeData( 14 | primarySwatch: Colors.blue, 15 | ), 16 | home: const HomePage(), 17 | ); 18 | } 19 | } 20 | 21 | class HomePage extends StatefulWidget { 22 | const HomePage({Key? key}) : super(key: key); 23 | 24 | @override 25 | State createState() => _HomePageState(); 26 | } 27 | 28 | class _HomePageState extends State { 29 | int count = 0; 30 | late EditorModel model; 31 | 32 | @override 33 | void initState() { 34 | super.initState(); 35 | // example of a easier way to write code instead of writing it in a single string 36 | List contentOfPage1 = [ 37 | "", 38 | "", 39 | "\t", 40 | "\t\tgo to page 2", 41 | "\t", 42 | "", 43 | ]; 44 | 45 | // The files displayed in the navigation bar of the editor. 46 | // You are not limited. 47 | // By default, [name] = "file.${language ?? 'txt'}", [language] = "text" and [code] = "", 48 | List files = [ 49 | FileEditor( 50 | name: "page1.html", 51 | language: "html", 52 | code: contentOfPage1.join("\n"), // [code] needs a string 53 | ), 54 | FileEditor( 55 | name: "page2.html", 56 | language: "html", 57 | code: "go back", 58 | readonly: true, // this file won't be editable 59 | ), 60 | FileEditor( 61 | name: "style.css", 62 | language: "css", 63 | code: "a { color: red; }", 64 | ), 65 | ]; 66 | 67 | // The model used by the CodeEditor widget, you need it in order to control it. 68 | // But, since 1.0.0, the model is not required inside the CodeEditor Widget. 69 | // To start without a model, you need to use `CodeEditor.empty()`. 70 | model = EditorModel( 71 | files: files, // the files created above 72 | // you can customize the editor as you want 73 | styleOptions: EditorModelStyleOptions( 74 | showUndoRedoButtons: true, 75 | reverseEditAndUndoRedoButtons: true, 76 | )..defineEditButtonPosition( 77 | bottom: 10, 78 | left: 15, 79 | ), 80 | ); 81 | } 82 | 83 | @override 84 | Widget build(BuildContext context) { 85 | return Scaffold( 86 | appBar: AppBar(title: const Text("code_editor example")), 87 | // /!\ The SingleChildScrollView is important because of the phone's keypad which causes a "RenderFlex overflowed by x pixels on the bottom" error 88 | body: SingleChildScrollView( 89 | child: Column( 90 | children: [ 91 | CodeEditor( 92 | model: model, 93 | formatters: const ["html"], 94 | ), 95 | 96 | // Just to show you how to add elements below the editor (or above), 97 | // here simple widgets you can remove. 98 | // It also shows how to manipulate state on the same page as an editor. 99 | 100 | ElevatedButton( 101 | onPressed: () { 102 | setState(() { 103 | count++; 104 | }); 105 | }, 106 | child: const Text("click"), 107 | ), 108 | Text("count = $count") 109 | ], 110 | ), 111 | ), 112 | ); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /lib/EditorModel.dart: -------------------------------------------------------------------------------- 1 | part of code_editor; 2 | 3 | /// Use the EditorModel into CodeEditor in order to control the files. 4 | class EditorModel extends ChangeNotifier { 5 | late EditorModelStyleOptions styleOptions; 6 | late List allFiles; 7 | 8 | int _currentPositionInFiles = 0; 9 | bool _isEditing = false; 10 | 11 | /// Define the required parameters for the editor to work properly. 12 | /// For that, you need to define [files] which is a `List`. 13 | /// 14 | /// You can also define your own preferences with [styleOptions] (instance of `EditorModelStyleOptions`). 15 | EditorModel({ 16 | List? files, 17 | EditorModelStyleOptions? styleOptions, 18 | }) { 19 | this.styleOptions = styleOptions ?? EditorModelStyleOptions(); 20 | this.allFiles = files ?? []; 21 | } 22 | 23 | /// Checks in all the given files if [language] is found, 24 | /// then returns a `List` of the files' content that uses [language]. 25 | List getCodeWithLanguage(String language) { 26 | List listOfCode = []; 27 | this.allFiles.forEach((FileEditor file) { 28 | if (file.language == language) { 29 | listOfCode.add(file.code); 30 | } 31 | }); 32 | return listOfCode; 33 | } 34 | 35 | /// Returns the code of the file where [index] corresponds. 36 | String getCodeWithIndex(int index) { 37 | return this.allFiles[index].code; 38 | } 39 | 40 | /// Returns the file where [index] corresponds. 41 | FileEditor? getFileWithIndex(int index) { 42 | if (index >= this.allFiles.length || index < 0) { 43 | return null; 44 | } 45 | return this.allFiles[index]; 46 | } 47 | 48 | /// Switch the file using [i] as index of the `List` files. 49 | /// 50 | /// The user can't change the file if he is editing another one. 51 | void changeIndexTo(int i) { 52 | if (this._isEditing) { 53 | return; 54 | } 55 | this._currentPositionInFiles = i; 56 | this.notify(); 57 | } 58 | 59 | /// Toggle the text field. 60 | void toggleEditing() { 61 | this._isEditing = !this._isEditing; 62 | this.notify(); 63 | } 64 | 65 | /// Overwrite the current code of the file where [index] corresponds by [newCode]. 66 | void updateCodeOfIndex(int index, String newCode) { 67 | this.allFiles[index].code = newCode; 68 | } 69 | 70 | void notify() => notifyListeners(); 71 | 72 | /// Gets the index of which file is currently displayed in the editor. 73 | int get position => this._currentPositionInFiles; 74 | 75 | /// Gets which language is currently shown. 76 | String get currentLanguage => this.allFiles[this._currentPositionInFiles].language; 77 | 78 | /// Is the text field shown? 79 | bool get isEditing => this._isEditing; 80 | 81 | /// Gets the number of files. 82 | int get numberOfFiles => this.allFiles.length; 83 | } 84 | -------------------------------------------------------------------------------- /lib/EditorModelStyleOptions.dart: -------------------------------------------------------------------------------- 1 | part of code_editor; 2 | 3 | /// Set the style of CodeEditor. 4 | /// You have to use it in EditorModel() just like this : 5 | /// ``` 6 | /// EditorModel model = EditorModel( 7 | /// files, // My files... 8 | /// styleOptions: EditorModelStyleOptions( 9 | /// fontSize: 13, // Example 10 | /// ), 11 | /// ); 12 | /// ``` 13 | /// An EditorModel instance has the default values of EditorModelStyleOptions. 14 | class EditorModelStyleOptions { 15 | /// Set the padding of the file's content. By default `15.0`. 16 | final EdgeInsets padding; 17 | 18 | /// Set the height of the container. By default `300`. 19 | final double heightOfContainer; 20 | 21 | /// Set the theme of the syntax. code_editor has its own theme. 22 | /// You can create your own or use other themes by looking at: 23 | /// `import 'package:flutter_highlight/themes/'`. 24 | final Map theme; 25 | 26 | /// The "undo" and "redo" buttons allow the user to undo or redo the modification of a file. 27 | /// 28 | /// They are automatically hidden if `readonly` is set to `true` on the `CodeEditor` widget. 29 | final bool showUndoRedoButtons; 30 | 31 | /// Set the font family of the entire editor. By default `"monospace"`. 32 | final String fontFamily; 33 | 34 | /// Set the letter spacing property of the text. By default `null`. 35 | final double? letterSpacing; 36 | 37 | /// Set the fontSize of the file's content. By default `15`. 38 | final double fontSize; 39 | 40 | /// Set the height (line-height in CSS) property of the text. By default `1.6`. 41 | final double lineHeight; 42 | 43 | /// Set the size of the tabulation. By default `2`. Do not use a too big number. 44 | final int tabSize; 45 | 46 | /// Set the background color of the editor. By default `Color(0xff2E3152)`. 47 | final Color editorColor; 48 | 49 | /// Set the color of the borders between the navigation bar and the content. 50 | /// By default `Color(0xFF3E416E)`. 51 | final Color editorBorderColor; 52 | 53 | /// Set the color of the file name in the navigation bar. By default `Color(0xFF6CD07A)`. 54 | final Color editorFilenameColor; 55 | 56 | /// Set the color property of the edit button. By default `Color(0xFF4650c7)`. 57 | final Color editorToolButtonColor; 58 | 59 | /// Set the color of the edit button's text. By default `Color(0xFF4650c7)`. 60 | final Color editorToolButtonTextColor; 61 | 62 | /// Set the font size of the file's name in the navigation bar. 63 | final double? fontSizeOfFilename; 64 | 65 | /// Set the textStyle of the text field. By default: 66 | /// ``` 67 | /// TextStyle( 68 | /// color: Colors.black87, 69 | /// fontSize: 16, 70 | /// letterSpacing: 1.25, 71 | /// fontWeight: FontWeight.w500, 72 | /// ) 73 | /// ``` 74 | final TextStyle textStyleOfTextField; 75 | 76 | /// The background color of the button "Edit". 77 | /// By default `Color(0xFFEEEEEE)`. 78 | final Color editButtonBackgroundColor; 79 | 80 | /// The text color of the "Edit" button. 81 | /// By default `Colors.black`. 82 | final Color editButtonTextColor; 83 | 84 | /// The name of the "Edit" button. 85 | /// By default `Edit`. 86 | final String editButtonName; 87 | 88 | /// By default the undo and redo buttons will be placed at the left of the Edit button. 89 | /// Set this property to `true` in order to place those buttons at the right. 90 | final bool reverseEditAndUndoRedoButtons; 91 | 92 | /// Options that can be proposed to the user when they select some text. 93 | /// By default, when a user selects some text, they can't copy or cut it. 94 | /// To fix this, you could use: 95 | /// 96 | /// ``` 97 | /// EditorModelStyleOptions( 98 | /// toolbarOptions: ToolbarOptions( 99 | /// copy: true, // now the user can copy a selection 100 | /// cut: true, // cut it 101 | /// selectAll: true // and select it all 102 | /// ), 103 | /// ) 104 | /// ``` 105 | final ToolbarOptions toolbarOptions; 106 | 107 | /// If the user is editing a file, then shall we place the cursor at the end? 108 | /// Default value is `true`. 109 | final bool placeCursorAtTheEndOnEdit; 110 | 111 | /// By default, if the user clicks somewhere outside the text field, 112 | /// the focus will be removed (so `true` by default). 113 | final bool removeFocusOfTextFieldOnTapOutside; 114 | 115 | static const Color defaultColorEditor = Color(0xff2E3152); 116 | static const Color defaultColorBorder = Color(0xFF3E416E); 117 | static const Color defaultColorFileName = Color(0xFF6CD07A); 118 | static const Color defaultToolButtonColor = Color(0xFF4650c7); 119 | static const Color defaultEditBackgroundColor = Color(0xFFEEEEEE); 120 | 121 | EditorModelStyleOptions({ 122 | this.padding = const EdgeInsets.all(15.0), 123 | this.heightOfContainer = 300, 124 | this.theme = myTheme, 125 | this.fontFamily = "monospace", 126 | this.letterSpacing, 127 | this.fontSize = 15, 128 | this.lineHeight = 1.6, 129 | this.tabSize = 2, 130 | this.showUndoRedoButtons = false, 131 | this.reverseEditAndUndoRedoButtons = false, 132 | this.editorColor = defaultColorEditor, 133 | this.editorBorderColor = defaultColorBorder, 134 | this.editorFilenameColor = defaultColorFileName, 135 | this.editorToolButtonColor = defaultToolButtonColor, 136 | this.editorToolButtonTextColor = Colors.white, 137 | this.editButtonBackgroundColor = defaultEditBackgroundColor, 138 | this.editButtonTextColor = Colors.black, 139 | this.editButtonName = "Edit", 140 | this.fontSizeOfFilename, 141 | this.textStyleOfTextField = const TextStyle( 142 | color: Colors.black87, 143 | fontSize: 16, 144 | letterSpacing: 1.25, 145 | fontWeight: FontWeight.w500, 146 | ), 147 | this.toolbarOptions = const ToolbarOptions(), 148 | this.placeCursorAtTheEndOnEdit = true, 149 | this.removeFocusOfTextFieldOnTapOutside = true, 150 | }); 151 | 152 | /// this will become 50 while editing because of the toolbar's height 153 | double? editButtonPosTop; 154 | double? editButtonPosLeft; 155 | double? editButtonPosBottom = 10; 156 | double? editButtonPosRight = 15; 157 | 158 | /// You can change the position of the button "Edit"/"OK". 159 | /// By default, `bottom: 10`, `right: 15`. 160 | void defineEditButtonPosition({ 161 | double? top, 162 | double? left, 163 | double? bottom, 164 | double? right, 165 | }) { 166 | this.editButtonPosTop = top; 167 | this.editButtonPosLeft = left; 168 | this.editButtonPosBottom = bottom; 169 | this.editButtonPosRight = right; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /lib/FileEditor.dart: -------------------------------------------------------------------------------- 1 | part of code_editor; 2 | 3 | /// A file in the editor, used by EditorModel [model.code]. 4 | /// 5 | /// - [name] is the name of the file shown in the navbar. 6 | /// - [language] is the language used by the theme 7 | /// - [code] is the content of the file, the code 8 | /// - [readonly] is a boolean that says if the file shall be editable or not 9 | /// 10 | /// Tip: to simplify writing code in a String, 11 | /// write line by line your code in a `List` and give as the first parameter `list.join("\n")`. 12 | class FileEditor { 13 | /// The name of the file. 14 | /// By default is will be called "file" 15 | /// and its extension will be the given [language], or 'txt'. 16 | late String name; 17 | 18 | /// The name of the language. 19 | /// By default: "text". 20 | late String language; 21 | 22 | /// Its content. 23 | /// By default an empty string. 24 | late String code; 25 | 26 | /// If the file shall not be edited, 27 | /// then set this to `true`. 28 | /// By default it is `false`. 29 | late bool readonly; 30 | 31 | FileEditor({String? name, String? language, String? code, bool? readonly}) { 32 | this.name = name ?? "file.${language ?? 'txt'}"; 33 | this.language = language ?? "text"; 34 | this.code = code ?? ""; 35 | this.readonly = readonly ?? false; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/Theme.dart: -------------------------------------------------------------------------------- 1 | part of code_editor; 2 | 3 | const FontWeight fontWeight = FontWeight.w400; 4 | 5 | // HTML 6 | const Color tagColor = Color(0xFFF79AA5); // tag 7 | const Color quoteColor = Color(0xFF6CD07A); // "" 8 | 9 | // CSS 10 | const Color attrColor = Color(0xFFCBBA7D); // CSS selectors 11 | const Color propertyColor = Color(0xFF8CDCFE); // property 12 | const Color idColor = Color(0xFFCBBA7D); 13 | const Color classColor = Color(0xFFCBBA7D); 14 | 15 | // JS 16 | const Color keywordColor = Color(0xFF3E9CD6); // keywords (function, ...) 17 | const Color methodsColor = Color(0xFFDCDC9D); // methods built in 18 | const Color titlesColor = Color(0xFFDCDC9D); // titles (function's title) 19 | 20 | /// The theme used by code_editor and created by code_editor. 21 | /// This is the default theme of the editor. 22 | /// 23 | /// You can create your own or use 24 | /// other themes by looking in this import: 25 | /// 26 | /// `import 'package:flutter_highlight/themes/'`. 27 | const myTheme = { 28 | 'root': TextStyle( 29 | backgroundColor: Color(0xff2E3152), 30 | color: Color(0xffdddddd) 31 | ), 32 | 'keyword': TextStyle(color: keywordColor), 33 | 'params': TextStyle(color: Color(0xffde935f)), 34 | 'selector-tag': TextStyle(color: attrColor), 35 | 'selector-id': TextStyle(color: idColor), 36 | 'selector-class': TextStyle(color: classColor), 37 | 'regexp': TextStyle(color: Color(0xffcc6666)), 38 | 'literal': TextStyle(color: Colors.white), 39 | 'section': TextStyle(color: Colors.white), 40 | 'link': TextStyle(color: Colors.white), 41 | 'subst': TextStyle(color: Color(0xffdddddd)), 42 | 'string': TextStyle(color: quoteColor), 43 | 'title': TextStyle(color: titlesColor), 44 | 'name': TextStyle(color: tagColor), 45 | 'type': TextStyle(color: tagColor), 46 | 'attribute': TextStyle(color: propertyColor), 47 | 'symbol': TextStyle(color: tagColor), 48 | 'bullet': TextStyle(color: tagColor), 49 | 'built_in': TextStyle(color: methodsColor), 50 | 'addition': TextStyle(color: tagColor), 51 | 'variable': TextStyle(color: tagColor), 52 | 'template-tag': TextStyle(color: tagColor), 53 | 'template-variable': TextStyle(color: tagColor), 54 | 'comment': TextStyle(color: Color(0xff777777)), 55 | 'quote': TextStyle(color: Color(0xff777777)), 56 | 'deletion': TextStyle(color: Color(0xff777777)), 57 | 'meta': TextStyle(color: Color(0xff777777)), 58 | 'emphasis': TextStyle(fontStyle: FontStyle.italic), 59 | }; 60 | -------------------------------------------------------------------------------- /lib/ToolButton.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// A button under the navigation bar to help the user write code. 4 | class ToolButton { 5 | /// The function to execute when pressing the tool button. 6 | void Function() press; 7 | 8 | /// The icon to display on that button in case there is no Unicode character for it. 9 | IconData? icon; 10 | 11 | /// Simple Unicode characters to represent what the button's going to do. 12 | /// If there is no character able to describe its role, then use `icon`. 13 | String? symbol; 14 | 15 | ToolButton({ 16 | required this.press, 17 | this.icon, 18 | this.symbol, 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /lib/code_editor.dart: -------------------------------------------------------------------------------- 1 | library code_editor; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_highlight/flutter_highlight.dart'; 5 | import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 6 | 7 | import 'ToolButton.dart'; 8 | import 'formatters/html.dart'; 9 | 10 | part 'EditorModel.dart'; 11 | part 'FileEditor.dart'; 12 | part 'EditorModelStyleOptions.dart'; 13 | part 'Theme.dart'; 14 | 15 | class CodeEditor extends StatefulWidget { 16 | /// The EditorModel in order to control the editor. 17 | late final EditorModel model; 18 | 19 | /// Function to execute when the user saves changes in a file. 20 | /// This is a function that takes [language] and [value] as arguments. 21 | /// 22 | /// - [language] is the language of the file edited by the user. 23 | /// - [value] is the content of the file. 24 | final void Function(String language, String value)? onSubmit; 25 | 26 | /// You can disable the navigation bar like this: 27 | /// 28 | /// ``` 29 | /// CodeEditor( 30 | /// disableNavigationbar: true, // hides the navigation bar 31 | /// ) 32 | /// ``` 33 | /// 34 | /// By default, the value is `false`. 35 | /// 36 | /// WARNING: if you set the value to `true`, only the first 37 | /// file will be displayed in the editor because 38 | /// it's not possible to switch between other files without the navigation bar. 39 | final bool disableNavigationbar; 40 | 41 | /// The list of languages that the editor is allowed to auto-format on save. 42 | /// As of now, only "html" is supported: 43 | /// 44 | /// ``` 45 | /// CodeEditor( 46 | /// formatters: const ["html"] 47 | /// ) 48 | /// ``` 49 | /// 50 | /// IMPORTANT: this is an experimental feature, 51 | /// there could be some edge cases that the auto-formatter doesn't handle. 52 | /// If you have issues with it, please consider reporting an issue on the GitHub repo. 53 | final List formatters; 54 | 55 | /// A function you can call right before the modification of a file is applied. 56 | /// 57 | /// ### Note that it is called before the auto-formatting. 58 | /// 59 | /// Be aware that if you apply changes to any file of a particular language, 60 | /// and at the same time allow the auto-formatting of this same language, 61 | /// considering it is supported by the editor and allowed by you, 62 | /// then the result of your function might not be the exact output the file will receive 63 | /// 64 | /// ``` 65 | /// CodeEditor( 66 | /// model: model, 67 | /// textModifier: (String language, String content) { 68 | /// if (language == "html") { 69 | /// // the content of any HTML file will become the return value 70 | /// // when they are modified 71 | /// return myCustomHTMLFormatter(content); 72 | /// } else { 73 | /// return content; 74 | /// } 75 | /// }, 76 | /// ) 77 | /// ``` 78 | final String Function(String language, String content)? textModifier; 79 | 80 | /// Creates a code editor that helps users to write and read code on mobile. 81 | /// 82 | /// You can define: 83 | /// - [model] an `EditorModel`, to control the editor, its content and its files (required, if you don't want it, use `CodeEditor.empty()`) 84 | /// - [onSubmit] a `Function(String language, String value)` executed when the user submits changes in a file. 85 | /// - [textModifier] a `String Function(String language, String content)` that allows you to change the modifications of the user before it is saved. 86 | /// - [disableNavigationbar] if set to true, the navigation bar will be hidden. By default, it is false. 87 | /// - [formatters] the list of languages the editor is allowed to auto-format on save. As of now, only `html` is supported. 88 | /// 89 | /// ``` 90 | /// CodeEditor( 91 | /// model: myModel, 92 | /// ), 93 | /// ``` 94 | CodeEditor({ 95 | Key? key, 96 | required this.model, 97 | this.textModifier, 98 | this.onSubmit, 99 | this.disableNavigationbar = false, 100 | this.formatters = const [], 101 | }) : super(key: key); 102 | 103 | /// Creates a code editor that helps users to write and read code on mobile. 104 | /// 105 | /// - [onSubmit] a `Function(String language, String value)` executed when the user submits changes in a file. 106 | /// - [textModifier] a `String Function(String language, String content)` that allows you to change the modifications of the user before it is saved. 107 | /// - [disableNavigationbar] if set to `true`, the navigation bar will be hidden. By default, it is `false`. 108 | /// - [formatters] the list of languages the editor is allowed to auto-format on save. As of now, only `html` is supported. 109 | CodeEditor.empty({ 110 | Key? key, 111 | this.textModifier, 112 | this.disableNavigationbar = false, 113 | this.onSubmit, 114 | this.formatters = const [], 115 | }) : super(key: key) { 116 | this.model = EditorModel(); 117 | } 118 | 119 | @override 120 | _CodeEditorState createState() => _CodeEditorState(); 121 | } 122 | 123 | class _CodeEditorState extends State { 124 | /// We need it to control the content of the text field. 125 | late TextEditingController editingController; 126 | 127 | /// The new content of a file when the user is editing one. 128 | String? newValue; 129 | 130 | /// The text field wants a focus node. 131 | FocusNode focusNode = FocusNode(); 132 | 133 | /// Initialize the formKey for the text field 134 | static final GlobalKey editableTextKey = GlobalKey(); 135 | 136 | // For each filename, 137 | // a stack of undos 138 | // and a stack of redos. 139 | // As the user does something in the application, we PUSH an action onto the undo stack. 140 | // If the user "undos" an action, we POP off the undo stack, do the operation, then we PUSH an action onto the redo stack 141 | // If the user undos multiple times, then does not redo but instead performs a unique action, we consider the redo stack lost 142 | Map> undos = {}; 143 | Map> redos = {}; 144 | 145 | @override 146 | void initState() { 147 | super.initState(); 148 | 149 | // Initialize the controller for the text field with the code of the first file. 150 | editingController = TextEditingController(text: widget.model.getCodeWithIndex(0)); 151 | 152 | newValue = ""; // if there are no changes 153 | 154 | // Init undos/redos stack with an empty array. 155 | // Each file has its own history. 156 | for (FileEditor file in widget.model.allFiles) { 157 | undos[file.name] = []; 158 | redos[file.name] = []; 159 | } 160 | } 161 | 162 | @override 163 | void dispose() { 164 | editingController.dispose(); 165 | super.dispose(); 166 | } 167 | 168 | void recordBeforeAction(FileEditor file) { 169 | undos[file.name]!.add(file.code); 170 | clearRedos(); // if the user edits an older version, the redo stack is lost 171 | } 172 | 173 | void clearRedos() { 174 | FileEditor editedFile = widget.model.getFileWithIndex(widget.model.position)!; 175 | redos[editedFile.name] = []; 176 | } 177 | 178 | void undo() { 179 | int currentPosition = widget.model.position; 180 | FileEditor editedFile = widget.model.getFileWithIndex(currentPosition)!; 181 | if (undos[editedFile.name]?.length != 0) { 182 | String previousState = undos[editedFile.name]!.removeLast(); 183 | String currentState = editedFile.code; 184 | redos[editedFile.name]!.add(currentState); 185 | setState(() { 186 | widget.model.updateCodeOfIndex(currentPosition, previousState); 187 | }); 188 | } 189 | } 190 | 191 | void redo() { 192 | int currentPosition = widget.model.position; 193 | FileEditor currentFile = widget.model.getFileWithIndex(currentPosition)!; 194 | if (redos[currentFile.name]?.length != 0) { 195 | undos[currentFile.name]!.add(currentFile.code); 196 | setState(() { 197 | widget.model.updateCodeOfIndex(currentPosition, redos[currentFile.name]!.removeLast()); 198 | }); 199 | } 200 | } 201 | 202 | /// Set the cursor at the end of the editableText. 203 | void placeCursorAtTheEnd() { 204 | editingController.selection = TextSelection.fromPosition( 205 | TextPosition(offset: editingController.text.length), 206 | ); 207 | } 208 | 209 | /// Place the cursor where it's needed. 210 | /// 211 | /// - [pos] the index where to place the cursor in the text field 212 | void placeCursor(int pos) { 213 | try { 214 | editingController.selection = TextSelection.fromPosition( 215 | TextPosition(offset: pos), 216 | ); 217 | } catch (e) { 218 | throw Exception("code_editor : placeCursor(int pos), pos is not valid."); 219 | } 220 | } 221 | 222 | /// Modifies a file according to its language to follow a precise format. 223 | String format(String content, String language) { 224 | switch (language) { 225 | case "html": 226 | return formatHTML(content); 227 | default: 228 | return content; 229 | } 230 | } 231 | 232 | /// The Text widget corresponding to the name of a file in the navigation bar. 233 | Text showFilename(String name, bool isSelected) { 234 | return Text( 235 | name, 236 | style: TextStyle( 237 | fontFamily: "monospace", 238 | letterSpacing: 1.0, 239 | fontWeight: FontWeight.normal, 240 | fontSize: widget.model.styleOptions.fontSizeOfFilename, 241 | color: isSelected ? widget.model.styleOptions.editorFilenameColor : widget.model.styleOptions.editorFilenameColor.withOpacity(0.5), 242 | ), 243 | ); 244 | } 245 | 246 | /// Build the navigation bar. 247 | Container buildNavbar() { 248 | return Container( 249 | width: double.infinity, 250 | height: 60, 251 | decoration: BoxDecoration( 252 | color: widget.model.styleOptions.editorColor, 253 | border: Border( 254 | bottom: BorderSide(color: widget.model.styleOptions.editorBorderColor), 255 | ), 256 | ), 257 | child: ListView.builder( 258 | padding: EdgeInsets.only(left: 15), 259 | itemCount: widget.model.numberOfFiles, 260 | scrollDirection: Axis.horizontal, 261 | itemBuilder: (context, int index) { 262 | final FileEditor file = widget.model.getFileWithIndex(index)!; 263 | 264 | return Container( 265 | margin: EdgeInsets.only(right: 15), 266 | child: Center( 267 | child: GestureDetector( 268 | // Checks if the position of the navbar is the current file. 269 | child: showFilename(file.name, widget.model.position == index), 270 | onTap: () { 271 | setState(() { 272 | widget.model.changeIndexTo(index); 273 | editingController.text = widget.model.getCodeWithIndex(index); 274 | }); 275 | }, 276 | ), 277 | ), 278 | ); 279 | }, 280 | ), 281 | ); 282 | } 283 | 284 | /// Creates the text field. 285 | SingleChildScrollView buildEditableText() { 286 | return SingleChildScrollView( 287 | child: Container( 288 | padding: EdgeInsets.only( 289 | right: 10, 290 | left: 10, 291 | top: 10, 292 | bottom: 50, 293 | ), 294 | child: TextField( 295 | decoration: InputDecoration(border: InputBorder.none), 296 | autofocus: true, 297 | keyboardType: TextInputType.multiline, 298 | maxLines: null, 299 | style: widget.model.styleOptions.textStyleOfTextField, 300 | focusNode: focusNode, 301 | controller: editingController, 302 | onChanged: (String v) => newValue = v, 303 | onTapOutside: (_) { 304 | if (widget.model.styleOptions.removeFocusOfTextFieldOnTapOutside) { 305 | // Because it's too annoying on IPhone 306 | if (focusNode.hasFocus) { 307 | focusNode.unfocus(); 308 | } 309 | } 310 | }, 311 | key: editableTextKey, 312 | toolbarOptions: widget.model.styleOptions.toolbarOptions, 313 | ), 314 | ), 315 | ); 316 | } 317 | 318 | /// Creates the edit button and the save button ("OK") with a 319 | /// particular function to execute. 320 | /// 321 | /// This button won't appear if the current file is set as `readonly`. 322 | Widget editButton(String name, void Function() press) { 323 | if (widget.model.getFileWithIndex(widget.model.position)?.readonly == true) { 324 | return SizedBox.shrink(); 325 | } 326 | final opt = widget.model.styleOptions; 327 | List buttons = []; 328 | if (widget.model.styleOptions.showUndoRedoButtons) { 329 | ElevatedButton undoButton = ElevatedButton( 330 | style: ElevatedButton.styleFrom( 331 | elevation: 0.0, 332 | backgroundColor: Colors.white.withOpacity(0), 333 | ), 334 | child: FaIcon( 335 | FontAwesomeIcons.arrowRotateLeft, 336 | color: Colors.white.withOpacity(0.5), 337 | size: 18, 338 | ), 339 | onPressed: () { 340 | undo(); 341 | }, 342 | ); 343 | ElevatedButton redoButton = ElevatedButton( 344 | style: ElevatedButton.styleFrom( 345 | elevation: 0.0, 346 | backgroundColor: Colors.white.withOpacity(0), 347 | ), 348 | child: FaIcon( 349 | FontAwesomeIcons.arrowRotateRight, 350 | color: Colors.white.withOpacity(0.5), 351 | size: 18, 352 | ), 353 | onPressed: () { 354 | redo(); 355 | }, 356 | ); 357 | if (opt.reverseEditAndUndoRedoButtons) { 358 | // it will get reversed afterwards 359 | // so that it's always `undoButton` before `redoButton` 360 | buttons.addAll([redoButton, undoButton]); 361 | } else { 362 | buttons.addAll([undoButton, redoButton]); 363 | } 364 | } 365 | buttons.add(ElevatedButton( 366 | style: ElevatedButton.styleFrom( 367 | backgroundColor: opt.editButtonBackgroundColor, 368 | ), 369 | onPressed: press, 370 | child: Text( 371 | name, 372 | style: TextStyle( 373 | fontSize: 16.0, 374 | fontFamily: "monospace", 375 | fontWeight: FontWeight.normal, 376 | color: opt.editButtonTextColor, 377 | ), 378 | ), 379 | )); 380 | // if the user allowed modifications: 381 | return Positioned( 382 | bottom: opt.editButtonPosBottom, 383 | right: opt.editButtonPosRight, 384 | top: (widget.model.isEditing && opt.editButtonPosTop != null && opt.editButtonPosTop! < 50) ? 50 : opt.editButtonPosTop, 385 | left: opt.editButtonPosLeft, 386 | child: Row( 387 | children: opt.reverseEditAndUndoRedoButtons ? buttons.reversed.toList() : buttons, 388 | ), 389 | ); 390 | } 391 | 392 | /// Add a particular string where the cursor is in the text field. 393 | /// - [str] the string to insert 394 | /// - [diff] by default, the cursor is placed after the placed string, but you can change this (example: -1 when quotes are placed in order to have the cursor at the center of the quotes) 395 | void insertIntoTextField(String str, {int diff = 0}) { 396 | // get the position of the cursor in the text field 397 | int pos = editingController.selection.baseOffset; 398 | // get the current text of the text field 399 | String baseText = editingController.text; 400 | // get the string: 0 -> pos of the current text and add the wanted string 401 | String begin = baseText.substring(0, pos) + str; 402 | // if we are already in the end of the string 403 | if (baseText.length == pos) { 404 | editingController.text = begin; 405 | } else { 406 | // get the end of the string and update the text of the text field 407 | String end = baseText.substring(pos, baseText.length); 408 | editingController.text = begin + end; 409 | } 410 | // if we don't do this, when we click on a toolbutton, the method 411 | // onChanged() isn't called, so newValue isn't updated 412 | newValue = editingController.text; 413 | placeCursor(pos + str.length + diff); 414 | } 415 | 416 | @override 417 | Widget build(BuildContext context) { 418 | /// Gets the style options from the parent widget. 419 | final EditorModelStyleOptions opt = widget.model.styleOptions; 420 | 421 | /// Which file in the list of file? 422 | final int position = widget.model.position; 423 | 424 | /// The content of the file where position corresponds to the list of file. 425 | final String? code = widget.model.getCodeWithIndex(position); 426 | 427 | // if the user does not change the value in the text field 428 | newValue = code; 429 | 430 | /// Creates the toolbar. 431 | Widget toolBar() { 432 | final List toolButtons = [ 433 | ToolButton( 434 | press: () => insertIntoTextField("\t"), 435 | icon: FontAwesomeIcons.indent, 436 | ), 437 | ToolButton( 438 | press: () => insertIntoTextField("<"), 439 | icon: FontAwesomeIcons.chevronLeft, 440 | ), 441 | ToolButton( 442 | press: () => insertIntoTextField(">"), 443 | icon: FontAwesomeIcons.chevronRight, 444 | ), 445 | ToolButton( 446 | press: () => insertIntoTextField('""', diff: -1), 447 | icon: FontAwesomeIcons.quoteLeft, 448 | ), 449 | ToolButton( 450 | press: () => insertIntoTextField(":"), 451 | symbol: ":", 452 | ), 453 | ToolButton( 454 | press: () => insertIntoTextField(";"), 455 | symbol: ";", 456 | ), 457 | ToolButton( 458 | press: () => insertIntoTextField('()', diff: -1), 459 | symbol: "()", 460 | ), 461 | ToolButton( 462 | press: () => insertIntoTextField('{}', diff: -1), 463 | symbol: "{}", 464 | ), 465 | ToolButton( 466 | press: () => insertIntoTextField('[]', diff: -1), 467 | symbol: "[]", 468 | ), 469 | ToolButton( 470 | press: () => insertIntoTextField("-"), 471 | icon: FontAwesomeIcons.minus, 472 | ), 473 | ToolButton( 474 | press: () => insertIntoTextField("="), 475 | icon: FontAwesomeIcons.equals, 476 | ), 477 | ToolButton( 478 | press: () => insertIntoTextField("+"), 479 | icon: FontAwesomeIcons.plus, 480 | ), 481 | ToolButton( 482 | press: () => insertIntoTextField("/"), 483 | icon: FontAwesomeIcons.divide, 484 | ), 485 | ToolButton( 486 | press: () => insertIntoTextField("*"), 487 | icon: FontAwesomeIcons.xmark, 488 | ), 489 | ]; 490 | 491 | return Container( 492 | height: 50, 493 | width: double.infinity, 494 | decoration: BoxDecoration( 495 | color: opt.editorColor, 496 | border: Border( 497 | bottom: BorderSide(color: opt.editorBorderColor), 498 | ), 499 | ), 500 | child: ListView.builder( 501 | padding: EdgeInsets.only(left: 15, top: 8, bottom: 8), 502 | itemCount: toolButtons.length, 503 | scrollDirection: Axis.horizontal, 504 | itemBuilder: (context, int index) { 505 | final ToolButton btn = toolButtons[index]; 506 | 507 | return Container( 508 | width: 55, 509 | margin: EdgeInsets.only(right: 15), // == padding right above 510 | child: TextButton( 511 | style: TextButton.styleFrom( 512 | backgroundColor: opt.editorToolButtonColor, 513 | ), 514 | onPressed: btn.press, 515 | child: btn.icon == null 516 | ? Text( 517 | btn.symbol ?? "", 518 | style: TextStyle( 519 | color: opt.editorToolButtonTextColor, 520 | fontSize: 16, 521 | fontWeight: FontWeight.bold, 522 | fontFamily: "monospace", 523 | ), 524 | ) 525 | : FaIcon( 526 | btn.icon, 527 | color: opt.editorToolButtonTextColor, 528 | size: 15, 529 | ), 530 | ), 531 | ); 532 | }, 533 | ), 534 | ); 535 | } 536 | 537 | // We place the cursor in the end of the text field. 538 | if (widget.model.isEditing && widget.model.styleOptions.placeCursorAtTheEndOnEdit) { 539 | placeCursorAtTheEnd(); 540 | } 541 | 542 | /// We toggle the editor and the text field. 543 | Widget buildContentEditor() { 544 | return widget.model.isEditing 545 | ? Stack( 546 | children: [ 547 | Column( 548 | children: [ 549 | toolBar(), 550 | // Container of the EditableText 551 | Container( 552 | width: double.infinity, 553 | height: opt.heightOfContainer, 554 | decoration: BoxDecoration( 555 | color: Colors.white, 556 | border: Border( 557 | bottom: BorderSide( 558 | color: opt.editorBorderColor.withOpacity(0.4), 559 | ), 560 | ), 561 | ), 562 | child: buildEditableText(), 563 | ), 564 | ], 565 | ), 566 | // The OK button 567 | editButton("OK", () { 568 | // Here, the user completed a change in the code 569 | setState(() { 570 | recordBeforeAction(widget.model.getFileWithIndex(position)!); 571 | 572 | String newCode = newValue ?? ""; 573 | if (widget.textModifier != null) { 574 | newCode = widget.textModifier!(widget.model.currentLanguage, newCode); 575 | } 576 | if (widget.formatters.contains(widget.model.currentLanguage)) { 577 | newCode = format(newCode, widget.model.currentLanguage); 578 | } 579 | editingController.text = newCode; // without it editing twice the same file in a row would display the previous content 580 | widget.model.updateCodeOfIndex(position, newCode); 581 | widget.model.toggleEditing(); 582 | widget.onSubmit?.call(widget.model.currentLanguage, newCode); 583 | }); 584 | }), 585 | ], 586 | ) 587 | : Stack( 588 | children: [ 589 | Container( 590 | width: double.infinity, 591 | height: opt.heightOfContainer, 592 | color: opt.editorColor, 593 | child: SingleChildScrollView( 594 | child: Padding( 595 | padding: opt.padding, 596 | child: Column( 597 | crossAxisAlignment: CrossAxisAlignment.start, 598 | children: [ 599 | HighlightView( 600 | code ?? "there is no code", 601 | language: widget.model.currentLanguage, 602 | theme: opt.theme, 603 | tabSize: opt.tabSize, 604 | textStyle: TextStyle( 605 | fontFamily: opt.fontFamily, 606 | letterSpacing: opt.letterSpacing, 607 | fontSize: opt.fontSize, 608 | height: opt.lineHeight, 609 | ), 610 | ), 611 | ], 612 | ), 613 | ), 614 | ), 615 | ), 616 | editButton(opt.editButtonName, () { 617 | setState(() { 618 | widget.model.toggleEditing(); 619 | }); 620 | }), 621 | ], 622 | ); 623 | } 624 | 625 | return Column( 626 | children: [ 627 | widget.disableNavigationbar ? SizedBox.shrink() : buildNavbar(), 628 | buildContentEditor(), 629 | ], 630 | ); 631 | } 632 | } 633 | -------------------------------------------------------------------------------- /lib/formatters/html.dart: -------------------------------------------------------------------------------- 1 | /// Formats HTML. 2 | /// Takes [content] as input and a precise tab size. 3 | String formatHTML(String content, {int tabSize = 2}) { 4 | content = content.trim(); 5 | String newContent = ""; 6 | int counter = 0; 7 | bool text = false; 8 | bool tag = false; 9 | bool enteringTag = false; 10 | String previousCharacter = ""; 11 | bool isXMLTag = false; 12 | for (int i = 0; i < content.length; i++) { 13 | String currentChar = content[i]; 14 | if (currentChar == '<') { 15 | i++; 16 | if (i == content.length) break; 17 | bool isClosingTag = content[i] == '/'; 18 | isXMLTag = content[i] == '!'; 19 | if (counter > 0) { 20 | newContent += '\n'; 21 | if ((isClosingTag ? counter - 1 > 0 : counter > 0)) { 22 | newContent += (' ' * tabSize * (isClosingTag ? counter - 1 : counter)); 23 | } 24 | } 25 | if (!isXMLTag) { 26 | if (isClosingTag) { 27 | counter--; 28 | } else { 29 | counter++; 30 | } 31 | } 32 | newContent += currentChar + content[i]; 33 | text = false; 34 | tag = true; 35 | } else { 36 | bool isControlledCharacter = currentChar == '\n' || currentChar == "\t" || currentChar == "\r"; 37 | bool isWhiteSpace = currentChar == " "; 38 | if (currentChar == '>') { 39 | tag = false; 40 | } else if (!tag && !text) { 41 | if (!isControlledCharacter && !isWhiteSpace) { 42 | text = true; 43 | newContent += (isXMLTag ? '' : '\n') + (' ' * tabSize * counter); 44 | } 45 | enteringTag = true; 46 | isXMLTag = false; 47 | } 48 | // We want to ignore the cases when: 49 | // - having white space 50 | // - entering a tag with white space 51 | // - having special escaped characters such as \n, \t or \r 52 | if ((previousCharacter == " " && isWhiteSpace) || (!tag && enteringTag && isWhiteSpace) || isControlledCharacter) { 53 | continue; 54 | } 55 | previousCharacter = currentChar; 56 | newContent += currentChar == '>' && isXMLTag ? '>\n' : currentChar; 57 | enteringTag = false; 58 | } 59 | } 60 | return newContent; 61 | } 62 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.11.0" 12 | boolean_selector: 13 | dependency: transitive 14 | description: 15 | name: boolean_selector 16 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.1.1" 20 | characters: 21 | dependency: transitive 22 | description: 23 | name: characters 24 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "1.3.0" 28 | clock: 29 | dependency: transitive 30 | description: 31 | name: clock 32 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "1.1.1" 36 | collection: 37 | dependency: transitive 38 | description: 39 | name: collection 40 | sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.17.1" 44 | fake_async: 45 | dependency: transitive 46 | description: 47 | name: fake_async 48 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.3.1" 52 | flutter: 53 | dependency: "direct main" 54 | description: flutter 55 | source: sdk 56 | version: "0.0.0" 57 | flutter_highlight: 58 | dependency: "direct main" 59 | description: 60 | name: flutter_highlight 61 | sha256: "7b96333867aa07e122e245c033b8ad622e4e3a42a1a2372cbb098a2541d8782c" 62 | url: "https://pub.dev" 63 | source: hosted 64 | version: "0.7.0" 65 | flutter_test: 66 | dependency: "direct dev" 67 | description: flutter 68 | source: sdk 69 | version: "0.0.0" 70 | font_awesome_flutter: 71 | dependency: "direct main" 72 | description: 73 | name: font_awesome_flutter 74 | sha256: "5fb789145cae1f4c3245c58b3f8fb287d055c26323879eab57a7bf0cfd1e45f3" 75 | url: "https://pub.dev" 76 | source: hosted 77 | version: "10.5.0" 78 | highlight: 79 | dependency: transitive 80 | description: 81 | name: highlight 82 | sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21" 83 | url: "https://pub.dev" 84 | source: hosted 85 | version: "0.7.0" 86 | js: 87 | dependency: transitive 88 | description: 89 | name: js 90 | sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 91 | url: "https://pub.dev" 92 | source: hosted 93 | version: "0.6.7" 94 | matcher: 95 | dependency: transitive 96 | description: 97 | name: matcher 98 | sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" 99 | url: "https://pub.dev" 100 | source: hosted 101 | version: "0.12.15" 102 | material_color_utilities: 103 | dependency: transitive 104 | description: 105 | name: material_color_utilities 106 | sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 107 | url: "https://pub.dev" 108 | source: hosted 109 | version: "0.2.0" 110 | meta: 111 | dependency: transitive 112 | description: 113 | name: meta 114 | sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" 115 | url: "https://pub.dev" 116 | source: hosted 117 | version: "1.9.1" 118 | path: 119 | dependency: transitive 120 | description: 121 | name: path 122 | sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" 123 | url: "https://pub.dev" 124 | source: hosted 125 | version: "1.8.3" 126 | sky_engine: 127 | dependency: transitive 128 | description: flutter 129 | source: sdk 130 | version: "0.0.99" 131 | source_span: 132 | dependency: transitive 133 | description: 134 | name: source_span 135 | sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 136 | url: "https://pub.dev" 137 | source: hosted 138 | version: "1.9.1" 139 | stack_trace: 140 | dependency: transitive 141 | description: 142 | name: stack_trace 143 | sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 144 | url: "https://pub.dev" 145 | source: hosted 146 | version: "1.11.0" 147 | stream_channel: 148 | dependency: transitive 149 | description: 150 | name: stream_channel 151 | sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" 152 | url: "https://pub.dev" 153 | source: hosted 154 | version: "2.1.1" 155 | string_scanner: 156 | dependency: transitive 157 | description: 158 | name: string_scanner 159 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 160 | url: "https://pub.dev" 161 | source: hosted 162 | version: "1.2.0" 163 | term_glyph: 164 | dependency: transitive 165 | description: 166 | name: term_glyph 167 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 168 | url: "https://pub.dev" 169 | source: hosted 170 | version: "1.2.1" 171 | test_api: 172 | dependency: transitive 173 | description: 174 | name: test_api 175 | sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb 176 | url: "https://pub.dev" 177 | source: hosted 178 | version: "0.5.1" 179 | vector_math: 180 | dependency: transitive 181 | description: 182 | name: vector_math 183 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 184 | url: "https://pub.dev" 185 | source: hosted 186 | version: "2.1.4" 187 | sdks: 188 | dart: ">=3.0.0 <4.0.0" 189 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: code_editor 2 | description: A code editor (dart, js, html,...) for Flutter with syntax highlighting and custom theme. 3 | version: 2.1.0 4 | homepage: https://github.com/CodoPixel/code_editor 5 | 6 | environment: 7 | sdk: ">=2.12.0 <4.0.0" 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | 13 | flutter_highlight: ^0.7.0 14 | font_awesome_flutter: ^10.1.0 15 | 16 | dev_dependencies: 17 | flutter_test: 18 | sdk: flutter 19 | -------------------------------------------------------------------------------- /test/html.dart: -------------------------------------------------------------------------------- 1 | import '../lib/formatters/html.dart'; 2 | import 'test_api.dart'; 3 | 4 | void main() { 5 | test("one unique empty tag", formatHTML("

"), "

\n

"); 6 | test("one tag inside another", formatHTML("

yo

"), "

\n \n yo\n \n

"); 7 | test("three nested tags", formatHTML("

yo

"), "

\n \n \n yo\n \n \n

"); 8 | test("one pair inside one common tag", formatHTML("

firstsecond

"), "

\n \n first\n \n \n second\n \n

"); 9 | test("three tags inside one common tag", formatHTML("

firstsecondthird

"), "

\n \n first\n \n \n second\n \n \n third\n \n

"); 10 | test("complex tree", formatHTML("

firstsecondthird

"), "

\n \n first\n \n \n \n second\n \n \n \n third\n \n

"); 11 | test("simple tree with spaces", formatHTML("

first

"), "

\n first\n

"); 12 | test("spaces between text and at the beginning of tag", formatHTML("

fi rst

"), "

\n fi rst\n

"); 13 | test("special escaped characters", formatHTML("

\nhi how are you

"), "

\n hi how are you\n

"); 14 | test("simple tag with attributes", formatHTML(""), "\n"); 15 | test("correct architecture", formatHTML(""" 16 | 17 | 18 | Hello 19 | 20 | 21 |

22 | Hello 23 |

24 | 25 | 26 | """), "\n \n \n Hello\n \n \n \n

\n Hello \n

\n \n"); 27 | test("incorrect architecture", formatHTML(""" 28 | 29 | 30 | 31 | Hello 32 | 33 | 34 | 35 |

36 | Hello 37 |

38 | 39 | 40 | """), "\n \n \n Hello \n \n \n \n

\n Hello \n

\n \n"); 41 | test("with missing closing tag", formatHTML("

"), "

\n \n

"); 42 | test("with DOCTYPE", formatHTML("

"), "\n\n

\n

\n"); 43 | test("with comment", formatHTML("

Hello

"), "

\n \n Hello\n

"); 44 | test("with attribute", formatHTML("go to page 2"), "\n \n go to page 2\n \n"); 45 | } 46 | -------------------------------------------------------------------------------- /test/test_api.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | void test(String name, String result, String expected) { 4 | if (result == expected) { 5 | print("Passed : " + name); 6 | } else { 7 | print("X Test failed : " + name); 8 | print("We got : "); 9 | print(result); 10 | print("But were expecting to get : "); 11 | print(expected); 12 | exit(0); 13 | } 14 | } 15 | --------------------------------------------------------------------------------