ソースを参照

Merge pull request #42 from memspace/toolbar-overlay

Allow embedding Zefyr into custom layouts with ZefyrScaffold
Anatoly Pulyaevskiy 6 年 前
コミット
2ff0df7c28
No account linked to committer's email address

+ 1
- 0
.gitignore ファイルの表示

@@ -0,0 +1 @@
1
+.idea/

+ 10
- 5
doc/quick_start.md ファイルの表示

@@ -12,7 +12,7 @@ Add `zefyr` package as a dependency to your `pubspec.yaml`:
12 12
 
13 13
 ```yaml
14 14
 dependencies:
15
-  zefyr: ^0.1.0
15
+  zefyr: ^0.3.0
16 16
 ```
17 17
 
18 18
 And run `flutter packages get` to install. This installs both `zefyr`
@@ -20,7 +20,7 @@ and `notus` packages.
20 20
 
21 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 25
 * `NotusDocument`, represents a rich text document and provides
26 26
   high-level methods for manipulating the document's state, like
@@ -30,6 +30,9 @@ There are 3 main objects you would normally interact with in your code:
30 30
 * `ZefyrEditor`, a Flutter widget responsible for rendering of rich text
31 31
   on the screen and reacting to user actions.
32 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 37
 Normally you would need to place `ZefyrEditor` inside of a
35 38
 `StatefulWidget`. Shown below is a minimal setup required to use the
@@ -60,9 +63,11 @@ class MyWidgetState extends State<MyWidget> {
60 63
 
61 64
   @override
62 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 ファイルの表示

@@ -14,3 +14,5 @@ example/ios/.symlinks
14 14
 example/ios/Flutter/Generated.xcconfig
15 15
 doc/api/
16 16
 build/
17
+
18
+example/feather

+ 15
- 0
packages/zefyr/CHANGELOG.md ファイルの表示

@@ -1,3 +1,18 @@
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 16
 ## 0.2.0
2 17
 
3 18
 * Breaking change: `ZefyrImageDelegate.createImageProvider` replaced with

+ 1
- 1
packages/zefyr/analysis_options.yaml ファイルの表示

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

+ 34
- 92
packages/zefyr/example/lib/main.dart ファイルの表示

@@ -1,125 +1,67 @@
1 1
 // Copyright (c) 2018, the Zefyr project authors.  Please see the AUTHORS file
2 2
 // for details. All rights reserved. Use of this source code is governed by a
3 3
 // BSD-style license that can be found in the LICENSE file.
4
-import 'dart:convert';
5
-
6 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 8
 void main() {
11 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 12
 class ZefyrApp extends StatelessWidget {
29 13
   @override
30 14
   Widget build(BuildContext context) {
31
-    return new MaterialApp(
15
+    return MaterialApp(
32 16
       debugShowCheckedModeBanner: false,
33 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 37
   @override
63 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 40
     return Scaffold(
77
-      resizeToAvoidBottomPadding: true,
78 41
       appBar: AppBar(
79 42
         elevation: 1.0,
80 43
         backgroundColor: Colors.grey.shade200,
81 44
         brightness: Brightness.light,
82 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 ファイルの表示

@@ -0,0 +1,65 @@
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 ファイルの表示

@@ -0,0 +1,108 @@
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 ファイルの表示

@@ -300,9 +300,11 @@ class LinkButton extends StatefulWidget {
300 300
 }
301 301
 
302 302
 class _LinkButtonState extends State<LinkButton> {
303
-  final TextEditingController _inputController = new TextEditingController();
303
+  final TextEditingController _inputController = TextEditingController();
304 304
   Key _inputKey;
305 305
   bool _formatError = false;
306
+  ZefyrEditorScope _editor;
307
+
306 308
   bool get isEditing => _inputKey != null;
307 309
 
308 310
   @override
@@ -376,7 +378,7 @@ class _LinkButtonState extends State<LinkButton> {
376 378
         _inputController.text = '';
377 379
         _inputController.removeListener(_handleInputChange);
378 380
         toolbar.markNeedsRebuild();
379
-        toolbar.editor.focus(context);
381
+        toolbar.editor.focus();
380 382
       }
381 383
     });
382 384
   }
@@ -388,7 +390,7 @@ class _LinkButtonState extends State<LinkButton> {
388 390
         _inputKey = null;
389 391
         _inputController.text = '';
390 392
         _inputController.removeListener(_handleInputChange);
391
-        editor.focus(context);
393
+        editor.focus();
392 394
       });
393 395
     }
394 396
   }
@@ -437,7 +439,6 @@ class _LinkButtonState extends State<LinkButton> {
437 439
         : _LinkInput(
438 440
             key: _inputKey,
439 441
             controller: _inputController,
440
-            focusNode: toolbar.editor.toolbarFocusNode,
441 442
             formatError: _formatError,
442 443
           );
443 444
     final items = <Widget>[Expanded(child: body)];
@@ -474,16 +475,13 @@ class _LinkButtonState extends State<LinkButton> {
474 475
 }
475 476
 
476 477
 class _LinkInput extends StatefulWidget {
477
-  final FocusNode focusNode;
478 478
   final TextEditingController controller;
479 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 485
   @override
488 486
   _LinkInputState createState() {
489 487
     return new _LinkInputState();
@@ -491,21 +489,38 @@ class _LinkInput extends StatefulWidget {
491 489
 }
492 490
 
493 491
 class _LinkInputState extends State<_LinkInput> {
492
+  final FocusNode _focusNode = FocusNode();
493
+
494
+  ZefyrEditorScope _editor;
494 495
   bool _didAutoFocus = false;
495 496
 
496 497
   @override
497 498
   void didChangeDependencies() {
498 499
     super.didChangeDependencies();
499 500
     if (!_didAutoFocus) {
500
-      FocusScope.of(context).requestFocus(widget.focusNode);
501
+      FocusScope.of(context).requestFocus(_focusNode);
501 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 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 524
     final theme = Theme.of(context);
510 525
     final toolbarTheme = ZefyrTheme.of(context).toolbarTheme;
511 526
     final color =
@@ -514,15 +529,16 @@ class _LinkInputState extends State<_LinkInput> {
514 529
     return TextField(
515 530
       style: style,
516 531
       keyboardType: TextInputType.url,
517
-      focusNode: widget.focusNode,
532
+      focusNode: _focusNode,
518 533
       controller: widget.controller,
519 534
       autofocus: true,
520 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 ファイルの表示

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

+ 101
- 35
packages/zefyr/lib/src/widgets/editor.dart ファイルの表示

@@ -7,6 +7,7 @@ import 'package:notus/notus.dart';
7 7
 import 'controller.dart';
8 8
 import 'editable_text.dart';
9 9
 import 'image.dart';
10
+import 'scaffold.dart';
10 11
 import 'theme.dart';
11 12
 import 'toolbar.dart';
12 13
 
@@ -15,15 +16,14 @@ class ZefyrEditorScope extends ChangeNotifier {
15 16
     @required ZefyrImageDelegate imageDelegate,
16 17
     @required ZefyrController controller,
17 18
     @required FocusNode focusNode,
18
-    @required FocusNode toolbarFocusNode,
19
+    @required FocusScopeNode focusScope,
19 20
   })  : _controller = controller,
20 21
         _imageDelegate = imageDelegate,
21
-        _focusNode = focusNode,
22
-        _toolbarFocusNode = toolbarFocusNode {
22
+        _focusScope = focusScope,
23
+        _focusNode = focusNode {
23 24
     _selectionStyle = _controller.getSelectionStyle();
24 25
     _selection = _controller.selection;
25 26
     _controller.addListener(_handleControllerChange);
26
-    toolbarFocusNode.addListener(_handleFocusChange);
27 27
     _focusNode.addListener(_handleFocusChange);
28 28
   }
29 29
 
@@ -32,9 +32,8 @@ class ZefyrEditorScope extends ChangeNotifier {
32 32
   ZefyrImageDelegate _imageDelegate;
33 33
   ZefyrImageDelegate get imageDelegate => _imageDelegate;
34 34
 
35
+  FocusScopeNode _focusScope;
35 36
   FocusNode _focusNode;
36
-  FocusNode _toolbarFocusNode;
37
-  FocusNode get toolbarFocusNode => _toolbarFocusNode;
38 37
 
39 38
   ZefyrController _controller;
40 39
   NotusStyle get selectionStyle => _selectionStyle;
@@ -46,7 +45,6 @@ class ZefyrEditorScope extends ChangeNotifier {
46 45
   void dispose() {
47 46
     assert(!_disposed);
48 47
     _controller.removeListener(_handleControllerChange);
49
-    _toolbarFocusNode.removeListener(_handleFocusChange);
50 48
     _focusNode.removeListener(_handleFocusChange);
51 49
     _disposed = true;
52 50
     super.dispose();
@@ -102,11 +100,24 @@ class ZefyrEditorScope extends ChangeNotifier {
102 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 116
   FocusOwner get focusOwner {
106 117
     assert(!_disposed);
107 118
     if (_focusNode.hasFocus) {
108 119
       return FocusOwner.editor;
109
-    } else if (toolbarFocusNode.hasFocus) {
120
+    } else if (_toolbarFocusNode?.hasFocus == true) {
110 121
       return FocusOwner.toolbar;
111 122
     } else {
112 123
       return FocusOwner.none;
@@ -124,9 +135,9 @@ class ZefyrEditorScope extends ChangeNotifier {
124 135
     _controller.formatSelection(value);
125 136
   }
126 137
 
127
-  void focus(BuildContext context) {
138
+  void focus() {
128 139
     assert(!_disposed);
129
-    FocusScope.of(context).requestFocus(_focusNode);
140
+    _focusScope.requestFocus(_focusNode);
130 141
   }
131 142
 
132 143
   void hideKeyboard() {
@@ -158,6 +169,7 @@ class ZefyrEditor extends StatefulWidget {
158 169
     this.padding: const EdgeInsets.symmetric(horizontal: 16.0),
159 170
     this.toolbarDelegate,
160 171
     this.imageDelegate,
172
+    this.physics,
161 173
   }) : super(key: key);
162 174
 
163 175
   final ZefyrController controller;
@@ -166,6 +178,7 @@ class ZefyrEditor extends StatefulWidget {
166 178
   final bool enabled;
167 179
   final ZefyrToolbarDelegate toolbarDelegate;
168 180
   final ZefyrImageDelegate imageDelegate;
181
+  final ScrollPhysics physics;
169 182
 
170 183
   /// Padding around editable area.
171 184
   final EdgeInsets padding;
@@ -181,20 +194,53 @@ class ZefyrEditor extends StatefulWidget {
181 194
 }
182 195
 
183 196
 class _ZefyrEditorState extends State<ZefyrEditor> {
184
-  final FocusNode _toolbarFocusNode = new FocusNode();
185 197
   ZefyrImageDelegate _imageDelegate;
186 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 240
   @override
189 241
   void initState() {
190 242
     super.initState();
191 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 246
   @override
@@ -208,10 +254,44 @@ class _ZefyrEditorState extends State<ZefyrEditor> {
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 290
   @override
212 291
   void dispose() {
292
+    hideToolbar();
293
+    _scope.removeListener(_handleChange);
213 294
     _scope.dispose();
214
-    _toolbarFocusNode.dispose();
215 295
     super.dispose();
216 296
   }
217 297
 
@@ -224,28 +304,14 @@ class _ZefyrEditorState extends State<ZefyrEditor> {
224 304
       autofocus: widget.autofocus,
225 305
       enabled: widget.enabled,
226 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 310
     return ZefyrTheme(
245
-      data: actualTheme,
311
+      data: _themeData,
246 312
       child: _ZefyrEditorScope(
247 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 ファイルの表示

@@ -0,0 +1,86 @@
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 ファイルの表示

@@ -0,0 +1,60 @@
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 ファイルの表示

@@ -66,6 +66,7 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
66 66
 
67 67
   @override
68 68
   void hideToolbar() {
69
+    _didCaretTap = false; // reset double tap.
69 70
     _toolbar?.remove();
70 71
     _toolbar = null;
71 72
     _toolbarController.stop();
@@ -82,7 +83,6 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
82 83
               editable: editable,
83 84
               controls: widget.controls,
84 85
               delegate: this,
85
-              visible: true,
86 86
             ),
87 87
           ),
88 88
     );
@@ -99,8 +99,6 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
99 99
     super.initState();
100 100
     _toolbarController = new AnimationController(
101 101
         duration: _kFadeDuration, vsync: widget.overlay);
102
-    _selection = widget.controller.selection;
103
-    widget.controller.addListener(_handleChange);
104 102
   }
105 103
 
106 104
   static const Duration _kFadeDuration = const Duration(milliseconds: 150);
@@ -114,23 +112,24 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
114 112
       _toolbarController = new AnimationController(
115 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 117
   @override
124 118
   void didChangeDependencies() {
125 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 130
   @override
132 131
   void dispose() {
133
-    widget.controller.removeListener(_handleChange);
132
+    _editor.removeListener(_handleChange);
134 133
     hideToolbar();
135 134
     _toolbarController.dispose();
136 135
     _toolbarController = null;
@@ -175,12 +174,14 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
175 174
   OverlayEntry _toolbar;
176 175
   AnimationController _toolbarController;
177 176
 
177
+  ZefyrEditorScope _editor;
178 178
   TextSelection _selection;
179
+  FocusOwner _focusOwner;
179 180
 
180 181
   bool _didCaretTap = false;
181 182
 
182 183
   void _handleChange() {
183
-    if (_selection != widget.controller.selection) {
184
+    if (_selection != _editor.selection || _focusOwner != _editor.focusOwner) {
184 185
       _updateToolbar();
185 186
     }
186 187
   }
@@ -209,6 +210,7 @@ class _ZefyrSelectionOverlayState extends State<ZefyrSelectionOverlay>
209 210
         }
210 211
       }
211 212
       _selection = selection;
213
+      _focusOwner = focusOwner;
212 214
     });
213 215
   }
214 216
 
@@ -423,13 +425,11 @@ class _SelectionToolbar extends StatefulWidget {
423 425
     @required this.editable,
424 426
     @required this.controls,
425 427
     @required this.delegate,
426
-    @required this.visible,
427 428
   }) : super(key: key);
428 429
 
429 430
   final ZefyrEditableTextScope editable;
430 431
   final TextSelectionControls controls;
431 432
   final TextSelectionDelegate delegate;
432
-  final bool visible;
433 433
 
434 434
   @override
435 435
   _SelectionToolbarState createState() => new _SelectionToolbarState();

+ 1
- 1
packages/zefyr/lib/src/widgets/theme.dart ファイルの表示

@@ -172,7 +172,7 @@ class HeadingTheme {
172 172
           height: 1.25,
173 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 177
       level2: StyleTheme(
178 178
         textStyle: TextStyle(

+ 9
- 23
packages/zefyr/lib/src/widgets/toolbar.dart ファイルの表示

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

+ 2
- 1
packages/zefyr/lib/zefyr.dart ファイルの表示

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

+ 2
- 2
packages/zefyr/pubspec.yaml ファイルの表示

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

+ 3
- 1
packages/zefyr/test/testing.dart ファイルの表示

@@ -30,7 +30,9 @@ class EditorSandBox {
30 30
     if (theme != null) {
31 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 37
     return EditorSandBox._(tester, focusNode, document, controller, widget);
36 38
   }

+ 3
- 2
packages/zefyr/test/widgets/buttons_test.dart ファイルの表示

@@ -104,6 +104,7 @@ void main() {
104 104
       await tester
105 105
           .tap(find.widgetWithText(GestureDetector, 'Tap to edit link'));
106 106
       await tester.pumpAndSettle();
107
+      expect(editor.focusNode.hasFocus, isFalse);
107 108
       await editor.updateSelection(base: 10, extent: 10);
108 109
       expect(find.byIcon(Icons.link_off), findsNothing);
109 110
     });
@@ -117,8 +118,8 @@ void main() {
117 118
       await tester
118 119
           .tap(find.widgetWithText(GestureDetector, 'Tap to edit link'));
119 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 123
       await tester.enterText(find.widgetWithText(TextField, 'https://').first,
123 124
           'https://github.com');
124 125
       await tester.pumpAndSettle();

+ 1
- 1
packages/zefyr/test/widgets/image_test.dart ファイルの表示

@@ -75,7 +75,7 @@ void main() {
75 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 79
         (tester) async {
80 80
       final editor = new EditorSandBox(tester: tester);
81 81
       await editor.tapEditor();

+ 8
- 6
packages/zefyr/test/widgets/render_context_test.dart ファイルの表示

@@ -52,12 +52,14 @@ void main() {
52 52
     });
53 53
 
54 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 65
       await tester.pumpWidget(widget);

+ 1
- 1
packages/zefyr/test/widgets/selection_test.dart ファイルの表示

@@ -65,7 +65,7 @@ void main() {
65 65
       RenderBox renderObject =
66 66
           tester.firstRenderObject(find.byType(ZefyrEditableText));
67 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 69
       await tester.tapAt(offset);
70 70
       await tester.pumpAndSettle();
71 71
       expect(editor.controller.selection.isCollapsed, isTrue);