Browse Source

Work in progress on non-scrollable read-only Zefyr view

Anatoly Pulyaevskiy 6 years ago
parent
commit
bc761b0baf

+ 12
- 0
packages/zefyr/example/lib/main.dart View File

@@ -4,6 +4,7 @@
4 4
 import 'package:flutter/material.dart';
5 5
 import 'src/form.dart';
6 6
 import 'src/full_page.dart';
7
+import 'src/view.dart';
7 8
 
8 9
 void main() {
9 10
   runApp(new ZefyrApp());
@@ -20,6 +21,7 @@ class ZefyrApp extends StatelessWidget {
20 21
       routes: {
21 22
         "/fullPage": buildFullPage,
22 23
         "/form": buildFormPage,
24
+        "/view": buildViewPage,
23 25
       },
24 26
     );
25 27
   }
@@ -31,6 +33,10 @@ class ZefyrApp extends StatelessWidget {
31 33
   Widget buildFormPage(BuildContext context) {
32 34
     return FormEmbeddedScreen();
33 35
   }
36
+
37
+  Widget buildViewPage(BuildContext context) {
38
+    return ViewScreen();
39
+  }
34 40
 }
35 41
 
36 42
 class HomePage extends StatelessWidget {
@@ -59,6 +65,12 @@ class HomePage extends StatelessWidget {
59 65
             color: Colors.lightBlue,
60 66
             textColor: Colors.white,
61 67
           ),
68
+          FlatButton(
69
+            onPressed: () => nav.pushNamed('/view'),
70
+            child: Text('Read-only embeddable view'),
71
+            color: Colors.lightBlue,
72
+            textColor: Colors.white,
73
+          ),
62 74
           Expanded(child: Container()),
63 75
         ],
64 76
       ),

+ 86
- 0
packages/zefyr/example/lib/src/view.dart View File

@@ -0,0 +1,86 @@
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
+import 'full_page.dart';
8
+
9
+class ViewScreen extends StatefulWidget {
10
+  @override
11
+  _ViewScreen createState() => new _ViewScreen();
12
+}
13
+
14
+final doc =
15
+    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":'
16
+    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'
17
+    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'
18
+    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'
19
+    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"}]';
20
+
21
+Delta getDelta() {
22
+  return Delta.fromJson(json.decode(doc));
23
+}
24
+
25
+class _ViewScreen extends State<ViewScreen> {
26
+  final doc = NotusDocument.fromDelta(getDelta());
27
+
28
+  @override
29
+  Widget build(BuildContext context) {
30
+    final theme = new ZefyrThemeData(
31
+      toolbarTheme: ZefyrToolbarTheme.fallback(context).copyWith(
32
+        color: Colors.grey.shade800,
33
+        toggleColor: Colors.grey.shade900,
34
+        iconColor: Colors.white,
35
+        disabledIconColor: Colors.grey.shade500,
36
+      ),
37
+    );
38
+    return Scaffold(
39
+      resizeToAvoidBottomPadding: true,
40
+      appBar: AppBar(
41
+        elevation: 1.0,
42
+        backgroundColor: Colors.grey.shade200,
43
+        brightness: Brightness.light,
44
+        title: ZefyrLogo(),
45
+      ),
46
+      body: ZefyrTheme(
47
+        data: theme,
48
+        child: ListView(
49
+          children: <Widget>[
50
+            SizedBox(height: 16.0),
51
+            ListTile(
52
+              leading: Icon(Icons.info),
53
+              title: Text('ZefyrView inside ListView'),
54
+              subtitle: Text('Allows embedding Notus documents in custom scrollables'),
55
+              trailing: Icon(Icons.keyboard_arrow_down),
56
+            ),
57
+            Padding(
58
+              padding: const EdgeInsets.all(16.0),
59
+              child: ZefyrView(
60
+                document: doc,
61
+                imageDelegate: new CustomImageDelegate(),
62
+              ),
63
+            )
64
+          ],
65
+        ),
66
+      ),
67
+    );
68
+  }
69
+}
70
+
71
+/// Custom image delegate used by this example to load image from application
72
+/// assets.
73
+///
74
+/// Default image delegate only supports [FileImage]s.
75
+class CustomImageDelegate extends ZefyrDefaultImageDelegate {
76
+  @override
77
+  Widget buildImage(BuildContext context, String imageSource) {
78
+    // We use custom "asset" scheme to distinguish asset images from other files.
79
+    if (imageSource.startsWith('asset://')) {
80
+      final asset = new AssetImage(imageSource.replaceFirst('asset://', ''));
81
+      return new Image(image: asset);
82
+    } else {
83
+      return super.buildImage(context, imageSource);
84
+    }
85
+  }
86
+}

+ 55
- 37
packages/zefyr/lib/src/widgets/common.dart View File

@@ -11,6 +11,7 @@ import 'horizontal_rule.dart';
11 11
 import 'image.dart';
12 12
 import 'rich_text.dart';
13 13
 import 'theme.dart';
14
+import 'view.dart';
14 15
 
15 16
 /// Raw widget representing a single line of rich text document in Zefyr editor.
16 17
 ///
@@ -43,36 +44,49 @@ class _RawZefyrLineState extends State<RawZefyrLine> {
43 44
 
44 45
   @override
45 46
   Widget build(BuildContext context) {
46
-    ensureVisible(context);
47
+    ZefyrViewState view = ZefyrView.of(context);
48
+    ZefyrEditableTextScope editable;
49
+    if (view == null) {
50
+      editable = ZefyrEditableText.of(context);
51
+    }
52
+
53
+    final isEditable = editable != null;
54
+
55
+    if (isEditable) {
56
+      ensureVisible(context);
57
+    }
47 58
     final theme = ZefyrTheme.of(context);
48
-    final editable = ZefyrEditableText.of(context);
49 59
 
50 60
     Widget content;
51 61
     if (widget.node.hasEmbed) {
52
-      content = buildEmbed(context);
62
+      content = buildEmbed(context, view, editable);
53 63
     } else {
54 64
       assert(widget.style != null);
55 65
 
56
-      final text = new EditableRichText(
66
+      final text = EditableRichText(
57 67
         node: widget.node,
58 68
         text: buildText(context),
59 69
       );
60
-      content = new EditableBox(
61
-        child: text,
62
-        node: widget.node,
63
-        layerLink: _link,
64
-        renderContext: editable.renderContext,
65
-        showCursor: editable.showCursor,
66
-        selection: editable.selection,
67
-        selectionColor: theme.selectionColor,
68
-      );
70
+      if (isEditable) {
71
+        content = EditableBox(
72
+          child: text,
73
+          node: widget.node,
74
+          layerLink: _link,
75
+          renderContext: editable.renderContext,
76
+          showCursor: editable.showCursor,
77
+          selection: editable.selection,
78
+          selectionColor: theme.selectionColor,
79
+        );
80
+        content = CompositedTransformTarget(link: _link, child: content);
81
+      } else {
82
+        content = text;
83
+      }
69 84
     }
70 85
 
71
-    final result = new CompositedTransformTarget(link: _link, child: content);
72 86
     if (widget.padding != null) {
73
-      return new Padding(padding: widget.padding, child: result);
87
+      return Padding(padding: widget.padding, child: content);
74 88
     }
75
-    return result;
89
+    return content;
76 90
   }
77 91
 
78 92
   void ensureVisible(BuildContext context) {
@@ -136,36 +150,40 @@ class _RawZefyrLineState extends State<RawZefyrLine> {
136 150
     return result;
137 151
   }
138 152
 
139
-  Widget buildEmbed(BuildContext context) {
153
+  Widget buildEmbed(BuildContext context, ZefyrViewState view,
154
+      ZefyrEditableTextScope editable) {
155
+    final isEditable = editable != null;
156
+
140 157
     final theme = ZefyrTheme.of(context);
141
-    final editable = ZefyrEditableText.of(context);
142 158
 
143 159
     EmbedNode node = widget.node.children.single;
144 160
     EmbedAttribute embed = node.style.get(NotusAttribute.embed);
145 161
 
162
+    Widget result;
146 163
     if (embed.type == EmbedType.horizontalRule) {
147
-      final hr = new ZefyrHorizontalRule(node: node);
148
-      return new EditableBox(
149
-        child: hr,
150
-        node: widget.node,
151
-        layerLink: _link,
152
-        renderContext: editable.renderContext,
153
-        showCursor: editable.showCursor,
154
-        selection: editable.selection,
155
-        selectionColor: theme.selectionColor,
156
-      );
164
+      result = ZefyrHorizontalRule(node: node);
157 165
     } else if (embed.type == EmbedType.image) {
158
-      return new EditableBox(
159
-        child: ZefyrImage(node: node, delegate: editable.imageDelegate),
160
-        node: widget.node,
161
-        layerLink: _link,
162
-        renderContext: editable.renderContext,
163
-        showCursor: editable.showCursor,
164
-        selection: editable.selection,
165
-        selectionColor: theme.selectionColor,
166
-      );
166
+      if (isEditable) {
167
+        result = ZefyrImage(node: node, delegate: editable.imageDelegate);
168
+      } else {
169
+        result = ZefyrImage(node: node, delegate: view.imageDelegate);
170
+      }
167 171
     } else {
168 172
       throw new UnimplementedError('Unimplemented embed type ${embed.type}');
169 173
     }
174
+
175
+    if (!isEditable) {
176
+      return result;
177
+    }
178
+
179
+    return new EditableBox(
180
+      child: result,
181
+      node: widget.node,
182
+      layerLink: _link,
183
+      renderContext: editable.renderContext,
184
+      showCursor: editable.showCursor,
185
+      selection: editable.selection,
186
+      selectionColor: theme.selectionColor,
187
+    );
170 188
   }
171 189
 }

+ 104
- 0
packages/zefyr/lib/src/widgets/view.dart View File

@@ -0,0 +1,104 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:notus/notus.dart';
3
+
4
+import 'code.dart';
5
+import 'common.dart';
6
+import 'image.dart';
7
+import 'list.dart';
8
+import 'paragraph.dart';
9
+import 'quote.dart';
10
+import 'theme.dart';
11
+
12
+class _ZefyrViewAccess extends InheritedWidget {
13
+  final ZefyrViewState state;
14
+
15
+  _ZefyrViewAccess({Key key, @required this.state, Widget child})
16
+      : super(key: key, child: child);
17
+
18
+  @override
19
+  bool updateShouldNotify(_ZefyrViewAccess oldWidget) {
20
+    return state != oldWidget.state;
21
+  }
22
+}
23
+
24
+/// Non-scrollable read-only view of a Notus rich text document.
25
+class ZefyrView extends StatefulWidget {
26
+  final NotusDocument document;
27
+  final ZefyrImageDelegate imageDelegate;
28
+
29
+  const ZefyrView({Key key, this.document, this.imageDelegate})
30
+      : super(key: key);
31
+
32
+  static ZefyrViewState of(BuildContext context) {
33
+    final _ZefyrViewAccess widget =
34
+        context.inheritFromWidgetOfExactType(_ZefyrViewAccess);
35
+    if (widget == null) return null;
36
+    return widget.state;
37
+  }
38
+
39
+  @override
40
+  ZefyrViewState createState() => ZefyrViewState();
41
+}
42
+
43
+class ZefyrViewState extends State<ZefyrView> {
44
+  ZefyrThemeData _themeData;
45
+
46
+  ZefyrImageDelegate get imageDelegate => widget.imageDelegate;
47
+
48
+  @override
49
+  void didChangeDependencies() {
50
+    super.didChangeDependencies();
51
+    final parentTheme = ZefyrTheme.of(context, nullOk: true);
52
+    final fallbackTheme = ZefyrThemeData.fallback(context);
53
+    _themeData = (parentTheme != null)
54
+        ? fallbackTheme.merge(parentTheme)
55
+        : fallbackTheme;
56
+  }
57
+
58
+  @override
59
+  Widget build(BuildContext context) {
60
+    return ZefyrTheme(
61
+      data: _themeData,
62
+      child: _ZefyrViewAccess(
63
+        state: this,
64
+        child: Column(
65
+          crossAxisAlignment: CrossAxisAlignment.stretch,
66
+          children: _buildChildren(context),
67
+        ),
68
+      ),
69
+    );
70
+  }
71
+
72
+  List<Widget> _buildChildren(BuildContext context) {
73
+    final result = <Widget>[];
74
+    for (var node in widget.document.root.children) {
75
+      result.add(_defaultChildBuilder(context, node));
76
+    }
77
+    return result;
78
+  }
79
+
80
+  Widget _defaultChildBuilder(BuildContext context, Node node) {
81
+    if (node is LineNode) {
82
+      if (node.hasEmbed) {
83
+        return new RawZefyrLine(node: node);
84
+      } else if (node.style.contains(NotusAttribute.heading)) {
85
+        return new ZefyrHeading(node: node);
86
+      }
87
+      return new ZefyrParagraph(node: node);
88
+    }
89
+
90
+    final BlockNode block = node;
91
+    final blockStyle = block.style.get(NotusAttribute.block);
92
+    if (blockStyle == NotusAttribute.block.code) {
93
+      return new ZefyrCode(node: node);
94
+    } else if (blockStyle == NotusAttribute.block.bulletList) {
95
+      return new ZefyrList(node: node);
96
+    } else if (blockStyle == NotusAttribute.block.numberList) {
97
+      return new ZefyrList(node: node);
98
+    } else if (blockStyle == NotusAttribute.block.quote) {
99
+      return new ZefyrQuote(node: node);
100
+    }
101
+
102
+    throw new UnimplementedError('Block format $blockStyle.');
103
+  }
104
+}

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

@@ -25,3 +25,4 @@ export 'src/widgets/scaffold.dart';
25 25
 export 'src/widgets/selection.dart' hide SelectionHandleDriver;
26 26
 export 'src/widgets/theme.dart';
27 27
 export 'src/widgets/toolbar.dart';
28
+export 'src/widgets/view.dart';