Browse Source

Merge pull request #42 from memspace/toolbar-overlay

Allow embedding Zefyr into custom layouts with ZefyrScaffold
Anatoly Pulyaevskiy 6 years ago
parent
commit
2ff0df7c28
No account linked to committer's email address

+ 1
- 0
.gitignore View File

1
+.idea/

+ 10
- 5
doc/quick_start.md View File

12
 
12
 
13
 ```yaml
13
 ```yaml
14
 dependencies:
14
 dependencies:
15
-  zefyr: ^0.1.0
15
+  zefyr: ^0.3.0
16
 ```
16
 ```
17
 
17
 
18
 And run `flutter packages get` to install. This installs both `zefyr`
18
 And run `flutter packages get` to install. This installs both `zefyr`
20
 
20
 
21
 ### Usage
21
 ### Usage
22
 
22
 
23
-There are 3 main objects you would normally interact with in your code:
23
+There are 4 main objects you would normally interact with in your code:
24
 
24
 
25
 * `NotusDocument`, represents a rich text document and provides
25
 * `NotusDocument`, represents a rich text document and provides
26
   high-level methods for manipulating the document's state, like
26
   high-level methods for manipulating the document's state, like
30
 * `ZefyrEditor`, a Flutter widget responsible for rendering of rich text
30
 * `ZefyrEditor`, a Flutter widget responsible for rendering of rich text
31
   on the screen and reacting to user actions.
31
   on the screen and reacting to user actions.
32
 * `ZefyrController`, ties the above two objects together.
32
 * `ZefyrController`, ties the above two objects together.
33
+* `ZefyrScaffold`, allows embedding Zefyr toolbar into any custom layout.
34
+
35
+`ZefyrEditor` depends on presence of `ZefyrScaffold` somewhere up the widget tree.
33
 
36
 
34
 Normally you would need to place `ZefyrEditor` inside of a
37
 Normally you would need to place `ZefyrEditor` inside of a
35
 `StatefulWidget`. Shown below is a minimal setup required to use the
38
 `StatefulWidget`. Shown below is a minimal setup required to use the
60
 
63
 
61
   @override
64
   @override
62
   Widget build(BuildContext context) {
65
   Widget build(BuildContext context) {
63
-    return ZefyrEditor(
64
-      controller: _controller,
65
-      focusNode: _focusNode,
66
+    return ZefyrScaffold(
67
+      child: ZefyrEditor(
68
+        controller: _controller,
69
+        focusNode: _focusNode,
70
+      ),
66
     );
71
     );
67
   }
72
   }
68
 }
73
 }

+ 2
- 0
packages/zefyr/.gitignore View File

14
 example/ios/Flutter/Generated.xcconfig
14
 example/ios/Flutter/Generated.xcconfig
15
 doc/api/
15
 doc/api/
16
 build/
16
 build/
17
+
18
+example/feather

+ 15
- 0
packages/zefyr/CHANGELOG.md View File

1
+## 0.3.0
2
+
3
+This version introduces new widget `ZefyrScaffold` which allows embedding Zefyr in custom
4
+layouts, like forms with multiple input fields.
5
+
6
+It is now required to always wrap `ZefyrEditor` with an instance of this new widget. See examples
7
+and readme for more details.
8
+
9
+There is also new `ZefyrField` widget which integrates Zefyr with material design decorations.
10
+
11
+* Breaking change: `ZefyrEditor` requires an ancestor `ZefyrScaffold`.
12
+* Upgraded to `url_launcher` version 4.0.0.
13
+* Exposed `ZefyrEditor.physics` property to allow customization of `ScrollPhysics`.
14
+* Added basic `ZefyrField` widget with material design decorations.
15
+
1
 ## 0.2.0
16
 ## 0.2.0
2
 
17
 
3
 * Breaking change: `ZefyrImageDelegate.createImageProvider` replaced with
18
 * Breaking change: `ZefyrImageDelegate.createImageProvider` replaced with

+ 1
- 1
packages/zefyr/analysis_options.yaml View File

1
 analyzer:
1
 analyzer:
2
   language:
2
   language:
3
-    enableSuperMixins: true
3
+#    enableSuperMixins: true
4
 
4
 
5
 # Lint rules and documentation, see http://dart-lang.github.io/linter/lints
5
 # Lint rules and documentation, see http://dart-lang.github.io/linter/lints
6
 linter:
6
 linter:

+ 34
- 92
packages/zefyr/example/lib/main.dart View File

1
 // Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
1
 // Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2
 // for details. All rights reserved. Use of this source code is governed by a
2
 // for details. All rights reserved. Use of this source code is governed by a
3
 // BSD-style license that can be found in the LICENSE file.
3
 // BSD-style license that can be found in the LICENSE file.
4
-import 'dart:convert';
5
-
6
 import 'package:flutter/material.dart';
4
 import 'package:flutter/material.dart';
7
-import 'package:quill_delta/quill_delta.dart';
8
-import 'package:zefyr/zefyr.dart';
5
+import 'src/form.dart';
6
+import 'src/full_page.dart';
9
 
7
 
10
 void main() {
8
 void main() {
11
   runApp(new ZefyrApp());
9
   runApp(new ZefyrApp());
12
 }
10
 }
13
 
11
 
14
-class ZefyrLogo extends StatelessWidget {
15
-  @override
16
-  Widget build(BuildContext context) {
17
-    return Row(
18
-      mainAxisAlignment: MainAxisAlignment.center,
19
-      children: <Widget>[
20
-        Text('Ze'),
21
-        FlutterLogo(size: 24.0),
22
-        Text('yr'),
23
-      ],
24
-    );
25
-  }
26
-}
27
-
28
 class ZefyrApp extends StatelessWidget {
12
 class ZefyrApp extends StatelessWidget {
29
   @override
13
   @override
30
   Widget build(BuildContext context) {
14
   Widget build(BuildContext context) {
31
-    return new MaterialApp(
15
+    return MaterialApp(
32
       debugShowCheckedModeBanner: false,
16
       debugShowCheckedModeBanner: false,
33
       title: 'Zefyr Editor',
17
       title: 'Zefyr Editor',
34
-      theme: new ThemeData(primarySwatch: Colors.cyan),
35
-      home: new MyHomePage(),
18
+      theme: ThemeData(primarySwatch: Colors.cyan),
19
+      home: HomePage(),
20
+      routes: {
21
+        "/fullPage": buildFullPage,
22
+        "/form": buildFormPage,
23
+      },
36
     );
24
     );
37
   }
25
   }
38
-}
39
-
40
-class MyHomePage extends StatefulWidget {
41
-  @override
42
-  _MyHomePageState createState() => new _MyHomePageState();
43
-}
44
 
26
 
45
-final doc =
46
-    r'[{"insert":"Zefyr"},{"insert":"\n","attributes":{"heading":1}},{"insert":"Soft and gentle rich text editing for Flutter applications.","attributes":{"i":true}},{"insert":"\n"},{"insert":"​","attributes":{"embed":{"type":"image","source":"asset://images/breeze.jpg"}}},{"insert":"\n"},{"insert":"Photo by Hiroyuki Takeda.","attributes":{"i":true}},{"insert":"\nZefyr is currently in "},{"insert":"early preview","attributes":{"b":true}},{"insert":". If you have a feature request or found a bug, please file it at the "},{"insert":"issue tracker","attributes":{"a":"https://github.com/memspace/zefyr/issues"}},{"insert":'
47
-    r'".\nDocumentation"},{"insert":"\n","attributes":{"heading":3}},{"insert":"Quick Start","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/quick_start.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Data Format and Document Model","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/data_and_document.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Style Attributes","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/attr'
48
-    r'ibutes.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Heuristic Rules","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/heuristics.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"FAQ","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/faq.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Clean and modern look"},{"insert":"\n","attributes":{"heading":2}},{"insert":"Zefyr’s rich text editor is built with simplicity and fle'
49
-    r'xibility in mind. It provides clean interface for distraction-free editing. Think Medium.com-like experience.\nMarkdown inspired semantics"},{"insert":"\n","attributes":{"heading":2}},{"insert":"Ever needed to have a heading line inside of a quote block, like this:\nI’m a Markdown heading"},{"insert":"\n","attributes":{"block":"quote","heading":3}},{"insert":"And I’m a regular paragraph"},{"insert":"\n","attributes":{"block":"quote"}},{"insert":"Code blocks"},{"insert":"\n","attributes":{"headin'
50
-    r'g":2}},{"insert":"Of course:\nimport ‘package:flutter/material.dart’;"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"import ‘package:zefyr/zefyr.dart’;"},{"insert":"\n\n","attributes":{"block":"code"}},{"insert":"void main() {"},{"insert":"\n","attributes":{"block":"code"}},{"insert":" runApp(MyZefyrApp());"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"}"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"\n\n\n"}]';
27
+  Widget buildFullPage(BuildContext context) {
28
+    return FullPageEditorScreen();
29
+  }
51
 
30
 
52
-Delta getDelta() {
53
-  return Delta.fromJson(json.decode(doc));
31
+  Widget buildFormPage(BuildContext context) {
32
+    return FormEmbeddedScreen();
33
+  }
54
 }
34
 }
55
 
35
 
56
-class _MyHomePageState extends State<MyHomePage> {
57
-  final ZefyrController _controller =
58
-      ZefyrController(NotusDocument.fromDelta(getDelta()));
59
-  final FocusNode _focusNode = new FocusNode();
60
-  bool _editing = false;
61
-
36
+class HomePage extends StatelessWidget {
62
   @override
37
   @override
63
   Widget build(BuildContext context) {
38
   Widget build(BuildContext context) {
64
-    final theme = new ZefyrThemeData(
65
-      toolbarTheme: ZefyrToolbarTheme.fallback(context).copyWith(
66
-        color: Colors.grey.shade800,
67
-        toggleColor: Colors.grey.shade900,
68
-        iconColor: Colors.white,
69
-        disabledIconColor: Colors.grey.shade500,
70
-      ),
71
-    );
72
-
73
-    final done = _editing
74
-        ? [new FlatButton(onPressed: _stopEditing, child: Text('DONE'))]
75
-        : [new FlatButton(onPressed: _startEditing, child: Text('EDIT'))];
39
+    final nav = Navigator.of(context);
76
     return Scaffold(
40
     return Scaffold(
77
-      resizeToAvoidBottomPadding: true,
78
       appBar: AppBar(
41
       appBar: AppBar(
79
         elevation: 1.0,
42
         elevation: 1.0,
80
         backgroundColor: Colors.grey.shade200,
43
         backgroundColor: Colors.grey.shade200,
81
         brightness: Brightness.light,
44
         brightness: Brightness.light,
82
         title: ZefyrLogo(),
45
         title: ZefyrLogo(),
83
-        actions: done,
84
       ),
46
       ),
85
-      body: ZefyrTheme(
86
-        data: theme,
87
-        child: ZefyrEditor(
88
-          controller: _controller,
89
-          focusNode: _focusNode,
90
-          enabled: _editing,
91
-          imageDelegate: new CustomImageDelegate(),
92
-        ),
47
+      body: Column(
48
+        children: <Widget>[
49
+          Expanded(child: Container()),
50
+          FlatButton(
51
+            onPressed: () => nav.pushNamed('/fullPage'),
52
+            child: Text('Full page editor'),
53
+            color: Colors.lightBlue,
54
+            textColor: Colors.white,
55
+          ),
56
+          FlatButton(
57
+            onPressed: () => nav.pushNamed('/form'),
58
+            child: Text('Embedded in a form'),
59
+            color: Colors.lightBlue,
60
+            textColor: Colors.white,
61
+          ),
62
+          Expanded(child: Container()),
63
+        ],
93
       ),
64
       ),
94
     );
65
     );
95
   }
66
   }
96
-
97
-  void _startEditing() {
98
-    setState(() {
99
-      _editing = true;
100
-    });
101
-  }
102
-
103
-  void _stopEditing() {
104
-    setState(() {
105
-      _editing = false;
106
-    });
107
-  }
108
-}
109
-
110
-/// Custom image delegate used by this example to load image from application
111
-/// assets.
112
-///
113
-/// Default image delegate only supports [FileImage]s.
114
-class CustomImageDelegate extends ZefyrDefaultImageDelegate {
115
-  @override
116
-  Widget buildImage(BuildContext context, String imageSource) {
117
-    // We use custom "asset" scheme to distinguish asset images from other files.
118
-    if (imageSource.startsWith('asset://')) {
119
-      final asset = new AssetImage(imageSource.replaceFirst('asset://', ''));
120
-      return new Image(image: asset);
121
-    } else {
122
-      return super.buildImage(context, imageSource);
123
-    }
124
-  }
125
 }
67
 }

+ 65
- 0
packages/zefyr/example/lib/src/form.dart View File

1
+import 'package:flutter/material.dart';
2
+import 'package:zefyr/zefyr.dart';
3
+
4
+import 'full_page.dart';
5
+
6
+class FormEmbeddedScreen extends StatefulWidget {
7
+  @override
8
+  _FormEmbeddedScreenState createState() => _FormEmbeddedScreenState();
9
+}
10
+
11
+class _FormEmbeddedScreenState extends State<FormEmbeddedScreen> {
12
+  final ZefyrController _controller = ZefyrController(NotusDocument());
13
+  final FocusNode _focusNode = new FocusNode();
14
+
15
+  @override
16
+  Widget build(BuildContext context) {
17
+    final form = ListView(
18
+      children: <Widget>[
19
+        TextField(decoration: InputDecoration(labelText: 'Name')),
20
+        buildEditor(),
21
+        TextField(decoration: InputDecoration(labelText: 'Email')),
22
+      ],
23
+    );
24
+
25
+    return Scaffold(
26
+      resizeToAvoidBottomPadding: true,
27
+      appBar: AppBar(
28
+        elevation: 1.0,
29
+        backgroundColor: Colors.grey.shade200,
30
+        brightness: Brightness.light,
31
+        title: ZefyrLogo(),
32
+      ),
33
+      body: ZefyrScaffold(
34
+        child: Padding(
35
+          padding: const EdgeInsets.all(8.0),
36
+          child: form,
37
+        ),
38
+      ),
39
+    );
40
+  }
41
+
42
+  Widget buildEditor() {
43
+    final theme = new ZefyrThemeData(
44
+      toolbarTheme: ZefyrToolbarTheme.fallback(context).copyWith(
45
+        color: Colors.grey.shade800,
46
+        toggleColor: Colors.grey.shade900,
47
+        iconColor: Colors.white,
48
+        disabledIconColor: Colors.grey.shade500,
49
+      ),
50
+    );
51
+
52
+    return ZefyrTheme(
53
+      data: theme,
54
+      child: ZefyrField(
55
+        height: 200.0,
56
+        decoration: InputDecoration(labelText: 'Description'),
57
+        controller: _controller,
58
+        focusNode: _focusNode,
59
+        autofocus: false,
60
+        imageDelegate: new CustomImageDelegate(),
61
+        physics: ClampingScrollPhysics(),
62
+      ),
63
+    );
64
+  }
65
+}

+ 108
- 0
packages/zefyr/example/lib/src/full_page.dart View File

1
+import 'dart:convert';
2
+
3
+import 'package:flutter/material.dart';
4
+import 'package:quill_delta/quill_delta.dart';
5
+import 'package:zefyr/zefyr.dart';
6
+
7
+class ZefyrLogo extends StatelessWidget {
8
+  @override
9
+  Widget build(BuildContext context) {
10
+    return Row(
11
+      mainAxisAlignment: MainAxisAlignment.center,
12
+      children: <Widget>[
13
+        Text('Ze'),
14
+        FlutterLogo(size: 24.0),
15
+        Text('yr'),
16
+      ],
17
+    );
18
+  }
19
+}
20
+
21
+class FullPageEditorScreen extends StatefulWidget {
22
+  @override
23
+  _FullPageEditorScreenState createState() => new _FullPageEditorScreenState();
24
+}
25
+
26
+final doc =
27
+    r'[{"insert":"Zefyr"},{"insert":"\n","attributes":{"heading":1}},{"insert":"Soft and gentle rich text editing for Flutter applications.","attributes":{"i":true}},{"insert":"\n"},{"insert":"​","attributes":{"embed":{"type":"image","source":"asset://images/breeze.jpg"}}},{"insert":"\n"},{"insert":"Photo by Hiroyuki Takeda.","attributes":{"i":true}},{"insert":"\nZefyr is currently in "},{"insert":"early preview","attributes":{"b":true}},{"insert":". If you have a feature request or found a bug, please file it at the "},{"insert":"issue tracker","attributes":{"a":"https://github.com/memspace/zefyr/issues"}},{"insert":'
28
+    r'".\nDocumentation"},{"insert":"\n","attributes":{"heading":3}},{"insert":"Quick Start","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/quick_start.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Data Format and Document Model","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/data_and_document.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Style Attributes","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/attr'
29
+    r'ibutes.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Heuristic Rules","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/heuristics.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"FAQ","attributes":{"a":"https://github.com/memspace/zefyr/blob/master/doc/faq.md"}},{"insert":"\n","attributes":{"block":"ul"}},{"insert":"Clean and modern look"},{"insert":"\n","attributes":{"heading":2}},{"insert":"Zefyr’s rich text editor is built with simplicity and fle'
30
+    r'xibility in mind. It provides clean interface for distraction-free editing. Think Medium.com-like experience.\nMarkdown inspired semantics"},{"insert":"\n","attributes":{"heading":2}},{"insert":"Ever needed to have a heading line inside of a quote block, like this:\nI’m a Markdown heading"},{"insert":"\n","attributes":{"block":"quote","heading":3}},{"insert":"And I’m a regular paragraph"},{"insert":"\n","attributes":{"block":"quote"}},{"insert":"Code blocks"},{"insert":"\n","attributes":{"headin'
31
+    r'g":2}},{"insert":"Of course:\nimport ‘package:flutter/material.dart’;"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"import ‘package:zefyr/zefyr.dart’;"},{"insert":"\n\n","attributes":{"block":"code"}},{"insert":"void main() {"},{"insert":"\n","attributes":{"block":"code"}},{"insert":" runApp(MyZefyrApp());"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"}"},{"insert":"\n","attributes":{"block":"code"}},{"insert":"\n\n\n"}]';
32
+
33
+Delta getDelta() {
34
+  return Delta.fromJson(json.decode(doc));
35
+}
36
+
37
+class _FullPageEditorScreenState extends State<FullPageEditorScreen> {
38
+  final ZefyrController _controller =
39
+      ZefyrController(NotusDocument.fromDelta(getDelta()));
40
+  final FocusNode _focusNode = new FocusNode();
41
+  bool _editing = false;
42
+
43
+  @override
44
+  Widget build(BuildContext context) {
45
+    final theme = new ZefyrThemeData(
46
+      toolbarTheme: ZefyrToolbarTheme.fallback(context).copyWith(
47
+        color: Colors.grey.shade800,
48
+        toggleColor: Colors.grey.shade900,
49
+        iconColor: Colors.white,
50
+        disabledIconColor: Colors.grey.shade500,
51
+      ),
52
+    );
53
+
54
+    final done = _editing
55
+        ? [new FlatButton(onPressed: _stopEditing, child: Text('DONE'))]
56
+        : [new FlatButton(onPressed: _startEditing, child: Text('EDIT'))];
57
+    return Scaffold(
58
+      resizeToAvoidBottomPadding: true,
59
+      appBar: AppBar(
60
+        elevation: 1.0,
61
+        backgroundColor: Colors.grey.shade200,
62
+        brightness: Brightness.light,
63
+        title: ZefyrLogo(),
64
+        actions: done,
65
+      ),
66
+      body: ZefyrScaffold(
67
+        child: ZefyrTheme(
68
+          data: theme,
69
+          child: ZefyrEditor(
70
+            controller: _controller,
71
+            focusNode: _focusNode,
72
+            enabled: _editing,
73
+            imageDelegate: new CustomImageDelegate(),
74
+          ),
75
+        ),
76
+      ),
77
+    );
78
+  }
79
+
80
+  void _startEditing() {
81
+    setState(() {
82
+      _editing = true;
83
+    });
84
+  }
85
+
86
+  void _stopEditing() {
87
+    setState(() {
88
+      _editing = false;
89
+    });
90
+  }
91
+}
92
+
93
+/// Custom image delegate used by this example to load image from application
94
+/// assets.
95
+///
96
+/// Default image delegate only supports [FileImage]s.
97
+class CustomImageDelegate extends ZefyrDefaultImageDelegate {
98
+  @override
99
+  Widget buildImage(BuildContext context, String imageSource) {
100
+    // We use custom "asset" scheme to distinguish asset images from other files.
101
+    if (imageSource.startsWith('asset://')) {
102
+      final asset = new AssetImage(imageSource.replaceFirst('asset://', ''));
103
+      return new Image(image: asset);
104
+    } else {
105
+      return super.buildImage(context, imageSource);
106
+    }
107
+  }
108
+}

+ 36
- 20
packages/zefyr/lib/src/widgets/buttons.dart View File

300
 }
300
 }
301
 
301
 
302
 class _LinkButtonState extends State<LinkButton> {
302
 class _LinkButtonState extends State<LinkButton> {
303
-  final TextEditingController _inputController = new TextEditingController();
303
+  final TextEditingController _inputController = TextEditingController();
304
   Key _inputKey;
304
   Key _inputKey;
305
   bool _formatError = false;
305
   bool _formatError = false;
306
+  ZefyrEditorScope _editor;
307
+
306
   bool get isEditing => _inputKey != null;
308
   bool get isEditing => _inputKey != null;
307
 
309
 
308
   @override
310
   @override
376
         _inputController.text = '';
378
         _inputController.text = '';
377
         _inputController.removeListener(_handleInputChange);
379
         _inputController.removeListener(_handleInputChange);
378
         toolbar.markNeedsRebuild();
380
         toolbar.markNeedsRebuild();
379
-        toolbar.editor.focus(context);
381
+        toolbar.editor.focus();
380
       }
382
       }
381
     });
383
     });
382
   }
384
   }
388
         _inputKey = null;
390
         _inputKey = null;
389
         _inputController.text = '';
391
         _inputController.text = '';
390
         _inputController.removeListener(_handleInputChange);
392
         _inputController.removeListener(_handleInputChange);
391
-        editor.focus(context);
393
+        editor.focus();
392
       });
394
       });
393
     }
395
     }
394
   }
396
   }
437
         : _LinkInput(
439
         : _LinkInput(
438
             key: _inputKey,
440
             key: _inputKey,
439
             controller: _inputController,
441
             controller: _inputController,
440
-            focusNode: toolbar.editor.toolbarFocusNode,
441
             formatError: _formatError,
442
             formatError: _formatError,
442
           );
443
           );
443
     final items = <Widget>[Expanded(child: body)];
444
     final items = <Widget>[Expanded(child: body)];
474
 }
475
 }
475
 
476
 
476
 class _LinkInput extends StatefulWidget {
477
 class _LinkInput extends StatefulWidget {
477
-  final FocusNode focusNode;
478
   final TextEditingController controller;
478
   final TextEditingController controller;
479
   final bool formatError;
479
   final bool formatError;
480
 
480
 
481
-  const _LinkInput({
482
-    Key key,
483
-    @required this.focusNode,
484
-    @required this.controller,
485
-    this.formatError: false,
486
-  }) : super(key: key);
481
+  const _LinkInput(
482
+      {Key key, @required this.controller, this.formatError: false})
483
+      : super(key: key);
484
+
487
   @override
485
   @override
488
   _LinkInputState createState() {
486
   _LinkInputState createState() {
489
     return new _LinkInputState();
487
     return new _LinkInputState();
491
 }
489
 }
492
 
490
 
493
 class _LinkInputState extends State<_LinkInput> {
491
 class _LinkInputState extends State<_LinkInput> {
492
+  final FocusNode _focusNode = FocusNode();
493
+
494
+  ZefyrEditorScope _editor;
494
   bool _didAutoFocus = false;
495
   bool _didAutoFocus = false;
495
 
496
 
496
   @override
497
   @override
497
   void didChangeDependencies() {
498
   void didChangeDependencies() {
498
     super.didChangeDependencies();
499
     super.didChangeDependencies();
499
     if (!_didAutoFocus) {
500
     if (!_didAutoFocus) {
500
-      FocusScope.of(context).requestFocus(widget.focusNode);
501
+      FocusScope.of(context).requestFocus(_focusNode);
501
       _didAutoFocus = true;
502
       _didAutoFocus = true;
502
     }
503
     }
504
+
505
+    final toolbar = ZefyrToolbar.of(context);
506
+
507
+    if (_editor != toolbar.editor) {
508
+      _editor?.setToolbarFocusNode(null);
509
+      _editor = toolbar.editor;
510
+      _editor.setToolbarFocusNode(_focusNode);
511
+    }
503
   }
512
   }
504
 
513
 
505
   @override
514
   @override
506
-  Widget build(BuildContext context) {
507
-    FocusScope.of(context).reparentIfNeeded(widget.focusNode);
515
+  void dispose() {
516
+    _editor?.setToolbarFocusNode(null);
517
+    _focusNode.dispose();
518
+    _editor = null;
519
+    super.dispose();
520
+  }
508
 
521
 
522
+  @override
523
+  Widget build(BuildContext context) {
509
     final theme = Theme.of(context);
524
     final theme = Theme.of(context);
510
     final toolbarTheme = ZefyrTheme.of(context).toolbarTheme;
525
     final toolbarTheme = ZefyrTheme.of(context).toolbarTheme;
511
     final color =
526
     final color =
514
     return TextField(
529
     return TextField(
515
       style: style,
530
       style: style,
516
       keyboardType: TextInputType.url,
531
       keyboardType: TextInputType.url,
517
-      focusNode: widget.focusNode,
532
+      focusNode: _focusNode,
518
       controller: widget.controller,
533
       controller: widget.controller,
519
       autofocus: true,
534
       autofocus: true,
520
       decoration: new InputDecoration(
535
       decoration: new InputDecoration(
521
-          hintText: 'https://',
522
-          filled: true,
523
-          fillColor: toolbarTheme.color,
524
-          border: InputBorder.none,
525
-          contentPadding: const EdgeInsets.all(10.0)),
536
+        hintText: 'https://',
537
+        filled: true,
538
+        fillColor: toolbarTheme.color,
539
+        border: InputBorder.none,
540
+        contentPadding: const EdgeInsets.all(10.0),
541
+      ),
526
     );
542
     );
527
   }
543
   }
528
 }
544
 }

+ 3
- 2
packages/zefyr/lib/src/widgets/editable_text.dart View File

36
     this.autofocus: true,
36
     this.autofocus: true,
37
     this.enabled: true,
37
     this.enabled: true,
38
     this.padding: const EdgeInsets.symmetric(horizontal: 16.0),
38
     this.padding: const EdgeInsets.symmetric(horizontal: 16.0),
39
+    this.physics,
39
   }) : super(key: key);
40
   }) : super(key: key);
40
 
41
 
41
   final ZefyrController controller;
42
   final ZefyrController controller;
43
   final ZefyrImageDelegate imageDelegate;
44
   final ZefyrImageDelegate imageDelegate;
44
   final bool autofocus;
45
   final bool autofocus;
45
   final bool enabled;
46
   final bool enabled;
47
+  final ScrollPhysics physics;
46
 
48
 
47
   /// Padding around editable area.
49
   /// Padding around editable area.
48
   final EdgeInsets padding;
50
   final EdgeInsets padding;
132
       body = new Padding(padding: widget.padding, child: body);
134
       body = new Padding(padding: widget.padding, child: body);
133
     }
135
     }
134
     final scrollable = SingleChildScrollView(
136
     final scrollable = SingleChildScrollView(
135
-      padding: EdgeInsets.only(top: 16.0),
136
-      physics: AlwaysScrollableScrollPhysics(),
137
+      physics: widget.physics,
137
       controller: _scrollController,
138
       controller: _scrollController,
138
       child: body,
139
       child: body,
139
     );
140
     );

+ 101
- 35
packages/zefyr/lib/src/widgets/editor.dart View File

7
 import 'controller.dart';
7
 import 'controller.dart';
8
 import 'editable_text.dart';
8
 import 'editable_text.dart';
9
 import 'image.dart';
9
 import 'image.dart';
10
+import 'scaffold.dart';
10
 import 'theme.dart';
11
 import 'theme.dart';
11
 import 'toolbar.dart';
12
 import 'toolbar.dart';
12
 
13
 
15
     @required ZefyrImageDelegate imageDelegate,
16
     @required ZefyrImageDelegate imageDelegate,
16
     @required ZefyrController controller,
17
     @required ZefyrController controller,
17
     @required FocusNode focusNode,
18
     @required FocusNode focusNode,
18
-    @required FocusNode toolbarFocusNode,
19
+    @required FocusScopeNode focusScope,
19
   })  : _controller = controller,
20
   })  : _controller = controller,
20
         _imageDelegate = imageDelegate,
21
         _imageDelegate = imageDelegate,
21
-        _focusNode = focusNode,
22
-        _toolbarFocusNode = toolbarFocusNode {
22
+        _focusScope = focusScope,
23
+        _focusNode = focusNode {
23
     _selectionStyle = _controller.getSelectionStyle();
24
     _selectionStyle = _controller.getSelectionStyle();
24
     _selection = _controller.selection;
25
     _selection = _controller.selection;
25
     _controller.addListener(_handleControllerChange);
26
     _controller.addListener(_handleControllerChange);
26
-    toolbarFocusNode.addListener(_handleFocusChange);
27
     _focusNode.addListener(_handleFocusChange);
27
     _focusNode.addListener(_handleFocusChange);
28
   }
28
   }
29
 
29
 
32
   ZefyrImageDelegate _imageDelegate;
32
   ZefyrImageDelegate _imageDelegate;
33
   ZefyrImageDelegate get imageDelegate => _imageDelegate;
33
   ZefyrImageDelegate get imageDelegate => _imageDelegate;
34
 
34
 
35
+  FocusScopeNode _focusScope;
35
   FocusNode _focusNode;
36
   FocusNode _focusNode;
36
-  FocusNode _toolbarFocusNode;
37
-  FocusNode get toolbarFocusNode => _toolbarFocusNode;
38
 
37
 
39
   ZefyrController _controller;
38
   ZefyrController _controller;
40
   NotusStyle get selectionStyle => _selectionStyle;
39
   NotusStyle get selectionStyle => _selectionStyle;
46
   void dispose() {
45
   void dispose() {
47
     assert(!_disposed);
46
     assert(!_disposed);
48
     _controller.removeListener(_handleControllerChange);
47
     _controller.removeListener(_handleControllerChange);
49
-    _toolbarFocusNode.removeListener(_handleFocusChange);
50
     _focusNode.removeListener(_handleFocusChange);
48
     _focusNode.removeListener(_handleFocusChange);
51
     _disposed = true;
49
     _disposed = true;
52
     super.dispose();
50
     super.dispose();
102
     notifyListeners();
100
     notifyListeners();
103
   }
101
   }
104
 
102
 
103
+  FocusNode _toolbarFocusNode;
104
+
105
+  void setToolbarFocusNode(FocusNode node) {
106
+    assert(!_disposed || node == null);
107
+    if (_toolbarFocusNode != node) {
108
+      _toolbarFocusNode?.removeListener(_handleFocusChange);
109
+      _toolbarFocusNode = node;
110
+      _toolbarFocusNode?.addListener(_handleFocusChange);
111
+      // We do not notify listeners here because it will happen when
112
+      // focus state changes, see [_handleFocusChange].
113
+    }
114
+  }
115
+
105
   FocusOwner get focusOwner {
116
   FocusOwner get focusOwner {
106
     assert(!_disposed);
117
     assert(!_disposed);
107
     if (_focusNode.hasFocus) {
118
     if (_focusNode.hasFocus) {
108
       return FocusOwner.editor;
119
       return FocusOwner.editor;
109
-    } else if (toolbarFocusNode.hasFocus) {
120
+    } else if (_toolbarFocusNode?.hasFocus == true) {
110
       return FocusOwner.toolbar;
121
       return FocusOwner.toolbar;
111
     } else {
122
     } else {
112
       return FocusOwner.none;
123
       return FocusOwner.none;
124
     _controller.formatSelection(value);
135
     _controller.formatSelection(value);
125
   }
136
   }
126
 
137
 
127
-  void focus(BuildContext context) {
138
+  void focus() {
128
     assert(!_disposed);
139
     assert(!_disposed);
129
-    FocusScope.of(context).requestFocus(_focusNode);
140
+    _focusScope.requestFocus(_focusNode);
130
   }
141
   }
131
 
142
 
132
   void hideKeyboard() {
143
   void hideKeyboard() {
158
     this.padding: const EdgeInsets.symmetric(horizontal: 16.0),
169
     this.padding: const EdgeInsets.symmetric(horizontal: 16.0),
159
     this.toolbarDelegate,
170
     this.toolbarDelegate,
160
     this.imageDelegate,
171
     this.imageDelegate,
172
+    this.physics,
161
   }) : super(key: key);
173
   }) : super(key: key);
162
 
174
 
163
   final ZefyrController controller;
175
   final ZefyrController controller;
166
   final bool enabled;
178
   final bool enabled;
167
   final ZefyrToolbarDelegate toolbarDelegate;
179
   final ZefyrToolbarDelegate toolbarDelegate;
168
   final ZefyrImageDelegate imageDelegate;
180
   final ZefyrImageDelegate imageDelegate;
181
+  final ScrollPhysics physics;
169
 
182
 
170
   /// Padding around editable area.
183
   /// Padding around editable area.
171
   final EdgeInsets padding;
184
   final EdgeInsets padding;
181
 }
194
 }
182
 
195
 
183
 class _ZefyrEditorState extends State<ZefyrEditor> {
196
 class _ZefyrEditorState extends State<ZefyrEditor> {
184
-  final FocusNode _toolbarFocusNode = new FocusNode();
185
   ZefyrImageDelegate _imageDelegate;
197
   ZefyrImageDelegate _imageDelegate;
186
   ZefyrEditorScope _scope;
198
   ZefyrEditorScope _scope;
199
+  ZefyrThemeData _themeData;
200
+  GlobalKey<ZefyrToolbarState> _toolbarKey;
201
+  ZefyrScaffoldState _scaffold;
202
+
203
+  bool get hasToolbar => _toolbarKey != null;
204
+
205
+  void showToolbar() {
206
+    assert(_toolbarKey == null);
207
+    _toolbarKey = GlobalKey();
208
+    _scaffold.showToolbar(buildToolbar);
209
+  }
210
+
211
+  void hideToolbar() {
212
+    if (_toolbarKey == null) return;
213
+    _scaffold.hideToolbar();
214
+    _toolbarKey = null;
215
+  }
216
+
217
+  Widget buildToolbar(BuildContext) {
218
+    return ZefyrTheme(
219
+      data: _themeData,
220
+      child: ZefyrToolbar(
221
+        key: _toolbarKey,
222
+        editor: _scope,
223
+        delegate: widget.toolbarDelegate,
224
+      ),
225
+    );
226
+  }
227
+
228
+  void _handleChange() {
229
+    if (_scope.focusOwner == FocusOwner.none) {
230
+      hideToolbar();
231
+    } else if (!hasToolbar) {
232
+      showToolbar();
233
+    } else {
234
+      WidgetsBinding.instance.addPostFrameCallback((_) {
235
+        _toolbarKey?.currentState?.markNeedsRebuild();
236
+      });
237
+    }
238
+  }
187
 
239
 
188
   @override
240
   @override
189
   void initState() {
241
   void initState() {
190
     super.initState();
242
     super.initState();
191
     _imageDelegate = widget.imageDelegate ?? new ZefyrDefaultImageDelegate();
243
     _imageDelegate = widget.imageDelegate ?? new ZefyrDefaultImageDelegate();
192
-    _scope = ZefyrEditorScope(
193
-      toolbarFocusNode: _toolbarFocusNode,
194
-      imageDelegate: _imageDelegate,
195
-      controller: widget.controller,
196
-      focusNode: widget.focusNode,
197
-    );
198
   }
244
   }
199
 
245
 
200
   @override
246
   @override
208
     }
254
     }
209
   }
255
   }
210
 
256
 
257
+  @override
258
+  void didChangeDependencies() {
259
+    super.didChangeDependencies();
260
+    final parentTheme = ZefyrTheme.of(context, nullOk: true);
261
+    final fallbackTheme = ZefyrThemeData.fallback(context);
262
+    _themeData = (parentTheme != null)
263
+        ? fallbackTheme.merge(parentTheme)
264
+        : fallbackTheme;
265
+
266
+    if (_scope == null) {
267
+      _scope = ZefyrEditorScope(
268
+        imageDelegate: _imageDelegate,
269
+        controller: widget.controller,
270
+        focusNode: widget.focusNode,
271
+        focusScope: FocusScope.of(context),
272
+      );
273
+      _scope.addListener(_handleChange);
274
+    } else {
275
+      final focusScope = FocusScope.of(context);
276
+      if (focusScope != _scope._focusScope) {
277
+        _scope._focusScope = focusScope;
278
+      }
279
+    }
280
+
281
+    final scaffold = ZefyrScaffold.of(context);
282
+    if (_scaffold != scaffold) {
283
+      bool didHaveToolbar = hasToolbar;
284
+      hideToolbar();
285
+      _scaffold = scaffold;
286
+      if (didHaveToolbar) showToolbar();
287
+    }
288
+  }
289
+
211
   @override
290
   @override
212
   void dispose() {
291
   void dispose() {
292
+    hideToolbar();
293
+    _scope.removeListener(_handleChange);
213
     _scope.dispose();
294
     _scope.dispose();
214
-    _toolbarFocusNode.dispose();
215
     super.dispose();
295
     super.dispose();
216
   }
296
   }
217
 
297
 
224
       autofocus: widget.autofocus,
304
       autofocus: widget.autofocus,
225
       enabled: widget.enabled,
305
       enabled: widget.enabled,
226
       padding: widget.padding,
306
       padding: widget.padding,
307
+      physics: widget.physics,
227
     );
308
     );
228
 
309
 
229
-    final children = <Widget>[];
230
-    children.add(Expanded(child: editable));
231
-    final toolbar = ZefyrToolbar(
232
-      editor: _scope,
233
-      focusNode: _toolbarFocusNode,
234
-      delegate: widget.toolbarDelegate,
235
-    );
236
-    children.add(toolbar);
237
-
238
-    final parentTheme = ZefyrTheme.of(context, nullOk: true);
239
-    final fallbackTheme = ZefyrThemeData.fallback(context);
240
-    final actualTheme = (parentTheme != null)
241
-        ? fallbackTheme.merge(parentTheme)
242
-        : fallbackTheme;
243
-
244
     return ZefyrTheme(
310
     return ZefyrTheme(
245
-      data: actualTheme,
311
+      data: _themeData,
246
       child: _ZefyrEditorScope(
312
       child: _ZefyrEditorScope(
247
         scope: _scope,
313
         scope: _scope,
248
-        child: Column(children: children),
314
+        child: editable,
249
       ),
315
       ),
250
     );
316
     );
251
   }
317
   }

+ 86
- 0
packages/zefyr/lib/src/widgets/field.dart View File

1
+import 'package:flutter/material.dart';
2
+
3
+import 'controller.dart';
4
+import 'editor.dart';
5
+import 'image.dart';
6
+import 'toolbar.dart';
7
+
8
+/// Zefyr editor with material design decorations.
9
+class ZefyrField extends StatefulWidget {
10
+  /// Decoration to paint around this editor.
11
+  final InputDecoration decoration;
12
+
13
+  /// Height of this editor field.
14
+  final double height;
15
+  final ZefyrController controller;
16
+  final FocusNode focusNode;
17
+  final bool autofocus;
18
+  final bool enabled;
19
+  final ZefyrToolbarDelegate toolbarDelegate;
20
+  final ZefyrImageDelegate imageDelegate;
21
+  final ScrollPhysics physics;
22
+
23
+  const ZefyrField({
24
+    Key key,
25
+    this.decoration,
26
+    this.height,
27
+    this.controller,
28
+    this.focusNode,
29
+    this.autofocus: false,
30
+    this.enabled,
31
+    this.toolbarDelegate,
32
+    this.imageDelegate,
33
+    this.physics,
34
+  }) : super(key: key);
35
+
36
+  @override
37
+  _ZefyrFieldState createState() => _ZefyrFieldState();
38
+}
39
+
40
+class _ZefyrFieldState extends State<ZefyrField> {
41
+  @override
42
+  Widget build(BuildContext context) {
43
+    Widget child = ZefyrEditor(
44
+      padding: EdgeInsets.symmetric(vertical: 6.0),
45
+      controller: widget.controller,
46
+      focusNode: widget.focusNode,
47
+      autofocus: widget.autofocus,
48
+      enabled: widget.enabled ?? true,
49
+      toolbarDelegate: widget.toolbarDelegate,
50
+      imageDelegate: widget.imageDelegate,
51
+      physics: widget.physics,
52
+    );
53
+
54
+    if (widget.height != null) {
55
+      child = ConstrainedBox(
56
+        constraints: BoxConstraints.tightFor(height: widget.height),
57
+        child: child,
58
+      );
59
+    }
60
+
61
+    return AnimatedBuilder(
62
+      animation:
63
+          Listenable.merge(<Listenable>[widget.focusNode, widget.controller]),
64
+      builder: (BuildContext context, Widget child) {
65
+        return InputDecorator(
66
+          decoration: _getEffectiveDecoration(),
67
+          isFocused: widget.focusNode.hasFocus,
68
+          isEmpty: widget.controller.document.length == 1,
69
+          child: child,
70
+        );
71
+      },
72
+      child: child,
73
+    );
74
+  }
75
+
76
+  InputDecoration _getEffectiveDecoration() {
77
+    final InputDecoration effectiveDecoration =
78
+        (widget.decoration ?? const InputDecoration())
79
+            .applyDefaults(Theme.of(context).inputDecorationTheme)
80
+            .copyWith(
81
+              enabled: widget.enabled ?? true,
82
+            );
83
+
84
+    return effectiveDecoration;
85
+  }
86
+}

+ 60
- 0
packages/zefyr/lib/src/widgets/scaffold.dart View File

1
+import 'package:flutter/material.dart';
2
+
3
+class ZefyrScaffold extends StatefulWidget {
4
+  final Widget child;
5
+
6
+  const ZefyrScaffold({Key key, this.child}) : super(key: key);
7
+
8
+  static ZefyrScaffoldState of(BuildContext context) {
9
+    final _ZefyrScaffoldAccess widget =
10
+        context.inheritFromWidgetOfExactType(_ZefyrScaffoldAccess);
11
+    return widget.scaffold;
12
+  }
13
+
14
+  @override
15
+  ZefyrScaffoldState createState() => ZefyrScaffoldState();
16
+}
17
+
18
+class ZefyrScaffoldState extends State<ZefyrScaffold> {
19
+  WidgetBuilder _toolbarBuilder;
20
+
21
+  void showToolbar(WidgetBuilder builder) {
22
+    setState(() {
23
+      _toolbarBuilder = builder;
24
+    });
25
+  }
26
+
27
+  void hideToolbar() {
28
+    if (_toolbarBuilder != null) {
29
+      setState(() {
30
+        _toolbarBuilder = null;
31
+      });
32
+    }
33
+  }
34
+
35
+  @override
36
+  Widget build(BuildContext context) {
37
+    final toolbar =
38
+        (_toolbarBuilder == null) ? Container() : _toolbarBuilder(context);
39
+    return _ZefyrScaffoldAccess(
40
+      scaffold: this,
41
+      child: Column(
42
+        children: <Widget>[
43
+          Expanded(child: widget.child),
44
+          toolbar,
45
+        ],
46
+      ),
47
+    );
48
+  }
49
+}
50
+
51
+class _ZefyrScaffoldAccess extends InheritedWidget {
52
+  final ZefyrScaffoldState scaffold;
53
+
54
+  _ZefyrScaffoldAccess({Widget child, this.scaffold}) : super(child: child);
55
+
56
+  @override
57
+  bool updateShouldNotify(_ZefyrScaffoldAccess oldWidget) {
58
+    return oldWidget.scaffold != scaffold;
59
+  }
60
+}

+ 14
- 14
packages/zefyr/lib/src/widgets/selection.dart View File

66
 
66
 
67
   @override
67
   @override
68
   void hideToolbar() {
68
   void hideToolbar() {
69
+    _didCaretTap = false; // reset double tap.
69
     _toolbar?.remove();
70
     _toolbar?.remove();
70
     _toolbar = null;
71
     _toolbar = null;
71
     _toolbarController.stop();
72
     _toolbarController.stop();
82
               editable: editable,
83
               editable: editable,
83
               controls: widget.controls,
84
               controls: widget.controls,
84
               delegate: this,
85
               delegate: this,
85
-              visible: true,
86
             ),
86
             ),
87
           ),
87
           ),
88
     );
88
     );
99
     super.initState();
99
     super.initState();
100
     _toolbarController = new AnimationController(
100
     _toolbarController = new AnimationController(
101
         duration: _kFadeDuration, vsync: widget.overlay);
101
         duration: _kFadeDuration, vsync: widget.overlay);
102
-    _selection = widget.controller.selection;
103
-    widget.controller.addListener(_handleChange);
104
   }
102
   }
105
 
103
 
106
   static const Duration _kFadeDuration = const Duration(milliseconds: 150);
104
   static const Duration _kFadeDuration = const Duration(milliseconds: 150);
114
       _toolbarController = new AnimationController(
112
       _toolbarController = new AnimationController(
115
           duration: _kFadeDuration, vsync: widget.overlay);
113
           duration: _kFadeDuration, vsync: widget.overlay);
116
     }
114
     }
117
-    if (oldWidget.controller != widget.controller) {
118
-      oldWidget.controller.removeListener(_handleChange);
119
-      widget.controller.addListener(_handleChange);
120
-    }
121
   }
115
   }
122
 
116
 
123
   @override
117
   @override
124
   void didChangeDependencies() {
118
   void didChangeDependencies() {
125
     super.didChangeDependencies();
119
     super.didChangeDependencies();
126
-    WidgetsBinding.instance.addPostFrameCallback((_) {
127
-      _updateToolbar();
128
-    });
120
+    final editor = ZefyrEditor.of(context);
121
+    if (_editor != editor) {
122
+      _editor?.removeListener(_handleChange);
123
+      _editor = editor;
124
+      _editor.addListener(_handleChange);
125
+      _selection = _editor.selection;
126
+      _focusOwner = _editor.focusOwner;
127
+    }
129
   }
128
   }
130
 
129
 
131
   @override
130
   @override
132
   void dispose() {
131
   void dispose() {
133
-    widget.controller.removeListener(_handleChange);
132
+    _editor.removeListener(_handleChange);
134
     hideToolbar();
133
     hideToolbar();
135
     _toolbarController.dispose();
134
     _toolbarController.dispose();
136
     _toolbarController = null;
135
     _toolbarController = null;
175
   OverlayEntry _toolbar;
174
   OverlayEntry _toolbar;
176
   AnimationController _toolbarController;
175
   AnimationController _toolbarController;
177
 
176
 
177
+  ZefyrEditorScope _editor;
178
   TextSelection _selection;
178
   TextSelection _selection;
179
+  FocusOwner _focusOwner;
179
 
180
 
180
   bool _didCaretTap = false;
181
   bool _didCaretTap = false;
181
 
182
 
182
   void _handleChange() {
183
   void _handleChange() {
183
-    if (_selection != widget.controller.selection) {
184
+    if (_selection != _editor.selection || _focusOwner != _editor.focusOwner) {
184
       _updateToolbar();
185
       _updateToolbar();
185
     }
186
     }
186
   }
187
   }
209
         }
210
         }
210
       }
211
       }
211
       _selection = selection;
212
       _selection = selection;
213
+      _focusOwner = focusOwner;
212
     });
214
     });
213
   }
215
   }
214
 
216
 
423
     @required this.editable,
425
     @required this.editable,
424
     @required this.controls,
426
     @required this.controls,
425
     @required this.delegate,
427
     @required this.delegate,
426
-    @required this.visible,
427
   }) : super(key: key);
428
   }) : super(key: key);
428
 
429
 
429
   final ZefyrEditableTextScope editable;
430
   final ZefyrEditableTextScope editable;
430
   final TextSelectionControls controls;
431
   final TextSelectionControls controls;
431
   final TextSelectionDelegate delegate;
432
   final TextSelectionDelegate delegate;
432
-  final bool visible;
433
 
433
 
434
   @override
434
   @override
435
   _SelectionToolbarState createState() => new _SelectionToolbarState();
435
   _SelectionToolbarState createState() => new _SelectionToolbarState();

+ 1
- 1
packages/zefyr/lib/src/widgets/theme.dart View File

172
           height: 1.25,
172
           height: 1.25,
173
           fontWeight: FontWeight.w600,
173
           fontWeight: FontWeight.w600,
174
         ),
174
         ),
175
-        padding: EdgeInsets.only(bottom: 16.0),
175
+        padding: EdgeInsets.only(top: 16.0, bottom: 16.0),
176
       ),
176
       ),
177
       level2: StyleTheme(
177
       level2: StyleTheme(
178
         textStyle: TextStyle(
178
         textStyle: TextStyle(

+ 9
- 23
packages/zefyr/lib/src/widgets/toolbar.dart View File

8
 import 'package:notus/notus.dart';
8
 import 'package:notus/notus.dart';
9
 
9
 
10
 import 'buttons.dart';
10
 import 'buttons.dart';
11
-import 'controller.dart';
12
 import 'editor.dart';
11
 import 'editor.dart';
13
 import 'theme.dart';
12
 import 'theme.dart';
14
 
13
 
102
 
101
 
103
   const ZefyrToolbar({
102
   const ZefyrToolbar({
104
     Key key,
103
     Key key,
105
-    @required this.focusNode,
106
     @required this.editor,
104
     @required this.editor,
107
     this.autoHide: true,
105
     this.autoHide: true,
108
     this.delegate,
106
     this.delegate,
109
   }) : super(key: key);
107
   }) : super(key: key);
110
 
108
 
111
-  final FocusNode focusNode;
112
   final ZefyrToolbarDelegate delegate;
109
   final ZefyrToolbarDelegate delegate;
113
   final ZefyrEditorScope editor;
110
   final ZefyrEditorScope editor;
114
 
111
 
153
   TextSelection _selection;
150
   TextSelection _selection;
154
 
151
 
155
   void markNeedsRebuild() {
152
   void markNeedsRebuild() {
156
-    setState(() {});
153
+    setState(() {
154
+      if (_selection != editor.selection) {
155
+        _selection = editor.selection;
156
+        closeOverlay();
157
+      }
158
+    });
157
   }
159
   }
158
 
160
 
159
   Widget buildButton(BuildContext context, ZefyrToolbarAction action,
161
   Widget buildButton(BuildContext context, ZefyrToolbarAction action,
187
 
189
 
188
   ZefyrEditorScope get editor => widget.editor;
190
   ZefyrEditorScope get editor => widget.editor;
189
 
191
 
190
-  void _handleChange() {
191
-    if (_selection != editor.selection) {
192
-      _selection = editor.selection;
193
-      closeOverlay();
194
-    }
195
-    setState(() {});
196
-  }
197
-
198
   @override
192
   @override
199
   void initState() {
193
   void initState() {
200
     super.initState();
194
     super.initState();
201
     _delegate = widget.delegate ?? new _DefaultZefyrToolbarDelegate();
195
     _delegate = widget.delegate ?? new _DefaultZefyrToolbarDelegate();
202
     _overlayAnimation = new AnimationController(
196
     _overlayAnimation = new AnimationController(
203
         vsync: this, duration: Duration(milliseconds: 100));
197
         vsync: this, duration: Duration(milliseconds: 100));
204
-    widget.editor.addListener(_handleChange);
198
+    _selection = editor.selection;
205
   }
199
   }
206
 
200
 
207
   @override
201
   @override
210
     if (widget.delegate != oldWidget.delegate) {
204
     if (widget.delegate != oldWidget.delegate) {
211
       _delegate = widget.delegate ?? new _DefaultZefyrToolbarDelegate();
205
       _delegate = widget.delegate ?? new _DefaultZefyrToolbarDelegate();
212
     }
206
     }
213
-    if (widget.editor != oldWidget.editor) {
214
-      oldWidget.editor.removeListener(_handleChange);
215
-      widget.editor.addListener(_handleChange);
216
-    }
217
   }
207
   }
218
 
208
 
219
   @override
209
   @override
220
   void dispose() {
210
   void dispose() {
221
-    widget.editor.removeListener(_handleChange);
211
+    _overlayAnimation.dispose();
222
     super.dispose();
212
     super.dispose();
223
   }
213
   }
224
 
214
 
225
   @override
215
   @override
226
   Widget build(BuildContext context) {
216
   Widget build(BuildContext context) {
227
-    if (editor.focusOwner == FocusOwner.none) {
228
-      return new Container();
229
-    }
230
-
231
     final layers = <Widget>[];
217
     final layers = <Widget>[];
232
 
218
 
233
     // Must set unique key for the toolbar to prevent it from reconstructing
219
     // Must set unique key for the toolbar to prevent it from reconstructing
253
 
239
 
254
     final constraints =
240
     final constraints =
255
         BoxConstraints.tightFor(height: ZefyrToolbar.kToolbarHeight);
241
         BoxConstraints.tightFor(height: ZefyrToolbar.kToolbarHeight);
256
-    return new _ZefyrToolbarScope(
242
+    return _ZefyrToolbarScope(
257
       toolbar: this,
243
       toolbar: this,
258
       child: Container(
244
       child: Container(
259
         constraints: constraints,
245
         constraints: constraints,

+ 2
- 1
packages/zefyr/lib/zefyr.dart View File

15
 export 'src/widgets/controller.dart';
15
 export 'src/widgets/controller.dart';
16
 export 'src/widgets/editable_text.dart';
16
 export 'src/widgets/editable_text.dart';
17
 export 'src/widgets/editor.dart';
17
 export 'src/widgets/editor.dart';
18
+export 'src/widgets/field.dart';
18
 export 'src/widgets/horizontal_rule.dart';
19
 export 'src/widgets/horizontal_rule.dart';
19
 export 'src/widgets/image.dart';
20
 export 'src/widgets/image.dart';
20
 export 'src/widgets/list.dart';
21
 export 'src/widgets/list.dart';
21
 export 'src/widgets/paragraph.dart';
22
 export 'src/widgets/paragraph.dart';
22
 export 'src/widgets/quote.dart';
23
 export 'src/widgets/quote.dart';
24
+export 'src/widgets/scaffold.dart';
23
 export 'src/widgets/selection.dart' hide SelectionHandleDriver;
25
 export 'src/widgets/selection.dart' hide SelectionHandleDriver;
24
 export 'src/widgets/theme.dart';
26
 export 'src/widgets/theme.dart';
25
 export 'src/widgets/toolbar.dart';
27
 export 'src/widgets/toolbar.dart';
26
-//export 'src/widgets/render_context.dart';

+ 2
- 2
packages/zefyr/pubspec.yaml View File

1
 name: zefyr
1
 name: zefyr
2
 description: Clean, minimalistic and collaboration-ready rich text editor for Flutter.
2
 description: Clean, minimalistic and collaboration-ready rich text editor for Flutter.
3
-version: 0.2.0
3
+version: 0.3.0
4
 author: Anatoly Pulyaevskiy <anatoly.pulyaevskiy@gmail.com>
4
 author: Anatoly Pulyaevskiy <anatoly.pulyaevskiy@gmail.com>
5
 homepage: https://github.com/memspace/zefyr
5
 homepage: https://github.com/memspace/zefyr
6
 
6
 
11
   flutter:
11
   flutter:
12
     sdk: flutter
12
     sdk: flutter
13
   collection: ^1.14.6
13
   collection: ^1.14.6
14
-  url_launcher: ^3.0.0
14
+  url_launcher: ^4.0.0
15
   image_picker: ^0.4.5
15
   image_picker: ^0.4.5
16
   quill_delta: ^1.0.0-dev.1.0
16
   quill_delta: ^1.0.0-dev.1.0
17
   notus: ^0.1.0
17
   notus: ^0.1.0

+ 3
- 1
packages/zefyr/test/testing.dart View File

30
     if (theme != null) {
30
     if (theme != null) {
31
       widget = ZefyrTheme(data: theme, child: widget);
31
       widget = ZefyrTheme(data: theme, child: widget);
32
     }
32
     }
33
-    widget = MaterialApp(home: widget);
33
+    widget = MaterialApp(
34
+      home: ZefyrScaffold(child: widget),
35
+    );
34
 
36
 
35
     return EditorSandBox._(tester, focusNode, document, controller, widget);
37
     return EditorSandBox._(tester, focusNode, document, controller, widget);
36
   }
38
   }

+ 3
- 2
packages/zefyr/test/widgets/buttons_test.dart View File

104
       await tester
104
       await tester
105
           .tap(find.widgetWithText(GestureDetector, 'Tap to edit link'));
105
           .tap(find.widgetWithText(GestureDetector, 'Tap to edit link'));
106
       await tester.pumpAndSettle();
106
       await tester.pumpAndSettle();
107
+      expect(editor.focusNode.hasFocus, isFalse);
107
       await editor.updateSelection(base: 10, extent: 10);
108
       await editor.updateSelection(base: 10, extent: 10);
108
       expect(find.byIcon(Icons.link_off), findsNothing);
109
       expect(find.byIcon(Icons.link_off), findsNothing);
109
     });
110
     });
117
       await tester
118
       await tester
118
           .tap(find.widgetWithText(GestureDetector, 'Tap to edit link'));
119
           .tap(find.widgetWithText(GestureDetector, 'Tap to edit link'));
119
       await tester.pumpAndSettle();
120
       await tester.pumpAndSettle();
120
-      // TODO: figure out why below finder finds 2 instances of TextField
121
-      expect(find.widgetWithText(TextField, 'https://'), findsWidgets);
121
+      expect(find.byType(TextField), findsOneWidget);
122
+
122
       await tester.enterText(find.widgetWithText(TextField, 'https://').first,
123
       await tester.enterText(find.widgetWithText(TextField, 'https://').first,
123
           'https://github.com');
124
           'https://github.com');
124
       await tester.pumpAndSettle();
125
       await tester.pumpAndSettle();

+ 1
- 1
packages/zefyr/test/widgets/image_test.dart View File

75
       expect(editor.selection.extentOffset, embed.documentOffset);
75
       expect(editor.selection.extentOffset, embed.documentOffset);
76
     });
76
     });
77
 
77
 
78
-    testWidgets('tap right side of horizontal rule puts caret after it',
78
+    testWidgets('tap right side of image puts caret after it',
79
         (tester) async {
79
         (tester) async {
80
       final editor = new EditorSandBox(tester: tester);
80
       final editor = new EditorSandBox(tester: tester);
81
       await editor.tapEditor();
81
       await editor.tapEditor();

+ 8
- 6
packages/zefyr/test/widgets/render_context_test.dart View File

52
     });
52
     });
53
 
53
 
54
     testWidgets('notifyListeners is delayed to next frame', (tester) async {
54
     testWidgets('notifyListeners is delayed to next frame', (tester) async {
55
-      var focusNode = new FocusNode();
56
-      var controller = new ZefyrController(new NotusDocument());
57
-      var widget = new MaterialApp(
58
-        home: new ZefyrEditor(
59
-          controller: controller,
60
-          focusNode: focusNode,
55
+      var focusNode = FocusNode();
56
+      var controller = ZefyrController(new NotusDocument());
57
+      var widget = MaterialApp(
58
+        home: ZefyrScaffold(
59
+          child: ZefyrEditor(
60
+            controller: controller,
61
+            focusNode: focusNode,
62
+          ),
61
         ),
63
         ),
62
       );
64
       );
63
       await tester.pumpWidget(widget);
65
       await tester.pumpWidget(widget);

+ 1
- 1
packages/zefyr/test/widgets/selection_test.dart View File

65
       RenderBox renderObject =
65
       RenderBox renderObject =
66
           tester.firstRenderObject(find.byType(ZefyrEditableText));
66
           tester.firstRenderObject(find.byType(ZefyrEditableText));
67
       var offset = renderObject.localToGlobal(Offset.zero);
67
       var offset = renderObject.localToGlobal(Offset.zero);
68
-      offset += Offset(50.0, renderObject.size.height - 5.0);
68
+      offset += Offset(50.0, renderObject.size.height - 500.0);
69
       await tester.tapAt(offset);
69
       await tester.tapAt(offset);
70
       await tester.pumpAndSettle();
70
       await tester.pumpAndSettle();
71
       expect(editor.controller.selection.isCollapsed, isTrue);
71
       expect(editor.controller.selection.isCollapsed, isTrue);