diff --git a/packages/cross_file/CHANGELOG.md b/packages/cross_file/CHANGELOG.md index 7f37f7bd53df..73e9db3048af 100644 --- a/packages/cross_file/CHANGELOG.md +++ b/packages/cross_file/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.5+2 + +* Separates "Save As" implementation details from XFile web class. + ## 0.3.5+1 * Replaces README mentions of `dart:html` with `package:web`. diff --git a/packages/cross_file/lib/src/types/html.dart b/packages/cross_file/lib/src/types/html.dart index 9c85e1511b85..40703aa754fc 100644 --- a/packages/cross_file/lib/src/types/html.dart +++ b/packages/cross_file/lib/src/types/html.dart @@ -7,7 +7,6 @@ import 'dart:convert'; import 'dart:js_interop'; import 'dart:typed_data'; -import 'package:meta/meta.dart'; import 'package:web/web.dart'; import '../web_helpers/web_helpers.dart'; @@ -35,11 +34,9 @@ class XFile extends XFileBase { int? length, Uint8List? bytes, DateTime? lastModified, - @visibleForTesting CrossFileTestOverrides? overrides, }) : _mimeType = mimeType, _path = path, _length = length, - _overrides = overrides, _lastModified = lastModified ?? DateTime.fromMillisecondsSinceEpoch(0), _name = name ?? '', super(path) { @@ -57,10 +54,8 @@ class XFile extends XFileBase { int? length, DateTime? lastModified, String? path, - @visibleForTesting CrossFileTestOverrides? overrides, }) : _mimeType = mimeType, _length = length, - _overrides = overrides, _lastModified = lastModified ?? DateTime.fromMillisecondsSinceEpoch(0), _name = name ?? '', super(path) { @@ -82,12 +77,16 @@ class XFile extends XFileBase { // MimeType of the file (eg: "image/gif"). final String? _mimeType; + // Name (with extension) of the file (eg: "anim.gif") final String _name; + // Path of the file (must be a valid Blob URL, when set manually!) late String _path; + // The size of the file (in bytes). final int? _length; + // The time the file was last modified. final DateTime _lastModified; @@ -97,17 +96,6 @@ class XFile extends XFileBase { // (Similar to a (read-only) dart:io File.) Blob? _browserBlob; - // An html Element that will be used to trigger a "save as" dialog later. - // TODO(dit): https://github.com/flutter/flutter/issues/91400 Remove this _target. - late Element _target; - - // Overrides for testing - // TODO(dit): https://github.com/flutter/flutter/issues/91400 Remove these _overrides, - // they're only used to Save As... - final CrossFileTestOverrides? _overrides; - - bool get _hasTestOverrides => _overrides != null; - @override String? get mimeType => _mimeType; @@ -200,35 +188,8 @@ class XFile extends XFileBase { /// Saves the data of this CrossFile at the location indicated by path. /// For the web implementation, the path variable is ignored. - // TODO(dit): https://github.com/flutter/flutter/issues/91400 - // Move implementation to web_helpers.dart @override Future saveTo(String path) async { - // Create a DOM container where the anchor can be injected. - _target = ensureInitialized('__x_file_dom_element'); - - // Create an tag with the appropriate download attributes and click it - // May be overridden with CrossFileTestOverrides - final HTMLAnchorElement element = _hasTestOverrides - ? _overrides!.createAnchorElement(this.path, name) as HTMLAnchorElement - : createAnchorElement(this.path, name); - - // Clear the children in _target and add an element to click - while (_target.children.length > 0) { - _target.removeChild(_target.children.item(0)!); - } - addElementToContainerAndClick(_target, element); + await saveFileAs(this); } } - -/// Overrides some functions to allow testing -// TODO(dit): https://github.com/flutter/flutter/issues/91400 -// Move this to web_helpers_test.dart -@visibleForTesting -class CrossFileTestOverrides { - /// Default constructor for overrides - CrossFileTestOverrides({required this.createAnchorElement}); - - /// For overriding the creation of the file input element. - Element Function(String href, String suggestedName) createAnchorElement; -} diff --git a/packages/cross_file/lib/src/web_helpers/web_helpers.dart b/packages/cross_file/lib/src/web_helpers/web_helpers.dart index daac03159509..58231dd8c30e 100644 --- a/packages/cross_file/lib/src/web_helpers/web_helpers.dart +++ b/packages/cross_file/lib/src/web_helpers/web_helpers.dart @@ -2,13 +2,26 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:meta/meta.dart'; import 'package:web/web.dart'; +import '../types/html.dart'; + +/// Type definition for function that creates anchor elements +typedef CreateAnchorElement = + HTMLAnchorElement Function(String href, String? suggestedName); + /// Create anchor element with download attribute -HTMLAnchorElement createAnchorElement(String href, String? suggestedName) => - (document.createElement('a') as HTMLAnchorElement) - ..href = href - ..download = suggestedName ?? 'download'; +HTMLAnchorElement _createAnchorElementImpl( + String href, + String? suggestedName, +) => (document.createElement('a') as HTMLAnchorElement) + ..href = href + ..download = suggestedName ?? 'download'; + +/// Function for creating anchor elements. Can be overridden for testing. +@visibleForTesting +CreateAnchorElement createAnchorElementFunction = _createAnchorElementImpl; /// Add an element to a container and click it void addElementToContainerAndClick(Element container, HTMLElement element) { @@ -35,3 +48,23 @@ Element ensureInitialized(String id) { bool isSafari() { return window.navigator.vendor == 'Apple Computer, Inc.'; } + +/// Saves the given [XFile] to user's device ("Save As" dialog). +Future saveFileAs(XFile file) async { + // Create container element. + final Element target = ensureInitialized('__x_file_dom_element'); + + // Create element. + final HTMLAnchorElement element = createAnchorElementFunction( + file.path, + file.name, + ); + + // Clear existing children before appending new one. + while (target.children.length > 0) { + target.removeChild(target.children.item(0)!); + } + + // Add and click. + addElementToContainerAndClick(target, element); +} diff --git a/packages/cross_file/pubspec.yaml b/packages/cross_file/pubspec.yaml index 532f392faef8..dedcc19f8ae5 100644 --- a/packages/cross_file/pubspec.yaml +++ b/packages/cross_file/pubspec.yaml @@ -2,7 +2,7 @@ name: cross_file description: An abstraction to allow working with files across multiple platforms. repository: https://github.com/flutter/packages/tree/main/packages/cross_file issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+cross_file%22 -version: 0.3.5+1 +version: 0.3.5+2 environment: sdk: ^3.8.0 diff --git a/packages/cross_file/test/x_file_html_test.dart b/packages/cross_file/test/x_file_html_test.dart index 75bbcebc6aa4..e57e36c285c7 100644 --- a/packages/cross_file/test/x_file_html_test.dart +++ b/packages/cross_file/test/x_file_html_test.dart @@ -10,6 +10,7 @@ import 'dart:js_interop'; import 'dart:typed_data'; import 'package:cross_file/cross_file.dart'; +import 'package:cross_file/src/web_helpers/web_helpers.dart' as helpers; import 'package:test/test.dart'; import 'package:web/web.dart' as html; @@ -149,15 +150,16 @@ void main() { final mockAnchor = html.document.createElement('a') as html.HTMLAnchorElement; - final overrides = CrossFileTestOverrides( - createAnchorElement: (_, __) => mockAnchor, - ); + // Save original function so we can restore it + final helpers.CreateAnchorElement original = + helpers.createAnchorElementFunction; - final file = XFile.fromData( - bytes, - name: textFile.name, - overrides: overrides, - ); + addTearDown(() { + helpers.createAnchorElementFunction = original; + }); + helpers.createAnchorElementFunction = (_, __) => mockAnchor; + + final file = XFile.fromData(bytes, name: textFile.name); var clicked = false; mockAnchor.onClick.listen((html.MouseEvent event) => clicked = true);