diff --git a/packages/block-editor/src/components/media-upload-progress/index.native.js b/packages/block-editor/src/components/media-upload-progress/index.native.js index ca13038a2795ed..c886ad1eb7f722 100644 --- a/packages/block-editor/src/components/media-upload-progress/index.native.js +++ b/packages/block-editor/src/components/media-upload-progress/index.native.js @@ -3,9 +3,6 @@ */ import React from 'react'; import { View } from 'react-native'; -import { - subscribeMediaUpload, -} from 'react-native-gutenberg-bridge'; /** * WordPress dependencies @@ -14,6 +11,9 @@ import { Spinner, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +import { + subscribeMediaUpload, +} from '@wordpress/react-native-bridge'; /** * Internal dependencies diff --git a/packages/block-editor/src/components/media-upload-progress/test/index.native.js b/packages/block-editor/src/components/media-upload-progress/test/index.native.js index e7c48e80d05df5..46a51770c5f331 100644 --- a/packages/block-editor/src/components/media-upload-progress/test/index.native.js +++ b/packages/block-editor/src/components/media-upload-progress/test/index.native.js @@ -2,9 +2,13 @@ * External dependencies */ import { shallow } from 'enzyme'; + +/** + * WordPress dependencies + */ import { sendMediaUpload, -} from 'react-native-gutenberg-bridge'; +} from '@wordpress/react-native-bridge'; /** * Internal dependencies @@ -17,7 +21,7 @@ import { MEDIA_UPLOAD_STATE_RESET, } from '../'; -jest.mock( 'react-native-gutenberg-bridge', () => { +jest.mock( '@wordpress/react-native-bridge', () => { const callUploadCallback = ( payload ) => { this.uploadCallBack( payload ); }; diff --git a/packages/block-editor/src/components/media-upload/index.native.js b/packages/block-editor/src/components/media-upload/index.native.js index 2e1c3a9fcd8010..2bb7925047128c 100644 --- a/packages/block-editor/src/components/media-upload/index.native.js +++ b/packages/block-editor/src/components/media-upload/index.native.js @@ -2,19 +2,19 @@ * External dependencies */ import React from 'react'; -import { - requestMediaPickFromMediaLibrary, - requestMediaPickFromDeviceLibrary, - requestMediaPickFromDeviceCamera, - getOtherMediaOptions, - requestOtherMediaPickFrom, -} from 'react-native-gutenberg-bridge'; /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; import { Picker } from '@wordpress/components'; +import { + requestMediaPickFromMediaLibrary, + requestMediaPickFromDeviceLibrary, + requestMediaPickFromDeviceCamera, + getOtherMediaOptions, + requestOtherMediaPickFrom, +} from '@wordpress/react-native-bridge'; export const MEDIA_TYPE_IMAGE = 'image'; export const MEDIA_TYPE_VIDEO = 'video'; diff --git a/packages/block-editor/src/components/media-upload/test/index.native.js b/packages/block-editor/src/components/media-upload/test/index.native.js index 85b63b098ca927..c95379b8c53108 100644 --- a/packages/block-editor/src/components/media-upload/test/index.native.js +++ b/packages/block-editor/src/components/media-upload/test/index.native.js @@ -3,11 +3,14 @@ */ import { shallow } from 'enzyme'; import { TouchableWithoutFeedback } from 'react-native'; +/** + * WordPress dependencies + */ import { requestMediaPickFromMediaLibrary, requestMediaPickFromDeviceLibrary, requestMediaPickFromDeviceCamera, -} from 'react-native-gutenberg-bridge'; +} from '@wordpress/react-native-bridge'; /** * Internal dependencies diff --git a/packages/block-library/src/image/edit.native.js b/packages/block-library/src/image/edit.native.js index 1d9274c3754697..66b00b1369a01c 100644 --- a/packages/block-library/src/image/edit.native.js +++ b/packages/block-library/src/image/edit.native.js @@ -3,12 +3,6 @@ */ import React from 'react'; import { View, ImageBackground, Text, TouchableWithoutFeedback, Dimensions } from 'react-native'; -import { - requestMediaImport, - mediaUploadSync, - requestImageFailedRetryDialog, - requestImageUploadCancelDialog, -} from 'react-native-gutenberg-bridge'; import { isEmpty, map } from 'lodash'; /** @@ -25,6 +19,13 @@ import { PanelActions, } from '@wordpress/components'; +import { + requestMediaImport, + mediaUploadSync, + requestImageFailedRetryDialog, + requestImageUploadCancelDialog, +} from '@wordpress/react-native-bridge'; + import { Caption, MediaPlaceholder, diff --git a/packages/block-library/src/media-text/media-container.native.js b/packages/block-library/src/media-text/media-container.native.js index 55022b18ff8cdd..88638692663ec6 100644 --- a/packages/block-library/src/media-text/media-container.native.js +++ b/packages/block-library/src/media-text/media-container.native.js @@ -2,11 +2,6 @@ * External dependencies */ import { View, ImageBackground, Text, TouchableWithoutFeedback } from 'react-native'; -import { - mediaUploadSync, - requestImageFailedRetryDialog, - requestImageUploadCancelDialog, -} from 'react-native-gutenberg-bridge'; /** * WordPress dependencies @@ -17,6 +12,11 @@ import { Toolbar, withNotices, } from '@wordpress/components'; +import { + mediaUploadSync, + requestImageFailedRetryDialog, + requestImageUploadCancelDialog, +} from '@wordpress/react-native-bridge'; import { BlockControls, MEDIA_TYPE_IMAGE, diff --git a/packages/block-library/src/video/edit.native.js b/packages/block-library/src/video/edit.native.js index bec2dfa7143e86..deaae87248fc04 100644 --- a/packages/block-library/src/video/edit.native.js +++ b/packages/block-library/src/video/edit.native.js @@ -3,14 +3,6 @@ */ import React from 'react'; import { View, TouchableWithoutFeedback, Text } from 'react-native'; -/** - * Internal dependencies - */ -import { - mediaUploadSync, - requestImageFailedRetryDialog, - requestImageUploadCancelDialog, -} from 'react-native-gutenberg-bridge'; /** * WordPress dependencies @@ -20,6 +12,11 @@ import { Toolbar, ToolbarButton, } from '@wordpress/components'; +import { + mediaUploadSync, + requestImageFailedRetryDialog, + requestImageUploadCancelDialog, +} from '@wordpress/react-native-bridge'; import { withPreferredColorScheme } from '@wordpress/compose'; import { Caption, diff --git a/packages/edit-post/src/components/layout/index.native.js b/packages/edit-post/src/components/layout/index.native.js index cc5a089a464ff7..d663d9b7989cfc 100644 --- a/packages/edit-post/src/components/layout/index.native.js +++ b/packages/edit-post/src/components/layout/index.native.js @@ -3,7 +3,6 @@ */ import { Platform, SafeAreaView, View } from 'react-native'; import SafeArea from 'react-native-safe-area'; -import { sendNativeEditorDidLayout } from 'react-native-gutenberg-bridge'; /** * WordPress dependencies @@ -14,6 +13,7 @@ import { BottomSheetSettings } from '@wordpress/block-editor'; import { compose, withPreferredColorScheme } from '@wordpress/compose'; import { HTMLTextInput, KeyboardAvoidingView, ReadableContentView } from '@wordpress/components'; import { AutosaveMonitor } from '@wordpress/editor'; +import { sendNativeEditorDidLayout } from '@wordpress/react-native-bridge'; /** * Internal dependencies diff --git a/packages/edit-post/src/editor.native.js b/packages/edit-post/src/editor.native.js index 74c2cec2e4b97d..83fcc70193d506 100644 --- a/packages/edit-post/src/editor.native.js +++ b/packages/edit-post/src/editor.native.js @@ -3,7 +3,6 @@ */ import memize from 'memize'; import { size, map, without } from 'lodash'; -import { subscribeSetFocusOnTitle } from 'react-native-gutenberg-bridge'; /** * WordPress dependencies @@ -14,6 +13,7 @@ import { parse, serialize } from '@wordpress/blocks'; import { withDispatch, withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; import { SlotFillProvider } from '@wordpress/components'; +import { subscribeSetFocusOnTitle } from '@wordpress/react-native-bridge'; /** * Internal dependencies diff --git a/packages/edit-post/src/test/editor.native.js b/packages/edit-post/src/test/editor.native.js index a48884c52e9ea0..1d5786111f7555 100644 --- a/packages/edit-post/src/test/editor.native.js +++ b/packages/edit-post/src/test/editor.native.js @@ -1,13 +1,13 @@ /** * External dependencies */ -import RNReactNativeGutenbergBridge from 'react-native-gutenberg-bridge'; import { mount } from 'enzyme'; import { act } from 'react-dom/test-utils'; /** * WordPress dependencies */ +import RNReactNativeGutenbergBridge from '@wordpress/react-native-bridge'; import { registerCoreBlocks } from '@wordpress/block-library'; // Force register 'core/editor' store. import { store } from '@wordpress/editor'; // eslint-disable-line no-unused-vars diff --git a/packages/editor/src/components/provider/index.native.js b/packages/editor/src/components/provider/index.native.js index 092f485ff5cc50..692414b4eed171 100644 --- a/packages/editor/src/components/provider/index.native.js +++ b/packages/editor/src/components/provider/index.native.js @@ -1,21 +1,17 @@ /** - * External dependencies + * WordPress dependencies */ +import { Component } from '@wordpress/element'; +import { parse, serialize, getUnregisteredTypeHandlerName, createBlock } from '@wordpress/blocks'; +import { withDispatch, withSelect } from '@wordpress/data'; +import { compose } from '@wordpress/compose'; import RNReactNativeGutenbergBridge, { subscribeParentGetHtml, subscribeParentToggleHTMLMode, subscribeUpdateHtml, subscribeSetTitle, subscribeMediaAppend, -} from 'react-native-gutenberg-bridge'; - -/** - * WordPress dependencies - */ -import { Component } from '@wordpress/element'; -import { parse, serialize, getUnregisteredTypeHandlerName, createBlock } from '@wordpress/blocks'; -import { withDispatch, withSelect } from '@wordpress/data'; -import { compose } from '@wordpress/compose'; +} from '@wordpress/react-native-bridge'; const postTypeEntities = [ { name: 'post', baseURL: '/wp/v2/posts' }, diff --git a/packages/editor/src/store/actions.native.js b/packages/editor/src/store/actions.native.js index 0154c92324640e..49d40749cb1c7a 100644 --- a/packages/editor/src/store/actions.native.js +++ b/packages/editor/src/store/actions.native.js @@ -1,7 +1,7 @@ /** - * External dependencies + * WordPress dependencies */ -import RNReactNativeGutenbergBridge from 'react-native-gutenberg-bridge'; +import RNReactNativeGutenbergBridge from '@wordpress/react-native-bridge'; export * from './actions.js'; diff --git a/packages/react-native-aztec/.flowconfig b/packages/react-native-aztec/.flowconfig new file mode 100644 index 00000000000000..1fed445333e85f --- /dev/null +++ b/packages/react-native-aztec/.flowconfig @@ -0,0 +1,11 @@ +[ignore] + +[include] + +[libs] + +[lints] + +[options] + +[strict] diff --git a/packages/react-native-aztec/.gitignore b/packages/react-native-aztec/.gitignore new file mode 100644 index 00000000000000..80b371f149ae9f --- /dev/null +++ b/packages/react-native-aztec/.gitignore @@ -0,0 +1,118 @@ +# OS X generated file +.DS_Store + +# built application files +*.apk +*.ap_ + +# files for the dex VM +*.dex + +# Java class files +*.class + +# generated files +bin/ +gen/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Eclipse project files +.settings/ +.classpath +.project + +# Intellij project files +*.iml +*.ipr +*.iws +.idea/ + +# Gradle +.gradle/ + +# Generated by gradle +crashlytics.properties + +# npm +node_modules/ + +# yarn +yarn-error.log + +### +### Starting at this point, the Swift .gitignore rules from https://github.com/github/gitignore/blob/master/Swift.gitignore +### + +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData/ + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside +*.xccheckout +*.xcscmblueprint + +## Obj-C/Swift specific +*.hmap +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +Pods/ + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. + +ios/Carthage +Carthage/Checkouts +Carthage/Build + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + diff --git a/packages/react-native-aztec/LICENSE b/packages/react-native-aztec/LICENSE new file mode 100644 index 00000000000000..d159169d105089 --- /dev/null +++ b/packages/react-native-aztec/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/packages/react-native-aztec/README.md b/packages/react-native-aztec/README.md new file mode 100644 index 00000000000000..2bda062f08aff2 --- /dev/null +++ b/packages/react-native-aztec/README.md @@ -0,0 +1,72 @@ +## Notice: We have git subtree'd this repo into https://github.com/wordpress-mobile/gutenberg-mobile. It is recommended that any new development happens on that one instead. Thanks! + +# react-native-aztec + +Wrapping Aztec Android and Aztec iOS in a React Native component + +# License + +GPL v2 + +## Android: Run the example app + +Make sure to have an emulator running or an Android device connected, and then: + +``` +$ cd example/ +$ yarn clean:install +$ yarn android +``` + +This will build the Android library (via `gradle`) and example app, then launch the main example activity on your connected device and run the Metro bundler at the same time. + +## iOS: Run the example app + +Before being able to run the Example App, you'll need to install [Carthage](https://github.com/Carthage/Carthage) and the dependencies for this project: +``` +cd ios +carthage bootstrap --platform iOS +``` + +Then go back to the root directory of the project and do: +``` +$ cd example/ +$ yarn clean:install +$ yarn ios +``` + +This will compile the example project, launch metro, run the simulator and run the app. + +## FAQ / Troubleshooting + +Q: The example app doesn't run + +A: Make sure you have yarn and babel installed (https://yarnpkg.com/lang/en/docs/install/) + + +Q: The example app gets compiled but ReactNative cannot connect to Metro bundler (I'm on a real device attached through USB) + +A: To debug on the device through USB, remember to revert ports before launching metro: +`adb reverse tcp:8081 tcp:8081` + + +Q: The example app gets compiled but ReactNative shows an error + +A: try running, from the root folder in the project +``` +$ cd example/ +$ yarn start --reset-cache +``` + +Open a new shell window and run either of these depending on the platform: + +``` +$ yarn android +``` + +or + +``` +$ yarn ios +``` + diff --git a/packages/react-native-aztec/android/build.gradle b/packages/react-native-aztec/android/build.gradle new file mode 100644 index 00000000000000..e13f9d1ac1586a --- /dev/null +++ b/packages/react-native-aztec/android/build.gradle @@ -0,0 +1,118 @@ +buildscript { + ext { + gradlePluginVersion = '3.3.1' + kotlinVersion = '1.3.11' + supportLibVersion = '28.0.0' + tagSoupVersion = '1.2.1' + glideVersion = '3.7.0' + picassoVersion = '2.5.2' + robolectricVersion = '3.5.1' + jUnitVersion = '4.12' + jSoupVersion = '1.10.3' + wordpressUtilsVersion = '1.22' + espressoVersion = '3.0.1' + + aztecVersion = 'v1.3.34' + } + + repositories { + jcenter() + google() + } + + dependencies { + classpath "com.android.tools.build:gradle:$gradlePluginVersion" + classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'com.github.dcendents.android-maven' + +// import the `readReactNativeVersion()` function +apply from: 'https://gist.githubusercontent.com/hypest/742448b9588b3a0aa580a5e80ae95bdf/raw/8eb62d40ee7a5104d2fcaeff21ce6f29bd93b054/readReactNativeVersion.gradle' + +group='com.github.wordpress-mobile.gutenberg-mobile' + +// fallback flag value for when lib is compiled individually (e.g. via jitpack) +project.ext.buildGutenbergFromSource = false + +// The sample build uses multiple directories to +// keep boilerplate and common code separate from +// the main sample code. +List dirs = [ + 'main', // main sample code; look here for the interesting stuff. + 'common', // components that are reused by multiple samples + 'template'] // boilerplate code that is generated by the sample template process + +android { + compileSdkVersion 28 + + defaultConfig { + minSdkVersion 21 + targetSdkVersion 28 + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + dirs.each { dir -> + java.srcDirs "src/${dir}/java" + java.srcDirs += "src/${dir}/kotlin" + res.srcDirs "src/${dir}/res" + } + } + + androidTest.setRoot('tests') + androidTest.java.srcDirs = ['tests/src'] + } + + lintOptions { + disable 'GradleCompatible' + } +} + +repositories { + jcenter() + google() + + maven { url "https://jitpack.io" } + + if (!rootProject.ext.buildGutenbergFromSource) { + // if not building from source (where the node_modules dir is used), use a remote RN maven repo + def reactNativeRepo = 'https://dl.bintray.com/wordpress-mobile/react-native-mirror/' + println "Will use the RN maven repo at ${reactNativeRepo}" + maven { url reactNativeRepo } + } +} + +dependencies { + api ("com.github.wordpress-mobile.WordPress-Aztec-Android:aztec:$aztecVersion") + api ("com.github.wordpress-mobile.WordPress-Aztec-Android:wordpress-shortcodes:$aztecVersion") + api ("com.github.wordpress-mobile.WordPress-Aztec-Android:wordpress-comments:$aztecVersion") + api ("com.github.wordpress-mobile.WordPress-Aztec-Android:glide-loader:$aztecVersion") + api "org.wordpress:utils:$wordpressUtilsVersion" + + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" + + implementation 'androidx.appcompat:appcompat:1.0.0' + + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.gridlayout:gridlayout:1.0.0' + implementation 'androidx.cardview:cardview:1.0.0' + implementation 'androidx.appcompat:appcompat:1.0.0' + implementation 'androidx.recyclerview:recyclerview:1.0.0' + + if (rootProject.ext.buildGutenbergFromSource) { + implementation "com.facebook.react:react-native:+" // From node_modules. + } else { + def rnVersion = readReactNativeVersion('../package.json', 'peerDependencies') + implementation "com.facebook.react:react-native:${rnVersion}" // From Maven repo + } +} diff --git a/packages/react-native-aztec/android/gradle.properties b/packages/react-native-aztec/android/gradle.properties new file mode 100644 index 00000000000000..938b16188c39d1 --- /dev/null +++ b/packages/react-native-aztec/android/gradle.properties @@ -0,0 +1,3 @@ +android.useAndroidX=true +android.enableJetifier=true + diff --git a/packages/react-native-aztec/android/gradle/wrapper/gradle-wrapper.jar b/packages/react-native-aztec/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000000000..29953ea141f55e Binary files /dev/null and b/packages/react-native-aztec/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/packages/react-native-aztec/android/gradle/wrapper/gradle-wrapper.properties b/packages/react-native-aztec/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000000000..d76b502e226a5c --- /dev/null +++ b/packages/react-native-aztec/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/packages/react-native-aztec/android/gradlew b/packages/react-native-aztec/android/gradlew new file mode 100755 index 00000000000000..cccdd3d517fc52 --- /dev/null +++ b/packages/react-native-aztec/android/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/packages/react-native-aztec/android/gradlew.bat b/packages/react-native-aztec/android/gradlew.bat new file mode 100644 index 00000000000000..e95643d6a2ca62 --- /dev/null +++ b/packages/react-native-aztec/android/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/packages/react-native-aztec/android/settings.gradle b/packages/react-native-aztec/android/settings.gradle new file mode 100644 index 00000000000000..b53737e2560ae3 --- /dev/null +++ b/packages/react-native-aztec/android/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'react-native-aztec' + diff --git a/packages/react-native-aztec/android/src/main/AndroidManifest.xml b/packages/react-native-aztec/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000000000..d937b1806d40fe --- /dev/null +++ b/packages/react-native-aztec/android/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecArrowKeyMovementMethod.java b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecArrowKeyMovementMethod.java new file mode 100644 index 00000000000000..97e25c35f02ee3 --- /dev/null +++ b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecArrowKeyMovementMethod.java @@ -0,0 +1,21 @@ +package org.wordpress.mobile.ReactNativeAztec; + +import android.text.Selection; +import android.text.Spannable; +import android.text.method.ArrowKeyMovementMethod; +import android.view.View; +import android.widget.TextView; + +public class ReactAztecArrowKeyMovementMethod extends ArrowKeyMovementMethod { + + @Override + public void onTakeFocus(TextView view, Spannable text, int dir) { + if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) { + if (view.getLayout() == null) { + Selection.setSelection(text, 0); // <-- setting caret to end of text + } + } else { + Selection.setSelection(text, text.length()); // <-- same as original Android implementation. Not sure if we should change this too + } + } +} diff --git a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecBackSpaceEvent.java b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecBackSpaceEvent.java new file mode 100644 index 00000000000000..01f05b4849c38c --- /dev/null +++ b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecBackSpaceEvent.java @@ -0,0 +1,49 @@ +package org.wordpress.mobile.ReactNativeAztec; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +/** + * Event emitted by Aztec native view when Backspace is detected. + */ +class ReactAztecBackspaceEvent extends Event { + + private static final String EVENT_NAME = "topTextInputBackspace"; + + private String mText; + private int mSelectionStart; + private int mSelectionEnd; + + public ReactAztecBackspaceEvent(int viewId, String text, int selectionStart, int selectionEnd) { + super(viewId); + mText = text; + mSelectionStart = selectionStart; + mSelectionEnd = selectionEnd; + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public boolean canCoalesce() { + return false; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); + } + + private WritableMap serializeEventData() { + WritableMap eventData = Arguments.createMap(); + eventData.putInt("target", getViewTag()); + eventData.putString("text", mText); + eventData.putInt("selectionStart", mSelectionStart); + eventData.putInt("selectionEnd", mSelectionEnd); + return eventData; + } +} diff --git a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecBlurEvent.java b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecBlurEvent.java new file mode 100644 index 00000000000000..3a8192b4397e63 --- /dev/null +++ b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecBlurEvent.java @@ -0,0 +1,39 @@ +package org.wordpress.mobile.ReactNativeAztec; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +/** + * Event emitted by Aztec native view when it loses focus. + */ +class ReactAztecBlurEvent extends Event { + + private static final String EVENT_NAME = "topBlur"; + + public ReactAztecBlurEvent(int viewId) { + super(viewId); + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public boolean canCoalesce() { + return false; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); + } + + private WritableMap serializeEventData() { + WritableMap eventData = Arguments.createMap(); + eventData.putInt("target", getViewTag()); + return eventData; + } +} diff --git a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecEndEditingEvent.java b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecEndEditingEvent.java new file mode 100644 index 00000000000000..b6f38a012bac94 --- /dev/null +++ b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecEndEditingEvent.java @@ -0,0 +1,46 @@ +package org.wordpress.mobile.ReactNativeAztec; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +/** + * Event emitted by AztecText native view when text editing ends, + * because of the user leaving the text input. + */ +class ReactAztecEndEditingEvent extends Event { + + private static final String EVENT_NAME = "topEndEditing"; + + private String mText; + + public ReactAztecEndEditingEvent( + int viewId, + String text) { + super(viewId); + mText = text; + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public boolean canCoalesce() { + return false; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); + } + + private WritableMap serializeEventData() { + WritableMap eventData = Arguments.createMap(); + eventData.putInt("target", getViewTag()); + eventData.putString("text", mText); + return eventData; + } +} diff --git a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecEnterEvent.java b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecEnterEvent.java new file mode 100644 index 00000000000000..3fc6d11653111d --- /dev/null +++ b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecEnterEvent.java @@ -0,0 +1,56 @@ +package org.wordpress.mobile.ReactNativeAztec; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +/** + * Event emitted by Aztec native view when KEYCODE_ENTER is detected. + */ +class ReactAztecEnterEvent extends Event { + + private static final String EVENT_NAME = "topTextInputEnter"; + + private String mText; + private int mSelectionStart; + private int mSelectionEnd; + private boolean mFiredAfterTextChanged; + private int mEventCount; + + public ReactAztecEnterEvent(int viewId, String text, int selectionStart, int selectionEnd, + boolean firedAfterTextChanged, int eventCount) { + super(viewId); + mText = text; + mSelectionStart = selectionStart; + mSelectionEnd = selectionEnd; + mFiredAfterTextChanged = firedAfterTextChanged; + mEventCount = eventCount; + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public boolean canCoalesce() { + return false; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); + } + + private WritableMap serializeEventData() { + WritableMap eventData = Arguments.createMap(); + eventData.putInt("target", getViewTag()); + eventData.putString("text", mText); + eventData.putInt("selectionStart", mSelectionStart); + eventData.putInt("selectionEnd", mSelectionEnd); + eventData.putBoolean("firedAfterTextChanged", mFiredAfterTextChanged); + eventData.putInt("eventCount", mEventCount); + return eventData; + } +} diff --git a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecFocusEvent.java b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecFocusEvent.java new file mode 100644 index 00000000000000..1cfb18fd136436 --- /dev/null +++ b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecFocusEvent.java @@ -0,0 +1,39 @@ +package org.wordpress.mobile.ReactNativeAztec; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +/** + * Event emitted by Aztec native view when it receives focus. + */ +class ReactAztecFocusEvent extends Event { + + private static final String EVENT_NAME = "topFocus"; + + public ReactAztecFocusEvent(int viewId) { + super(viewId); + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public boolean canCoalesce() { + return false; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); + } + + private WritableMap serializeEventData() { + WritableMap eventData = Arguments.createMap(); + eventData.putInt("target", getViewTag()); + return eventData; + } +} diff --git a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecFormattingChangeEvent.java b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecFormattingChangeEvent.java new file mode 100644 index 00000000000000..2929f2cda7ea5a --- /dev/null +++ b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecFormattingChangeEvent.java @@ -0,0 +1,45 @@ +package org.wordpress.mobile.ReactNativeAztec; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +/** + * Event emitted by Aztec native view when it receives focus. + */ +class ReactAztecFormattingChangeEvent extends Event { + + private static final String EVENT_NAME = "topFormatsChanges"; + + private String[] mFormats; + + public ReactAztecFormattingChangeEvent(int viewId, String[] formats) { + super(viewId); + this.mFormats = formats; + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public boolean canCoalesce() { + return false; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); + } + + private WritableMap serializeEventData() { + WritableMap eventData = Arguments.createMap(); + eventData.putInt("target", getViewTag()); + WritableArray newFormats = Arguments.fromArray(mFormats); + eventData.putArray("formats", newFormats); + return eventData; + } +} diff --git a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecManager.java b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecManager.java new file mode 100644 index 00000000000000..3388e171a1be51 --- /dev/null +++ b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecManager.java @@ -0,0 +1,681 @@ +package org.wordpress.mobile.ReactNativeAztec; + + +import android.graphics.Color; +import android.graphics.Typeface; +import android.os.Build; +import androidx.annotation.Nullable; +import android.text.Editable; +import android.text.Layout; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; + +import com.facebook.infer.annotation.Assertions; +import com.facebook.react.bridge.JSApplicationIllegalArgumentException; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.common.MapBuilder; +import com.facebook.react.uimanager.BaseViewManager; +import com.facebook.react.uimanager.LayoutShadowNode; +import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.ViewDefaults; +import com.facebook.react.uimanager.ViewProps; +import com.facebook.react.uimanager.annotations.ReactProp; +import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.react.views.scroll.ScrollEvent; +import com.facebook.react.views.scroll.ScrollEventType; +import com.facebook.react.views.text.DefaultStyleValuesUtil; +import com.facebook.react.views.text.ReactFontManager; +import com.facebook.react.views.text.ReactTextUpdate; +import com.facebook.react.views.textinput.ReactContentSizeChangedEvent; +import com.facebook.react.views.textinput.ReactTextChangedEvent; +import com.facebook.react.views.textinput.ReactTextInputEvent; +import com.facebook.react.views.textinput.ReactTextInputManager; +import com.facebook.react.views.textinput.ScrollWatcher; + +import org.wordpress.aztec.formatting.LinkFormatter; +import org.wordpress.aztec.glideloader.GlideImageLoader; +import org.wordpress.aztec.glideloader.GlideVideoThumbnailLoader; +import org.wordpress.aztec.plugins.CssUnderlinePlugin; +import org.wordpress.aztec.plugins.shortcodes.AudioShortcodePlugin; +import org.wordpress.aztec.plugins.shortcodes.CaptionShortcodePlugin; +import org.wordpress.aztec.plugins.shortcodes.VideoShortcodePlugin; +import org.wordpress.aztec.plugins.wpcomments.HiddenGutenbergPlugin; +import org.wordpress.aztec.plugins.wpcomments.WordPressCommentsPlugin; +import org.wordpress.aztec.plugins.wpcomments.toolbar.MoreToolbarButton; + +import java.util.Map; +import java.util.ArrayList; +import java.util.Arrays; + +public class ReactAztecManager extends BaseViewManager { + + public static final String REACT_CLASS = "RCTAztecView"; + + private static final int FOCUS_TEXT_INPUT = 1; + private static final int BLUR_TEXT_INPUT = 2; + private static final int UNSET = -1; + + // we define the same codes in ReactAztecText as they have for ReactNative's TextInput, so + // it's easier to handle focus between Aztec and TextInput instances on the same screen. + // see https://github.com/wordpress-mobile/react-native-aztec/pull/79 + private int mFocusTextInputCommandCode = FOCUS_TEXT_INPUT; // pre-init + private int mBlurTextInputCommandCode = BLUR_TEXT_INPUT; // pre-init + + private static final String TAG = "ReactAztecText"; + + private static final String BLOCK_TYPE_TAG_KEY = "tag"; + + public ReactAztecManager() { + initializeFocusAndBlurCommandCodes(); + } + + private void initializeFocusAndBlurCommandCodes() { + // For this, we'd like to keep track of potential command code changes in the future, + // so we obtain an instance of ReactTextInputManager and call getCommandsMap in our + // constructor to use the very same codes as TextInput does. + ReactTextInputManager reactTextInputManager = new ReactTextInputManager(); + Map map = reactTextInputManager.getCommandsMap(); + mFocusTextInputCommandCode = map.get("focusTextInput"); + mBlurTextInputCommandCode = map.get("blurTextInput"); + } + + @Override + public String getName() { + return REACT_CLASS; + } + + @Override + protected ReactAztecText createViewInstance(ThemedReactContext reactContext) { + ReactAztecText aztecText = new ReactAztecText(reactContext); + aztecText.setFocusableInTouchMode(false); + aztecText.setFocusable(false); + aztecText.setCalypsoMode(false); + // This is a temporary hack that sets the correct GB link color and underline + // see: https://github.com/wordpress-mobile/gutenberg-mobile/pull/1109 + aztecText.setLinkFormatter(new LinkFormatter(aztecText, + new LinkFormatter.LinkStyle( + Color.parseColor("#016087"), true) + )); + aztecText.addPlugin(new CssUnderlinePlugin()); + return aztecText; + } + + @Override + public LayoutShadowNode createShadowNodeInstance() { + return new ReactAztecTextShadowNode(); + } + + @Override + public Class getShadowNodeClass() { + return ReactAztecTextShadowNode.class; + } + + @Nullable + @Override + public Map getExportedCustomBubblingEventTypeConstants() { + return MapBuilder.builder() + /* .put( + "topSubmitEditing", + MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of( + "bubbled", "onSubmitEditing", "captured", "onSubmitEditingCapture")))*/ + .put( + "topChange", + MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of("bubbled", "onChange"))) + .put( + "topFormatsChanges", + MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of("bubbled", "onActiveFormatsChange"))) + .put( + "topEndEditing", + MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of("bubbled", "onEndEditing", "captured", "onEndEditingCapture"))) + .put( + "topTextInput", + MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of("bubbled", "onTextInput", "captured", "onTextInputCapture"))) + .put( + "topTextInputEnter", + MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of("bubbled", "onEnter"))) + .put( + "topTextInputBackspace", + MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of("bubbled", "onBackspace"))) + .put( + "topTextInputPaste", + MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of("bubbled", "onPaste"))) + .put( + "topFocus", + MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of("bubbled", "onFocus", "captured", "onFocusCapture"))) + .put( + "topBlur", + MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of("bubbled", "onBlur", "captured", "onBlurCapture"))) + /* .put( + "topKeyPress", + MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of("bubbled", "onKeyPress", "captured", "onKeyPressCapture")))*/ + .build(); + } + + @Nullable + @Override + public Map getExportedCustomDirectEventTypeConstants() { + return MapBuilder.of( + "topSelectionChange", + MapBuilder.of("registrationName", "onSelectionChange") + ); + } + + @ReactProp(name = "text") + public void setText(ReactAztecText view, ReadableMap inputMap) { + if (!inputMap.hasKey("eventCount")) { + setTextfromJS(view, inputMap.getString("text"), inputMap.getMap("selection")); + } else { + // Don't think there is necessity of this branch, but justin case we want to + // force a 2nd setText from JS side to Native, just set a high eventCount + int eventCount = inputMap.getInt("eventCount"); + + if (view.mNativeEventCount < eventCount) { + setTextfromJS(view, inputMap.getString("text"), inputMap.getMap("selection")); + } + } + } + + private void setTextfromJS(ReactAztecText view, String text, @Nullable ReadableMap selection) { + view.setIsSettingTextFromJS(true); + view.disableOnSelectionListener(); + view.fromHtml(text, true); + view.enableOnSelectionListener(); + view.setIsSettingTextFromJS(false); + updateSelectionIfNeeded(view, selection); + } + + private void updateSelectionIfNeeded(ReactAztecText view, @Nullable ReadableMap selection) { + if ( selection != null ) { + int start = selection.getInt("start"); + int end = selection.getInt("end"); + view.setSelection(start, end); + } + } + + @ReactProp(name = "activeFormats", defaultBoolean = false) + public void setActiveFormats(final ReactAztecText view, @Nullable ReadableArray activeFormats) { + if (activeFormats != null) { + String[] activeFormatsArray = new String[activeFormats.size()]; + for (int i = 0; i < activeFormats.size(); i++) { + activeFormatsArray[i] = activeFormats.getString(i); + } + view.setActiveFormats(Arrays.asList(activeFormatsArray)); + } else { + view.setActiveFormats(new ArrayList()); + } + } + + /* + The code below was taken from the class ReactTextInputManager + */ + @ReactProp(name = ViewProps.FONT_SIZE, defaultFloat = ViewDefaults.FONT_SIZE_SP) + public void setFontSize(ReactAztecText view, float fontSize) { + view.setTextSize( + TypedValue.COMPLEX_UNIT_PX, + (int) Math.ceil(PixelUtil.toPixelFromSP(fontSize))); + } + + @ReactProp(name = ViewProps.FONT_FAMILY) + public void setFontFamily(ReactAztecText view, String fontFamily) { + int style = Typeface.NORMAL; + if (view.getTypeface() != null) { + style = view.getTypeface().getStyle(); + } + Typeface newTypeface = ReactFontManager.getInstance().getTypeface( + fontFamily, + style, + view.getContext().getAssets()); + view.setTypeface(newTypeface); + } + + /** + /* This code was taken from the method setFontWeight of the class ReactTextShadowNode + /* TODO: Factor into a common place they can both use + */ + @ReactProp(name = ViewProps.FONT_WEIGHT) + public void setFontWeight(ReactAztecText view, @Nullable String fontWeightString) { + int fontWeightNumeric = fontWeightString != null ? + parseNumericFontWeight(fontWeightString) : -1; + int fontWeight = UNSET; + if (fontWeightNumeric >= 500 || "bold".equals(fontWeightString)) { + fontWeight = Typeface.BOLD; + } else if ("normal".equals(fontWeightString) || + (fontWeightNumeric != -1 && fontWeightNumeric < 500)) { + fontWeight = Typeface.NORMAL; + } + Typeface currentTypeface = view.getTypeface(); + if (currentTypeface == null) { + currentTypeface = Typeface.DEFAULT; + } + if (fontWeight != currentTypeface.getStyle()) { + view.setTypeface(currentTypeface, fontWeight); + } + } + + /** + /* This code was taken from the method setFontStyle of the class ReactTextShadowNode + /* TODO: Factor into a common place they can both use + */ + @ReactProp(name = ViewProps.FONT_STYLE) + public void setFontStyle(ReactAztecText view, @Nullable String fontStyleString) { + int fontStyle = UNSET; + if ("italic".equals(fontStyleString)) { + fontStyle = Typeface.ITALIC; + } else if ("normal".equals(fontStyleString)) { + fontStyle = Typeface.NORMAL; + } + + Typeface currentTypeface = view.getTypeface(); + if (currentTypeface == null) { + currentTypeface = Typeface.DEFAULT; + } + if (fontStyle != currentTypeface.getStyle()) { + view.setTypeface(currentTypeface, fontStyle); + } + } + + /** + * This code was taken from the method parseNumericFontWeight of the class ReactTextShadowNode + * TODO: Factor into a common place they can both use + * + * Return -1 if the input string is not a valid numeric fontWeight (100, 200, ..., 900), otherwise + * return the weight. + */ + private static int parseNumericFontWeight(String fontWeightString) { + // This should be much faster than using regex to verify input and Integer.parseInt + return fontWeightString.length() == 3 && fontWeightString.endsWith("00") + && fontWeightString.charAt(0) <= '9' && fontWeightString.charAt(0) >= '1' ? + 100 * (fontWeightString.charAt(0) - '0') : -1; + } + + /** + * This method is based on {@link ReactTextInputManager#setTextAlign}. The only change made to that method is to + * use {@link android.widget.TextView#setGravity} instead of a custom method that preserves vertical gravity + * like the setGravityHorizontal method from {@link com.facebook.react.views.textinput.ReactEditText}. The + * reason for this change was to simplify the code. Since we never set vertical gravity, we do not need to add + * the complexity of preserving vertical gravity—we can just use {@link android.widget.TextView#setGravity} + * directly. + */ + @ReactProp(name = ViewProps.TEXT_ALIGN) + public void setTextAlign(ReactAztecText view, @Nullable String textAlign) { + if ("justify".equals(textAlign)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + view.setJustificationMode(Layout.JUSTIFICATION_MODE_INTER_WORD); + } + view.setGravity(Gravity.START); + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + view.setJustificationMode(Layout.JUSTIFICATION_MODE_NONE); + } + + if (textAlign == null || "auto".equals(textAlign)) { + view.setGravity(Gravity.NO_GRAVITY); + } else if ("left".equals(textAlign)) { + view.setGravity(Gravity.START); + } else if ("right".equals(textAlign)) { + view.setGravity(Gravity.END); + } else if ("center".equals(textAlign)) { + view.setGravity(Gravity.CENTER_HORIZONTAL); + } else { + throw new JSApplicationIllegalArgumentException("Invalid textAlign: " + textAlign); + } + } + } + + /* End of the code taken from ReactTextInputManager */ + + @ReactProp(name = "color", customType = "Color") + public void setColor(ReactAztecText view, @Nullable Integer color) { + int newColor = Color.BLACK; + if (color != null) { + newColor = color; + } + view.setTextColor(newColor); + } + + @ReactProp(name = "blockType") + public void setBlockType(ReactAztecText view, ReadableMap inputMap) { + if (inputMap.hasKey(BLOCK_TYPE_TAG_KEY)) { + view.setTagName(inputMap.getString(BLOCK_TYPE_TAG_KEY)); + } + } + + @ReactProp(name = "placeholder") + public void setPlaceholder(ReactAztecText view, @Nullable String placeholder) { + view.setHint(placeholder); + } + + @ReactProp(name = "placeholderTextColor", customType = "Color") + public void setPlaceholderTextColor(ReactAztecText view, @Nullable Integer color) { + if (color == null) { + view.setHintTextColor(DefaultStyleValuesUtil.getDefaultTextColorHint(view.getContext())); + } else { + view.setHintTextColor(color); + } + } + + @ReactProp(name = "maxImagesWidth") + public void setMaxImagesWidth(ReactAztecText view, int maxWidth) { + view.setMaxImagesWidth(maxWidth); + } + + @ReactProp(name = "minImagesWidth") + public void setMinImagesWidth(ReactAztecText view, int minWidth) { + view.setMinImagesWidth(minWidth); + } + + /* + * This property/method is used to disable the Gutenberg compatibility mode on AztecRN. + * + * Aztec comes along with some nice plugins that are able to show preview of Pictures/Videos/shortcodes, + * and WP specific features, in the visual editor. + * + * We don't need those improvements in Gutenberg mobile, so this RN wrapper around Aztec + * that's only used in GB-mobile at the moment, does have them OFF by default. + * + * An external 3rd party RN-app can use AztecRN wrapper and set the `disableGutenbergMode` to false to have a fully + * working visual editor. See the demo app, where `disableGutenbergMode` is already OFF. + */ + @ReactProp(name = "disableGutenbergMode", defaultBoolean = false) + public void disableGBMode(final ReactAztecText view, boolean disable) { + if (disable) { + view.addPlugin(new WordPressCommentsPlugin(view)); + view.addPlugin(new MoreToolbarButton(view)); + view.addPlugin(new CaptionShortcodePlugin(view)); + view.addPlugin(new VideoShortcodePlugin()); + view.addPlugin(new AudioShortcodePlugin()); + view.addPlugin(new HiddenGutenbergPlugin(view)); + view.setImageGetter(new GlideImageLoader(view.getContext())); + view.setVideoThumbnailGetter(new GlideVideoThumbnailLoader(view.getContext())); + // we need to restart the editor now + String content = view.toHtml(view.getText(), false); + view.fromHtml(content, false); + } + } + + @ReactProp(name = "onContentSizeChange", defaultBoolean = false) + public void setOnContentSizeChange(final ReactAztecText view, boolean onContentSizeChange) { + if (onContentSizeChange) { + view.setContentSizeWatcher(new AztecContentSizeWatcher(view)); + } else { + view.setContentSizeWatcher(null); + } + } + + @ReactProp(name = "onSelectionChange", defaultBoolean = false) + public void setOnSelectionChange(final ReactAztecText view, boolean onSelectionChange) { + view.shouldHandleOnSelectionChange = onSelectionChange; + } + + @ReactProp(name = "onScroll", defaultBoolean = false) + public void setOnScroll(final ReactAztecText view, boolean onScroll) { + if (onScroll) { + view.setScrollWatcher(new AztecScrollWatcher(view)); + } else { + view.setScrollWatcher(null); + } + } + + @ReactProp(name = "onEnter", defaultBoolean = false) + public void setOnEnterHandling(final ReactAztecText view, boolean onEnterHandling) { + view.shouldHandleOnEnter = onEnterHandling; + } + + @ReactProp(name = "onBackspace", defaultBoolean = false) + public void setOnBackspaceHandling(final ReactAztecText view, boolean onBackspaceHandling) { + view.shouldHandleOnBackspace = onBackspaceHandling; + } + + @ReactProp(name = "onPaste", defaultBoolean = false) + public void setOnPasteHandling(final ReactAztecText view, boolean onPasteHandling) { + view.shouldHandleOnPaste = onPasteHandling; + } + + @ReactProp(name = "deleteEnter", defaultBoolean = false) + public void setShouldDeleteEnter(final ReactAztecText view, boolean shouldDeleteEnter) { + view.shouldDeleteEnter = shouldDeleteEnter; + } + + @Override + public Map getCommandsMap() { + return MapBuilder.builder() + .put("focusTextInput", mFocusTextInputCommandCode) + .put("blurTextInput", mBlurTextInputCommandCode) + .build(); + } + + @Override + public void receiveCommand(final ReactAztecText parent, int commandType, @Nullable ReadableArray args) { + Assertions.assertNotNull(parent); + if (commandType == mFocusTextInputCommandCode) { + parent.requestFocusFromJS(); + return; + } else if (commandType == mBlurTextInputCommandCode) { + parent.clearFocusFromJS(); + return; + } + super.receiveCommand(parent, commandType, args); + } + + @Override + protected void addEventEmitters(final ThemedReactContext reactContext, final ReactAztecText aztecText) { + aztecText.addTextChangedListener(new AztecTextWatcher(reactContext, aztecText)); + aztecText.setOnFocusChangeListener( + new View.OnFocusChangeListener() { + public void onFocusChange(View v, boolean hasFocus) { + EventDispatcher eventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); + final ReactAztecText editText = (ReactAztecText)v; + if (hasFocus) { + eventDispatcher.dispatchEvent( + new ReactAztecFocusEvent( + editText.getId())); + } else { + eventDispatcher.dispatchEvent( + new ReactAztecBlurEvent( + editText.getId())); + + eventDispatcher.dispatchEvent( + new ReactAztecEndEditingEvent( + editText.getId(), + editText.toHtml(editText.getText(), false))); + } + } + }); + + // Don't think we need to add setOnEditorActionListener here (intercept Enter for example), but + // in case check ReactTextInputManager + } + + @Override + public void updateExtraData(ReactAztecText view, Object extraData) { + if (extraData instanceof ReactTextUpdate) { + ReactTextUpdate update = (ReactTextUpdate) extraData; + + view.setPadding( + (int) update.getPaddingLeft(), + (int) update.getPaddingTop(), + (int) update.getPaddingRight(), + (int) update.getPaddingBottom()); + } + } + + private class AztecTextWatcher implements TextWatcher { + + private EventDispatcher mEventDispatcher; + private ReactAztecText mEditText; + private String mPreviousText; + + public AztecTextWatcher(final ReactContext reactContext, final ReactAztecText aztecText) { + mEventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); + mEditText = aztecText; + mPreviousText = null; + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Incoming charSequence gets mutated before onTextChanged() is invoked + mPreviousText = s.toString(); + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // Rearranging the text (i.e. changing between singleline and multiline attributes) can + // also trigger onTextChanged, call the event in JS only when the text actually changed + if (count == 0 && before == 0) { + return; + } + + Assertions.assertNotNull(mPreviousText); + String newText = s.toString().substring(start, start + count); + String oldText = mPreviousText.substring(start, start + before); + // Don't send same text changes + if (count == before && newText.equals(oldText)) { + return; + } + + // if the "Enter" handling is underway, don't sent text change events. The ReactAztecEnterEvent will have + // the text (minus the Enter char itself). + if (!mEditText.isEnterPressedUnderway()) { + int currentEventCount = mEditText.incrementAndGetEventCounter(); + // The event that contains the event counter and updates it must be sent first. + // TODO: t7936714 merge these events + mEventDispatcher.dispatchEvent( + new ReactTextChangedEvent( + mEditText.getId(), + mEditText.toHtml(mEditText.getText(), false), + currentEventCount)); + + mEventDispatcher.dispatchEvent( + new ReactTextInputEvent( + mEditText.getId(), + newText, + oldText, + start, + start + before)); + } + + + if (mPreviousText.length() == 0 + && !TextUtils.isEmpty(newText) + && !TextUtils.isEmpty(mEditText.getTagName()) + && mEditText.getSelectedStyles().isEmpty()) { + + // Some block types (e.g. header block ) need to be created with default style (e.g. h2) + // In order to achieve that, we need to toggle formatting with proper style, + // otherwise header block won't be created with style, it will be presented as plain text + ReactAztecTextFormatEnum reactAztecTextFormat = ReactAztecTextFormatEnum.get(mEditText.getTagName()); + if (reactAztecTextFormat != null) { + mEditText.toggleFormatting(reactAztecTextFormat.getAztecTextFormat()); + } + } + } + + @Override + public void afterTextChanged(Editable s) { + } + } + + private class AztecContentSizeWatcher implements com.facebook.react.views.textinput.ContentSizeWatcher { + private ReactAztecText mReactAztecText; + private EventDispatcher mEventDispatcher; + private int mPreviousContentWidth = 0; + private int mPreviousContentHeight = 0; + + public AztecContentSizeWatcher(ReactAztecText view) { + mReactAztecText = view; + ReactContext reactContext = (ReactContext) mReactAztecText.getContext(); + mEventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); + } + + @Override + public void onLayout() { + int contentWidth = mReactAztecText.getWidth(); + int contentHeight = mReactAztecText.getHeight(); + + // Use instead size of text content within EditText when available + if (mReactAztecText.getLayout() != null) { + contentWidth = mReactAztecText.getCompoundPaddingLeft() + mReactAztecText.getLayout().getWidth() + + mReactAztecText.getCompoundPaddingRight(); + contentHeight = mReactAztecText.getCompoundPaddingTop() + mReactAztecText.getLayout().getHeight() + + mReactAztecText.getCompoundPaddingBottom(); + } + + if (contentWidth != mPreviousContentWidth || contentHeight != mPreviousContentHeight) { + mPreviousContentHeight = contentHeight; + mPreviousContentWidth = contentWidth; + + // FIXME: Note the 2 hacks here + mEventDispatcher.dispatchEvent( + new ReactContentSizeChangedEvent( + mReactAztecText.getId(), + PixelUtil.toDIPFromPixel(contentWidth), + PixelUtil.toDIPFromPixel(contentHeight))); + } + } + } + + private class AztecScrollWatcher implements ScrollWatcher { + + private ReactAztecText mReactAztecText; + private EventDispatcher mEventDispatcher; + private int mPreviousHoriz; + private int mPreviousVert; + + public AztecScrollWatcher(ReactAztecText editText) { + mReactAztecText = editText; + ReactContext reactContext = (ReactContext) editText.getContext(); + mEventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); + } + + @Override + public void onScrollChanged(int horiz, int vert, int oldHoriz, int oldVert) { + if (mPreviousHoriz != horiz || mPreviousVert != vert) { + ScrollEvent event = ScrollEvent.obtain( + mReactAztecText.getId(), + ScrollEventType.SCROLL, + horiz, + vert, + 0f, // can't get x velocity + 0f, // can't get y velocity + 0, // can't get content width + 0, // can't get content height + mReactAztecText.getWidth(), + mReactAztecText.getHeight()); + + mEventDispatcher.dispatchEvent(event); + + mPreviousHoriz = horiz; + mPreviousVert = vert; + } + } + } +} diff --git a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecPackage.java b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecPackage.java new file mode 100644 index 00000000000000..b23c6c90ffb49f --- /dev/null +++ b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecPackage.java @@ -0,0 +1,27 @@ +package org.wordpress.mobile.ReactNativeAztec; + + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ReactAztecPackage implements ReactPackage { + + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + List views = new ArrayList<>(); + views.add(new ReactAztecManager()); + return views; + } + + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } + +} \ No newline at end of file diff --git a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecPasteEvent.java b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecPasteEvent.java new file mode 100644 index 00000000000000..0b6557ba9aaa7f --- /dev/null +++ b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecPasteEvent.java @@ -0,0 +1,56 @@ +package org.wordpress.mobile.ReactNativeAztec; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +/** + * Event emitted by Aztec native view when paste is detected. + */ +class ReactAztecPasteEvent extends Event { + + private static final String EVENT_NAME = "topTextInputPaste"; + + private String mCurrentContent; + private int mSelectionStart; + private int mSelectionEnd; + private String mPastedText; + private String mPastedHtml; + + public ReactAztecPasteEvent(int viewId, String currentContent, int selectionStart, + int selectionEnd, String pastedText, String pastedHtml) { + super(viewId); + mCurrentContent = currentContent; + mSelectionStart = selectionStart; + mSelectionEnd = selectionEnd; + mPastedText = pastedText; + mPastedHtml = pastedHtml; + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public boolean canCoalesce() { + return false; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); + } + + private WritableMap serializeEventData() { + WritableMap eventData = Arguments.createMap(); + eventData.putInt("target", getViewTag()); + eventData.putString("currentContent", mCurrentContent); + eventData.putInt("selectionStart", mSelectionStart); + eventData.putInt("selectionEnd", mSelectionEnd); + eventData.putString("pastedText", mPastedText); + eventData.putString("pastedHtml", mPastedHtml); + return eventData; + } +} diff --git a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecSelectionChangeEvent.java b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecSelectionChangeEvent.java new file mode 100644 index 00000000000000..bb553b9daf334f --- /dev/null +++ b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecSelectionChangeEvent.java @@ -0,0 +1,53 @@ +package org.wordpress.mobile.ReactNativeAztec; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +/** + * Event emitted by Aztec native view when selection changes. + */ +class ReactAztecSelectionChangeEvent extends Event { + + private static final String EVENT_NAME = "topSelectionChange"; + + private String mText; + private int mSelectionStart; + private int mSelectionEnd; + private int mEventCount; + + public ReactAztecSelectionChangeEvent(int viewId, String text, int selectionStart, int selectionEnd, int eventCount) { + super(viewId); + mText = text; + mSelectionStart = selectionStart; + mSelectionEnd = selectionEnd; + mEventCount = eventCount; + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public boolean canCoalesce() { + return false; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData()); + } + + private WritableMap serializeEventData() { + WritableMap eventData = Arguments.createMap(); + eventData.putInt("target", getViewTag()); + eventData.putString("text", mText); + eventData.putInt("selectionStart", mSelectionStart); + eventData.putInt("selectionEnd", mSelectionEnd); + eventData.putInt("eventCount", mEventCount); + return eventData; + } +} diff --git a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java new file mode 100644 index 00000000000000..475c30ba7a0f53 --- /dev/null +++ b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java @@ -0,0 +1,535 @@ +package org.wordpress.mobile.ReactNativeAztec; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.graphics.Rect; +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.Nullable; +import android.text.Editable; +import android.text.InputType; +import android.text.Spannable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.view.inputmethod.InputMethodManager; + +import com.facebook.infer.annotation.Assertions; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.react.views.textinput.ContentSizeWatcher; +import com.facebook.react.views.textinput.ReactTextInputLocalData; +import com.facebook.react.views.textinput.ScrollWatcher; + +import org.wordpress.aztec.AztecText; +import org.wordpress.aztec.AztecTextFormat; +import org.wordpress.aztec.ITextFormat; +import org.wordpress.aztec.plugins.IAztecPlugin; +import org.wordpress.aztec.plugins.IToolbarButton; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.Set; +import java.util.HashSet; +import java.util.HashMap; + +import static android.content.ClipData.*; + +public class ReactAztecText extends AztecText { + + private final InputMethodManager mInputMethodManager; + // This flag is set to true when we set the text of the EditText explicitly. In that case, no + // *TextChanged events should be triggered. This is less expensive than removing the text + // listeners and adding them back again after the text change is completed. + private boolean mIsSettingTextFromJS = false; + // This component is controlled, so we want it to get focused only when JS ask it to do so. + // Whenever android requests focus (which it does for random reasons), it will be ignored. + private boolean mIsJSSettingFocus = false; + private @Nullable ArrayList mListeners; + private @Nullable TextWatcherDelegator mTextWatcherDelegator; + private @Nullable ContentSizeWatcher mContentSizeWatcher; + private @Nullable ScrollWatcher mScrollWatcher; + + // FIXME: Used in `incrementAndGetEventCounter` but never read. I guess we can get rid of it, but before this + // check when it's used in EditText in RN. (maybe tests?) + int mNativeEventCount = 0; + + String lastSentFormattingOptionsEventString = ""; + boolean shouldHandleOnEnter = false; + boolean shouldHandleOnBackspace = false; + boolean shouldHandleOnPaste = false; + boolean shouldHandleOnSelectionChange = false; + boolean shouldHandleActiveFormatsChange = false; + + boolean shouldDeleteEnter = false; + + // This optional variable holds the outer HTML tag that will be added to the text when the user start typing in it + // This is required to keep placeholder text working, and start typing with styled text. + // Ref: https://github.com/wordpress-mobile/gutenberg-mobile/issues/707 + private String mTagName = ""; + private String mEmptyTagHTML = ""; + + private static final HashMap typingFormatsMap = new HashMap() { + { + put(AztecTextFormat.FORMAT_BOLD, "bold"); + put(AztecTextFormat.FORMAT_STRONG, "bold"); + put(AztecTextFormat.FORMAT_EMPHASIS, "italic"); + put(AztecTextFormat.FORMAT_ITALIC, "italic"); + put(AztecTextFormat.FORMAT_CITE, "italic"); + put(AztecTextFormat.FORMAT_STRIKETHROUGH, "strikethrough"); + put(AztecTextFormat.FORMAT_UNDERLINE, "underline"); + } + }; + + public ReactAztecText(ThemedReactContext reactContext) { + super(reactContext); + + setGutenbergMode(true); + + // don't auto-focus when Aztec becomes visible. + // Needed on rotation and multiple Aztec instances to avoid losing the exact care position. + setFocusOnVisible(false); + + forceCaretAtStartOnTakeFocus(); + + this.setAztecKeyListener(new ReactAztecText.OnAztecKeyListener() { + @Override + public boolean onEnterKey(Spannable text, boolean firedAfterTextChanged, int selStart, int selEnd) { + if (shouldHandleOnEnter && !isTextChangedListenerDisabled()) { + return onEnter(text, firedAfterTextChanged, selStart, selEnd); + } + return false; + } + @Override + public boolean onBackspaceKey() { + if (shouldHandleOnBackspace && !isTextChangedListenerDisabled()) { + String content = toHtml(getText(), false); + if (TextUtils.isEmpty(content)) { + return onBackspace(); + } + else { + if (!content.equals(mEmptyTagHTML)) { + return onBackspace(); + } + } + } + return false; + } + }); + + mInputMethodManager = (InputMethodManager) + Assertions.assertNotNull(getContext().getSystemService(Context.INPUT_METHOD_SERVICE)); + this.setOnSelectionChangedListener(new OnSelectionChangedListener() { + @Override + public void onSelectionChanged(int selStart, int selEnd) { + ReactAztecText.this.updateToolbarButtons(selStart, selEnd); + ReactAztecText.this.propagateSelectionChanges(selStart, selEnd); + } + }); + this.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_MULTI_LINE); + } + + private void forceCaretAtStartOnTakeFocus() { + // set a custom ArrowKeyMovementMethod: sets caret to the start of the text instead of the default (end of text) + // Fixes https://github.com/wordpress-mobile/gutenberg-mobile/issues/602 + // onTakeFocus adapted from the Android source code at: + // https://android.googlesource.com/platform/frameworks/base/+/refs/heads/pie-release/core/java/android/text/method/ArrowKeyMovementMethod.java#316 + setMovementMethod(new ReactAztecArrowKeyMovementMethod()); + } + + @Override + public void refreshText() { + super.refreshText(); + onContentSizeChange(); + } + + void addPlugin(IAztecPlugin plugin) { + super.getPlugins().add(plugin); + if (plugin instanceof IToolbarButton && getToolbar() != null ) { + getToolbar().addButton((IToolbarButton)plugin); + } + } + + @Override + public boolean onTextContextMenuItem(int id) { + if (shouldHandleOnPaste) { + switch (id) { + case android.R.id.paste: + return onPaste(false); + case android.R.id.pasteAsPlainText: + return onPaste(true); + } + } + + return super.onTextContextMenuItem(id); + } + + // VisibleForTesting from {@link TextInputEventsTestCase}. + public void requestFocusFromJS() { + mIsJSSettingFocus = true; + requestFocus(); + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override + public void run() { + // let's pinpoint the caret line to ask the system to bring that line into the viewport + int lineNumber = getLayout().getLineForOffset(getSelectionStart()); + + Rect caretLineRect = new Rect(); + getLineBounds(lineNumber, caretLineRect); + requestRectangleOnScreen(caretLineRect); + } + }, 100); + mIsJSSettingFocus = false; + } + + void clearFocusFromJS() { + clearFocus(); + } + + @Override + public void clearFocus() { + setFocusableInTouchMode(false); + setFocusable(false); + super.clearFocus(); + hideSoftKeyboard(); + } + + @Override + public boolean requestFocus(int direction, Rect previouslyFocusedRect) { + // Always return true if we are already focused. This is used by android in certain places, + // such as text selection. + if (isFocused()) { + return true; + } + //TODO check why it's needed - doesn't seem to work fine with this in it, since each focus call + // from the Android FW is skipped here. + /*if (!mIsJSSettingFocus) { + return false; + }*/ + setFocusableInTouchMode(true); + setFocusable(true); + boolean focused = super.requestFocus(direction, previouslyFocusedRect); + showSoftKeyboard(); + return focused; + } + + private void showSoftKeyboard() { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (mInputMethodManager != null) { + mInputMethodManager.showSoftInput(ReactAztecText.this, 0); + } + } + }); + } + + private void hideSoftKeyboard() { + mInputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); + } + + public void setScrollWatcher(ScrollWatcher scrollWatcher) { + mScrollWatcher = scrollWatcher; + } + + @Override + protected void onScrollChanged(int horiz, int vert, int oldHoriz, int oldVert) { + super.onScrollChanged(horiz, vert, oldHoriz, oldVert); + + if (mScrollWatcher != null) { + mScrollWatcher.onScrollChanged(horiz, vert, oldHoriz, oldVert); + } + } + + public void setContentSizeWatcher(ContentSizeWatcher contentSizeWatcher) { + mContentSizeWatcher = contentSizeWatcher; + } + + private void onContentSizeChange() { + if (mContentSizeWatcher != null) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (mContentSizeWatcher != null) { + mContentSizeWatcher.onLayout(); + } + } + }); + } + setIntrinsicContentSize(); + } + + public void setTagName(@Nullable String tagName) { + mTagName = tagName; + mEmptyTagHTML = "<" + mTagName + ">"; + } + + public String getTagName() { + return mTagName; + } + + + private void updateToolbarButtons(int selStart, int selEnd) { + ArrayList appliedStyles = getAppliedStyles(selStart, selEnd); + updateToolbarButtons(appliedStyles); + } + + private void updateToolbarButtons(ArrayList appliedStyles) { + // Read the applied styles and get the String list of formatting options + LinkedList formattingOptions = new LinkedList<>(); + for (ITextFormat currentStyle : appliedStyles) { + if ((currentStyle == AztecTextFormat.FORMAT_STRONG || currentStyle == AztecTextFormat.FORMAT_BOLD) + && !formattingOptions.contains("bold")) { + formattingOptions.add("bold"); + } + if ((currentStyle == AztecTextFormat.FORMAT_ITALIC || currentStyle == AztecTextFormat.FORMAT_CITE) + && !formattingOptions.contains("italic")) { + formattingOptions.add("italic"); + } + if (currentStyle == AztecTextFormat.FORMAT_STRIKETHROUGH) { + formattingOptions.add("strikethrough"); + } + } + + // Check if the same formatting event was already sent + String newOptionsAsString = ""; + for (String currentFormatting: formattingOptions) { + newOptionsAsString += currentFormatting; + } + if (newOptionsAsString.equals(lastSentFormattingOptionsEventString)) { + // no need to send any event now + return; + } + lastSentFormattingOptionsEventString = newOptionsAsString; + + if (shouldHandleActiveFormatsChange) { + ReactContext reactContext = (ReactContext) getContext(); + EventDispatcher eventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); + eventDispatcher.dispatchEvent( + new ReactAztecFormattingChangeEvent( + getId(), + formattingOptions.toArray(new String[formattingOptions.size()]) + ) + ); + } + } + + private void propagateSelectionChanges(int selStart, int selEnd) { + if (!shouldHandleOnSelectionChange) { + return; + } + String content = toHtml(getText(), false); + ReactContext reactContext = (ReactContext) getContext(); + EventDispatcher eventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); + eventDispatcher.dispatchEvent( + new ReactAztecSelectionChangeEvent(getId(), content, selStart, selEnd, incrementAndGetEventCounter()) + ); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + onContentSizeChange(); + } + + private void setIntrinsicContentSize() { + ReactContext reactContext = (ReactContext) getContext(); + UIManagerModule uiManager = reactContext.getNativeModule(UIManagerModule.class); + final ReactTextInputLocalData localData = new ReactTextInputLocalData(this); + uiManager.setViewLocalData(getId(), localData); + } + + //// Text changed events + + public int incrementAndGetEventCounter() { + return ++mNativeEventCount; + } + + @Override + public void addTextChangedListener(TextWatcher watcher) { + if (mListeners == null) { + mListeners = new ArrayList<>(); + super.addTextChangedListener(getTextWatcherDelegator()); + + // Keep the enter pressed watcher at the beginning of the watchers list. + // We want to intercept Enter.key as soon as possible, and before other listeners start modifying the text. + // Also note that this Watchers, when the AztecKeyListener is set, keep hold a copy of the content in the editor. + mListeners.add(new EnterPressedWatcher(this, new EnterDeleter() { + @Override + public boolean shouldDeleteEnter() { + return shouldDeleteEnter; + } + })); + } + + mListeners.add(watcher); + } + + @Override + public void removeTextChangedListener(TextWatcher watcher) { + if (mListeners != null) { + mListeners.remove(watcher); + + if (mListeners.isEmpty()) { + mListeners = null; + super.removeTextChangedListener(getTextWatcherDelegator()); + } + } + } + + private TextWatcherDelegator getTextWatcherDelegator() { + if (mTextWatcherDelegator == null) { + mTextWatcherDelegator = new TextWatcherDelegator(); + } + return mTextWatcherDelegator; + } + + public void setIsSettingTextFromJS(boolean mIsSettingTextFromJS) { + this.mIsSettingTextFromJS = mIsSettingTextFromJS; + } + + private boolean onEnter(Spannable text, boolean firedAfterTextChanged, int selStart, int selEnd) { + disableTextChangedListener(); + String content = toHtml(text, false); + int cursorPositionStart = firedAfterTextChanged ? selStart : getSelectionStart(); + int cursorPositionEnd = firedAfterTextChanged ? selEnd : getSelectionEnd(); + enableTextChangedListener(); + ReactContext reactContext = (ReactContext) getContext(); + EventDispatcher eventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); + eventDispatcher.dispatchEvent( + new ReactAztecEnterEvent(getId(), content, cursorPositionStart, cursorPositionEnd, + firedAfterTextChanged, incrementAndGetEventCounter()) + ); + return true; + } + + private boolean onBackspace() { + int cursorPositionStart = getSelectionStart(); + int cursorPositionEnd = getSelectionEnd(); + // Make sure to report backspace at the beginning only, with no selection. + if (cursorPositionStart != 0 || cursorPositionEnd != 0) { + return false; + } + + disableTextChangedListener(); + String content = toHtml(getText(), false); + enableTextChangedListener(); + ReactContext reactContext = (ReactContext) getContext(); + EventDispatcher eventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); + // TODO: isRTL? Should be passed here? + eventDispatcher.dispatchEvent( + new ReactAztecBackspaceEvent(getId(), content, cursorPositionStart, cursorPositionEnd) + ); + return true; + } + + /** + * Handle paste action by retrieving clipboard contents and dispatching a + * {@link ReactAztecPasteEvent} with the data + * + * @param isPastedAsPlainText boolean indicating whether the paste action chosen was + * "PASTE AS PLAIN TEXT" + * + * @return boolean to indicate that the action was handled (always true) + */ + private boolean onPaste(boolean isPastedAsPlainText) { + ClipboardManager clipboardManager = (ClipboardManager) getContext().getSystemService( + Context.CLIPBOARD_SERVICE); + + StringBuilder text = new StringBuilder(); + StringBuilder html = new StringBuilder(); + + if (clipboardManager != null && clipboardManager.hasPrimaryClip()) { + ClipData clipData = clipboardManager.getPrimaryClip(); + int itemCount = clipData.getItemCount(); + + for (int i = 0; i < itemCount; i++) { + Item item = clipData.getItemAt(i); + text.append(item.coerceToText(getContext())); + if (!isPastedAsPlainText) { + html.append(item.coerceToHtmlText(getContext())); + } + } + } + + // temporarily disable listener during call to toHtml() + disableTextChangedListener(); + String content = toHtml(getText(), false); + int cursorPositionStart = getSelectionStart(); + int cursorPositionEnd = getSelectionEnd(); + enableTextChangedListener(); + ReactContext reactContext = (ReactContext) getContext(); + EventDispatcher eventDispatcher = reactContext.getNativeModule(UIManagerModule.class) + .getEventDispatcher(); + eventDispatcher.dispatchEvent(new ReactAztecPasteEvent(getId(), content, + cursorPositionStart, cursorPositionEnd, text.toString(), html.toString()) + ); + return true; + } + + public void setActiveFormats(Iterable newFormats) { + Set selectedStylesSet = new HashSet<>(getSelectedStyles()); + Set newFormatsSet = new HashSet<>(); + for (String newFormat : newFormats) { + switch (newFormat) { + case "bold": + newFormatsSet.add(AztecTextFormat.FORMAT_STRONG); + break; + case "italic": + newFormatsSet.add(AztecTextFormat.FORMAT_EMPHASIS); + break; + case "strikethrough": + newFormatsSet.add(AztecTextFormat.FORMAT_STRIKETHROUGH); + break; + case "underline": + newFormatsSet.add(AztecTextFormat.FORMAT_UNDERLINE); + break; + } + } + selectedStylesSet.removeAll(typingFormatsMap.keySet()); + selectedStylesSet.addAll(newFormatsSet); + ArrayList newStylesList = new ArrayList<>(selectedStylesSet); + setSelectedStyles(newStylesList); + updateToolbarButtons(newStylesList); + } + + protected boolean isEnterPressedUnderway() { + return EnterPressedWatcher.Companion.isEnterPressedUnderway(getText()); + } + + /** + * This class will redirect *TextChanged calls to the listeners only in the case where the text + * is changed by the user, and not explicitly set by JS. + */ + private class TextWatcherDelegator implements TextWatcher { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + if (!mIsSettingTextFromJS && mListeners != null) { + for (TextWatcher listener : mListeners) { + listener.beforeTextChanged(s, start, count, after); + } + } + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (!mIsSettingTextFromJS && mListeners != null) { + for (TextWatcher listener : mListeners) { + listener.onTextChanged(s, start, before, count); + } + } + + onContentSizeChange(); + } + + @Override + public void afterTextChanged(Editable s) { + if (!mIsSettingTextFromJS && mListeners != null) { + for (TextWatcher listener : mListeners) { + listener.afterTextChanged(s); + } + } + } + } +} diff --git a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecTextFormatEnum.java b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecTextFormatEnum.java new file mode 100644 index 00000000000000..6856082d1d850e --- /dev/null +++ b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecTextFormatEnum.java @@ -0,0 +1,41 @@ +package org.wordpress.mobile.ReactNativeAztec; + +import org.wordpress.aztec.AztecTextFormat; + +import java.util.HashMap; +import java.util.Map; + +public enum ReactAztecTextFormatEnum { + + P("p", AztecTextFormat.FORMAT_PARAGRAPH), + H1("h1", AztecTextFormat.FORMAT_HEADING_1), + H2("h2", AztecTextFormat.FORMAT_HEADING_2), + H3("h3", AztecTextFormat.FORMAT_HEADING_3), + H4("h4", AztecTextFormat.FORMAT_HEADING_4), + H5("h5", AztecTextFormat.FORMAT_HEADING_5), + H6("h6", AztecTextFormat.FORMAT_HEADING_6); + + private final String mTag; + private final AztecTextFormat mAztecTextFormat; + + private static final Map lookup = new HashMap<>(); + + static { + for (ReactAztecTextFormatEnum value : ReactAztecTextFormatEnum.values()) { + lookup.put(value.mTag, value); + } + } + + ReactAztecTextFormatEnum(String tag, AztecTextFormat aztecTextFormat) { + mTag = tag; + mAztecTextFormat = aztecTextFormat; + } + + public AztecTextFormat getAztecTextFormat() { + return mAztecTextFormat; + } + + static ReactAztecTextFormatEnum get(String tag) { + return lookup.get(tag); + } +} diff --git a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecTextShadowNode.java b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecTextShadowNode.java new file mode 100644 index 00000000000000..5be81e2d5365ed --- /dev/null +++ b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecTextShadowNode.java @@ -0,0 +1,24 @@ +package org.wordpress.mobile.ReactNativeAztec; + +import androidx.annotation.Nullable; + +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.uimanager.annotations.ReactProp; +import com.facebook.react.views.textinput.ReactTextInputShadowNode; + +public class ReactAztecTextShadowNode extends ReactTextInputShadowNode { + private @Nullable ReadableMap mTextMap = null; + private @Nullable Integer mColor = null; + + @ReactProp(name = PROP_TEXT) + public void setText(@Nullable ReadableMap inputMap) { + mTextMap = inputMap; + markUpdated(); + } + + @ReactProp(name = "color", customType = "Color") + public void setColor(@Nullable Integer color) { + mColor = color; + markUpdated(); + } +} diff --git a/packages/react-native-aztec/android/src/main/kotlin/org/wordpress/mobile/ReactNativeAztec/EnterPressedWatcher.kt b/packages/react-native-aztec/android/src/main/kotlin/org/wordpress/mobile/ReactNativeAztec/EnterPressedWatcher.kt new file mode 100644 index 00000000000000..bd466a3a8c23e7 --- /dev/null +++ b/packages/react-native-aztec/android/src/main/kotlin/org/wordpress/mobile/ReactNativeAztec/EnterPressedWatcher.kt @@ -0,0 +1,71 @@ +package org.wordpress.mobile.ReactNativeAztec + +import android.text.Editable +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.TextWatcher +import org.wordpress.aztec.AztecText +import org.wordpress.aztec.Constants +import java.lang.ref.WeakReference + +// Class to be used as a span to temporarily denote that Enter was detected +class EnterPressedUnderway + +interface EnterDeleter { + fun shouldDeleteEnter(): Boolean +} + +class EnterPressedWatcher(aztecText: AztecText, var enterDeleter: EnterDeleter) : TextWatcher { + + private val aztecTextRef: WeakReference = WeakReference(aztecText) + private lateinit var textBefore : SpannableStringBuilder + private var start: Int = -1 + private var selStart: Int = 0 + private var selEnd: Int = 0 + var done = false + + override fun beforeTextChanged(text: CharSequence, start: Int, count: Int, after: Int) { + val aztecText = aztecTextRef.get() + if (aztecText?.getAztecKeyListener() != null && !aztecText.isTextChangedListenerDisabled()) { + // we need to make a copy to preserve the contents as they were before the change + textBefore = SpannableStringBuilder(text) + this.start = start + this.selStart = aztecText.selectionStart + this.selEnd = aztecText.selectionEnd + } + } + + override fun onTextChanged(text: CharSequence, start: Int, before: Int, count: Int) { + val aztecText = aztecTextRef.get() + val aztecKeyListener = aztecText?.getAztecKeyListener() + if (aztecText != null && !aztecText.isTextChangedListenerDisabled() && aztecKeyListener != null) { + val newTextCopy = SpannableStringBuilder(text) + // if new text length is longer than original text by 1 + if (textBefore?.length == newTextCopy.length - 1) { + // now check that the inserted character is actually a NEWLINE + if (newTextCopy[this.start] == Constants.NEWLINE) { + done = false + aztecText.editableText.setSpan(EnterPressedUnderway(), 0, 0, Spanned.SPAN_USER) + aztecKeyListener.onEnterKey(newTextCopy.replace(this.start, this.start+1, ""), true, selStart, selEnd) + } + } + } + } + + override fun afterTextChanged(text: Editable) { + aztecTextRef.get()?.editableText?.getSpans(0, 0, EnterPressedUnderway::class.java)?.forEach { + if (!done) { + done = true + if (enterDeleter.shouldDeleteEnter()) + text.replace(start, start + 1, "") + } + aztecTextRef.get()?.editableText?.removeSpan(it) + } + } + + companion object { + fun isEnterPressedUnderway(spanned: Spanned?): Boolean { + return spanned?.getSpans(0, 0, EnterPressedUnderway::class.java)?.isNotEmpty() ?: false + } + } +} diff --git a/packages/react-native-aztec/android/src/main/res/drawable-hdpi/ic_launcher.png b/packages/react-native-aztec/android/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 00000000000000..bcb72b12f55111 Binary files /dev/null and b/packages/react-native-aztec/android/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/packages/react-native-aztec/android/src/main/res/drawable-mdpi/ic_launcher.png b/packages/react-native-aztec/android/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 00000000000000..37e5bce60f0951 Binary files /dev/null and b/packages/react-native-aztec/android/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/packages/react-native-aztec/android/src/main/res/drawable-xhdpi/ic_launcher.png b/packages/react-native-aztec/android/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 00000000000000..1c4a85a0204f3e Binary files /dev/null and b/packages/react-native-aztec/android/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/packages/react-native-aztec/android/src/main/res/drawable-xxhdpi/ic_launcher.png b/packages/react-native-aztec/android/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 00000000000000..b26545c36b837e Binary files /dev/null and b/packages/react-native-aztec/android/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/packages/react-native-aztec/android/src/main/res/values/bools.xml b/packages/react-native-aztec/android/src/main/res/values/bools.xml new file mode 100644 index 00000000000000..2df1159d1838ab --- /dev/null +++ b/packages/react-native-aztec/android/src/main/res/values/bools.xml @@ -0,0 +1,7 @@ + + + + + false + + diff --git a/packages/react-native-aztec/android/src/main/res/values/integers.xml b/packages/react-native-aztec/android/src/main/res/values/integers.xml new file mode 100644 index 00000000000000..59c2c0c389fbb8 --- /dev/null +++ b/packages/react-native-aztec/android/src/main/res/values/integers.xml @@ -0,0 +1,7 @@ + + + + + 0 + + diff --git a/packages/react-native-aztec/android/src/main/res/values/strings.xml b/packages/react-native-aztec/android/src/main/res/values/strings.xml new file mode 100644 index 00000000000000..5b3c79dd838556 --- /dev/null +++ b/packages/react-native-aztec/android/src/main/res/values/strings.xml @@ -0,0 +1,20 @@ + + + + + Aztec React Native example + \ No newline at end of file diff --git a/packages/react-native-aztec/android/src/main/res/values/template-dimens.xml b/packages/react-native-aztec/android/src/main/res/values/template-dimens.xml new file mode 100644 index 00000000000000..bedb28fb4a736d --- /dev/null +++ b/packages/react-native-aztec/android/src/main/res/values/template-dimens.xml @@ -0,0 +1,25 @@ + + + + + + + 8dp + 16dp + 32dp + + diff --git a/packages/react-native-aztec/example/.babelrc b/packages/react-native-aztec/example/.babelrc new file mode 100644 index 00000000000000..d4b74b5be7b43d --- /dev/null +++ b/packages/react-native-aztec/example/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["module:metro-react-native-babel-preset"] +} diff --git a/packages/react-native-aztec/example/App.js b/packages/react-native-aztec/example/App.js new file mode 100644 index 00000000000000..fdb6b468f875fa --- /dev/null +++ b/packages/react-native-aztec/example/App.js @@ -0,0 +1,83 @@ +import React from 'react'; +import {AppRegistry, StyleSheet, TextInput, FlatList, KeyboardAvoidingView, SafeAreaView, Platform} from 'react-native'; +import {example_content} from './content'; +import Editor from './editor' + +const _minHeight = 100; + +const sampleContent = example_content(); + +const elements = [ + {key: '1', text: sampleContent, height: _minHeight}, + {key: '2', text: sampleContent, height: _minHeight}, + {key: '3', text: sampleContent, height: _minHeight}, + {key: '4', text: sampleContent, height: _minHeight}, + {key: '5', text: sampleContent, height: _minHeight}, + {key: '6', text: sampleContent, height: _minHeight}, + ] + +export default class example extends React.Component { + constructor(props) { + super(props); + this.renderItem = this.renderItem.bind(this) + this.renderItemAsTextInput = this.renderItemAsTextInput.bind(this) + this.state = {isShowingText: true, data: elements}; + } + + renderItem( { item } ) { + const key = item.key; + return ( + { + let newHeight = contentSize.height; + const newElements = this.state.data.map( searchItem => { + if (searchItem.key == key) { + return {...searchItem, height: newHeight}; + } else { + return searchItem; + } + }) + this.setState( { data: newElements}) + }} + /> + ) + } + + renderItemAsTextInput( { item } ) { + return ( + ) + } + + render() { + const data = this.state.data; + const mainContent = ( + + + + ); + if (Platform.OS === "ios") { + return ({mainContent}) + } else { + return mainContent + } + } +} + +var styles = StyleSheet.create({ + container: { + flex: 1 + }, + aztec_editor: { + minHeight: _minHeight, + margin: 10, + }, +}); + +AppRegistry.registerComponent('example', () => example); diff --git a/packages/react-native-aztec/example/android/app/build.gradle b/packages/react-native-aztec/example/android/app/build.gradle new file mode 100644 index 00000000000000..181aabb1d30085 --- /dev/null +++ b/packages/react-native-aztec/example/android/app/build.gradle @@ -0,0 +1,159 @@ +apply plugin: 'com.android.application' + +/** + * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets + * and bundleReleaseJsAndAssets). + * These basically call `react-native bundle` with the correct arguments during the Android build + * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the + * bundle directly from the development server. Below you can see all the possible configurations + * and their defaults. If you decide to add a configuration block, make sure to add it before the + * `apply from: "../../node_modules/react-native/react.gradle"` line. + * + * project.ext.react = [ + * // the name of the generated asset file containing your JS bundle + * bundleAssetName: "index.android.bundle", + * + * // the entry file for bundle generation + * entryFile: "index.android.js", + * + * // whether to bundle JS and assets in debug mode + * bundleInDebug: false, + * + * // whether to bundle JS and assets in release mode + * bundleInRelease: true, + * + * // whether to bundle JS and assets in another build variant (if configured). + * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants + * // The configuration property can be in the following formats + * // 'bundleIn${productFlavor}${buildType}' + * // 'bundleIn${buildType}' + * // bundleInFreeDebug: true, + * // bundleInPaidRelease: true, + * // bundleInBeta: true, + * + * // whether to disable dev mode in custom build variants (by default only disabled in release) + * // for example: to disable dev mode in the staging build type (if configured) + * devDisabledInStaging: true, + * // The configuration property can be in the following formats + * // 'devDisabledIn${productFlavor}${buildType}' + * // 'devDisabledIn${buildType}' + * + * // the root of your project, i.e. where "package.json" lives + * root: "../../", + * + * // where to put the JS bundle asset in debug mode + * jsBundleDirDebug: "$buildDir/intermediates/assets/debug", + * + * // where to put the JS bundle asset in release mode + * jsBundleDirRelease: "$buildDir/intermediates/assets/release", + * + * // where to put drawable resources / React Native assets, e.g. the ones you use via + * // require('./image.png')), in debug mode + * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug", + * + * // where to put drawable resources / React Native assets, e.g. the ones you use via + * // require('./image.png')), in release mode + * resourcesDirRelease: "$buildDir/intermediates/res/merged/release", + * + * // by default the gradle tasks are skipped if none of the JS files or assets change; this means + * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to + * // date; if you have any other folders that you want to ignore for performance reasons (gradle + * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/ + * // for example, you might want to remove it from here. + * inputExcludes: ["android/**", "ios/**"], + * + * // override which node gets called and with what additional arguments + * nodeExecutableAndArgs: ["node"], + * + * // supply additional arguments to the packager + * extraPackagerArgs: [] + * ] + */ + +project.ext.react = [ + entryFile: "index.js" +] + +apply from: "../../node_modules/react-native/react.gradle" + +// The sample build uses multiple directories to +// keep boilerplate and common code separate from +// the main sample code. +List dirs = [ + 'main', // main sample code; look here for the interesting stuff. + 'common', // components that are reused by multiple samples + 'template'] // boilerplate code that is generated by the sample template process + +android { + compileSdkVersion 27 + + buildToolsVersion "27.0.3" + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 26 + + ndk { + abiFilters "armeabi-v7a", "x86" + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + } + + sourceSets { + main { + dirs.each { dir -> + java.srcDirs "src/${dir}/java" + res.srcDirs "src/${dir}/res" + } + } + + androidTest.setRoot('tests') + androidTest.java.srcDirs = ['tests/src'] + } + + lintOptions { + disable 'GradleCompatible' + } +} + +buildscript { + repositories { + jcenter() + google() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.3.1' + } +} + +repositories { + jcenter() + google() + maven { url "https://jitpack.io" } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" + + implementation "com.android.support:appcompat-v7:$supportLibVersion" + implementation "org.wordpress:utils:$wordpressUtilsVersion" + + implementation project(':react-native-aztec') + + implementation "com.android.support:support-v4:$supportLibVersion" + implementation "com.android.support:gridlayout-v7:$supportLibVersion" + implementation "com.android.support:cardview-v7:$supportLibVersion" + implementation "com.android.support:appcompat-v7:$supportLibVersion" + implementation "com.android.support:recyclerview-v7:$supportLibVersion" + + implementation "com.facebook.react:react-native:+" // From node_modules. +} + + + + diff --git a/packages/react-native-aztec/example/android/app/src/main/AndroidManifest.xml b/packages/react-native-aztec/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000000000..0a320c48727b18 --- /dev/null +++ b/packages/react-native-aztec/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/react-native-aztec/example/android/app/src/main/java/com/example/android/MainActivity.java b/packages/react-native-aztec/example/android/app/src/main/java/com/example/android/MainActivity.java new file mode 100644 index 00000000000000..20f7dcac324d69 --- /dev/null +++ b/packages/react-native-aztec/example/android/app/src/main/java/com/example/android/MainActivity.java @@ -0,0 +1,59 @@ +/* +* Copyright 2013 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + +package com.example.android; + +import android.os.Bundle; +import android.support.v4.app.FragmentTransaction; +import android.view.KeyEvent; + +import com.example.android.common.activities.SampleRNBaseActivity; + +/** + * A simple launcher activity containing a summary sample description, sample log and a custom + * {@link android.support.v4.app.Fragment} which can display a view. + *

+ * For devices with displays with a width of 720dp or greater, the sample log is always visible, + * on other devices it's visibility is controlled by an item on the Action Bar. + */ +public class MainActivity extends SampleRNBaseActivity { + + public static final String TAG = "MainActivity"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + if (savedInstanceState == null) { + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + MyFragment fragment = new MyFragment(); + transaction.replace(R.id.sample_content_fragment, fragment); + transaction.commit(); + } + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_MENU && mReactInstanceManager != null) { + mReactInstanceManager.showDevOptionsDialog(); + return true; + } + return super.onKeyUp(keyCode, event); + } + +} diff --git a/packages/react-native-aztec/example/android/app/src/main/java/com/example/android/MyFragment.java b/packages/react-native-aztec/example/android/app/src/main/java/com/example/android/MyFragment.java new file mode 100644 index 00000000000000..ae4986adc122b7 --- /dev/null +++ b/packages/react-native-aztec/example/android/app/src/main/java/com/example/android/MyFragment.java @@ -0,0 +1,55 @@ +package com.example.android; + +import android.app.Activity; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.example.android.common.activities.SampleRNBaseActivity; +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.ReactRootView; + +public class MyFragment extends Fragment { + + private static final String TAG = "MyFragment"; + + private ReactInstanceManager mReactInstanceManager; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + try { + mReactInstanceManager = ((SampleRNBaseActivity) activity).getReactInstanceManager(); + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + " must extends SampleRNBaseActivity"); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + if (mReactInstanceManager == null) { + try { + mReactInstanceManager = ((SampleRNBaseActivity) getActivity()).getReactInstanceManager(); + } catch (ClassCastException e) { + throw new ClassCastException(getActivity().toString() + " must extends SampleRNBaseActivity"); + } + } + + ReactRootView reactRootView = new ReactRootView(getContext()); + reactRootView.startReactApplication(mReactInstanceManager, "example", null); + return reactRootView; + } + + @Override + public void onSaveInstanceState(Bundle savedInstanceState) { + super.onSaveInstanceState(savedInstanceState); + } + +} diff --git a/packages/react-native-aztec/example/android/app/src/main/java/com/example/android/common/activities/SampleRNBaseActivity.java b/packages/react-native-aztec/example/android/app/src/main/java/com/example/android/common/activities/SampleRNBaseActivity.java new file mode 100644 index 00000000000000..76084a08a7ddf3 --- /dev/null +++ b/packages/react-native-aztec/example/android/app/src/main/java/com/example/android/common/activities/SampleRNBaseActivity.java @@ -0,0 +1,86 @@ +/* +* Copyright 2013 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package com.example.android.common.activities; + +import android.os.Bundle; +import android.support.v4.app.FragmentActivity; + +import org.wordpress.mobile.ReactNativeAztec.ReactAztecPackage; +import org.wordpress.mobile.ReactNativeAztec.BuildConfig; +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.common.LifecycleState; +import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; +import com.facebook.react.shell.MainReactPackage; + +/** + * Base launcher activity, to handle most of the common plumbing for samples. + */ +public class SampleRNBaseActivity extends FragmentActivity implements DefaultHardwareBackBtnHandler { + + public static final String TAG = "SampleRNBaseActivity"; + protected ReactInstanceManager mReactInstanceManager; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mReactInstanceManager = ReactInstanceManager.builder() + .setApplication(getApplication()) + .setBundleAssetName("index.android.bundle") + .setJSMainModulePath("index") + .addPackage(new MainReactPackage()) + .addPackage(new ReactAztecPackage()) + .setUseDeveloperSupport(BuildConfig.DEBUG) + .setInitialLifecycleState(LifecycleState.RESUMED) + .build(); + } + + public ReactInstanceManager getReactInstanceManager() { + return mReactInstanceManager; + } + + @Override + protected void onPause() { + super.onPause(); + + if (mReactInstanceManager != null) { + mReactInstanceManager.onHostPause(this); + } + } + + @Override + protected void onResume() { + super.onResume(); + + if (mReactInstanceManager != null) { + mReactInstanceManager.onHostResume(this, this); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + if (mReactInstanceManager != null) { + mReactInstanceManager.onHostDestroy(this); + } + } + + @Override + public void invokeDefaultOnBackPressed() { + super.onBackPressed(); + } +} diff --git a/packages/react-native-aztec/example/android/app/src/main/res/drawable-hdpi/ic_launcher.png b/packages/react-native-aztec/example/android/app/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 00000000000000..bcb72b12f55111 Binary files /dev/null and b/packages/react-native-aztec/example/android/app/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/packages/react-native-aztec/example/android/app/src/main/res/drawable-hdpi/tile.9.png b/packages/react-native-aztec/example/android/app/src/main/res/drawable-hdpi/tile.9.png new file mode 100644 index 00000000000000..135862883e26ed Binary files /dev/null and b/packages/react-native-aztec/example/android/app/src/main/res/drawable-hdpi/tile.9.png differ diff --git a/packages/react-native-aztec/example/android/app/src/main/res/drawable-mdpi/ic_launcher.png b/packages/react-native-aztec/example/android/app/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 00000000000000..37e5bce60f0951 Binary files /dev/null and b/packages/react-native-aztec/example/android/app/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/packages/react-native-aztec/example/android/app/src/main/res/drawable-xhdpi/ic_launcher.png b/packages/react-native-aztec/example/android/app/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 00000000000000..1c4a85a0204f3e Binary files /dev/null and b/packages/react-native-aztec/example/android/app/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/packages/react-native-aztec/example/android/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/packages/react-native-aztec/example/android/app/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 00000000000000..b26545c36b837e Binary files /dev/null and b/packages/react-native-aztec/example/android/app/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/packages/react-native-aztec/example/android/app/src/main/res/drawable/ic_reorder_black_24dp.xml b/packages/react-native-aztec/example/android/app/src/main/res/drawable/ic_reorder_black_24dp.xml new file mode 100644 index 00000000000000..2b87cd8404bfb8 --- /dev/null +++ b/packages/react-native-aztec/example/android/app/src/main/res/drawable/ic_reorder_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/packages/react-native-aztec/example/android/app/src/main/res/layout-w720dp/activity_main.xml b/packages/react-native-aztec/example/android/app/src/main/res/layout-w720dp/activity_main.xml new file mode 100644 index 00000000000000..7e7ec64b17e862 --- /dev/null +++ b/packages/react-native-aztec/example/android/app/src/main/res/layout-w720dp/activity_main.xml @@ -0,0 +1,36 @@ + + + + + + + + + + diff --git a/packages/react-native-aztec/example/android/app/src/main/res/layout/activity_main.xml b/packages/react-native-aztec/example/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000000000..51434768bad50b --- /dev/null +++ b/packages/react-native-aztec/example/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,35 @@ + + + + + + + + + diff --git a/packages/react-native-aztec/example/android/app/src/main/res/menu/main.xml b/packages/react-native-aztec/example/android/app/src/main/res/menu/main.xml new file mode 100644 index 00000000000000..29c52f8ed465f6 --- /dev/null +++ b/packages/react-native-aztec/example/android/app/src/main/res/menu/main.xml @@ -0,0 +1,23 @@ + + +

+ + diff --git a/packages/react-native-aztec/example/android/app/src/main/res/values-sw600dp/template-dimens.xml b/packages/react-native-aztec/example/android/app/src/main/res/values-sw600dp/template-dimens.xml new file mode 100644 index 00000000000000..22074a2bdbaf60 --- /dev/null +++ b/packages/react-native-aztec/example/android/app/src/main/res/values-sw600dp/template-dimens.xml @@ -0,0 +1,24 @@ + + + + + + + @dimen/margin_huge + @dimen/margin_medium + + diff --git a/packages/react-native-aztec/example/android/app/src/main/res/values-sw600dp/template-styles.xml b/packages/react-native-aztec/example/android/app/src/main/res/values-sw600dp/template-styles.xml new file mode 100644 index 00000000000000..03d1974183dd64 --- /dev/null +++ b/packages/react-native-aztec/example/android/app/src/main/res/values-sw600dp/template-styles.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/packages/react-native-aztec/example/android/app/src/main/res/values/colors.xml b/packages/react-native-aztec/example/android/app/src/main/res/values/colors.xml new file mode 100644 index 00000000000000..c2832bdb7b1ec7 --- /dev/null +++ b/packages/react-native-aztec/example/android/app/src/main/res/values/colors.xml @@ -0,0 +1,23 @@ + + + + + + #00BCD4 + #00838F + + diff --git a/packages/react-native-aztec/example/android/app/src/main/res/values/dimens.xml b/packages/react-native-aztec/example/android/app/src/main/res/values/dimens.xml new file mode 100644 index 00000000000000..8c5b99af6bdbac --- /dev/null +++ b/packages/react-native-aztec/example/android/app/src/main/res/values/dimens.xml @@ -0,0 +1,20 @@ + + + + + 100dp + \ No newline at end of file diff --git a/packages/react-native-aztec/example/android/app/src/main/res/values/fragmentview_strings.xml b/packages/react-native-aztec/example/android/app/src/main/res/values/fragmentview_strings.xml new file mode 100644 index 00000000000000..7b9d9ec4f3c52b --- /dev/null +++ b/packages/react-native-aztec/example/android/app/src/main/res/values/fragmentview_strings.xml @@ -0,0 +1,19 @@ + + + Show Log + Hide Log + diff --git a/packages/react-native-aztec/example/android/app/src/main/res/values/strings.xml b/packages/react-native-aztec/example/android/app/src/main/res/values/strings.xml new file mode 100644 index 00000000000000..45031ada040fa9 --- /dev/null +++ b/packages/react-native-aztec/example/android/app/src/main/res/values/strings.xml @@ -0,0 +1,23 @@ + + + + + Aztec React Native example + Element + Grid Layout Manager + Linear Layout Manager + \ No newline at end of file diff --git a/packages/react-native-aztec/example/android/app/src/main/res/values/styles.xml b/packages/react-native-aztec/example/android/app/src/main/res/values/styles.xml new file mode 100755 index 00000000000000..5db0d48712eef0 --- /dev/null +++ b/packages/react-native-aztec/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/packages/react-native-aztec/example/android/app/src/main/res/values/template-dimens.xml b/packages/react-native-aztec/example/android/app/src/main/res/values/template-dimens.xml new file mode 100644 index 00000000000000..39e710b5ca358c --- /dev/null +++ b/packages/react-native-aztec/example/android/app/src/main/res/values/template-dimens.xml @@ -0,0 +1,32 @@ + + + + + + + 4dp + 8dp + 16dp + 32dp + 64dp + + + + @dimen/margin_medium + @dimen/margin_medium + + diff --git a/packages/react-native-aztec/example/android/app/src/main/res/values/template-styles.xml b/packages/react-native-aztec/example/android/app/src/main/res/values/template-styles.xml new file mode 100644 index 00000000000000..199025ef2359d8 --- /dev/null +++ b/packages/react-native-aztec/example/android/app/src/main/res/values/template-styles.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + diff --git a/packages/react-native-aztec/example/android/build.gradle b/packages/react-native-aztec/example/android/build.gradle new file mode 100644 index 00000000000000..5a92126f4064b7 --- /dev/null +++ b/packages/react-native-aztec/example/android/build.gradle @@ -0,0 +1,46 @@ +buildscript { + ext { + gradlePluginVersion = '3.0.1' + kotlinVersion = '1.2.61' + supportLibVersion = '27.1.1' + tagSoupVersion = '1.2.1' + glideVersion = '3.7.0' + picassoVersion = '2.5.2' + robolectricVersion = '3.5.1' + jUnitVersion = '4.12' + jSoupVersion = '1.10.3' + wordpressUtilsVersion = '1.22' + espressoVersion = '3.0.1' + } + + repositories { + jcenter() + google() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.3.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" + } + + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.3.1' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + mavenLocal() + jcenter() + maven { + // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm + url "$rootDir/../node_modules/react-native/android" + } + } +} diff --git a/packages/react-native-aztec/example/android/gradle/wrapper/gradle-wrapper.jar b/packages/react-native-aztec/example/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000000000..8c0fb64a8698b0 Binary files /dev/null and b/packages/react-native-aztec/example/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/packages/react-native-aztec/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/react-native-aztec/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000000000..56fe372f412c1e --- /dev/null +++ b/packages/react-native-aztec/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Sep 08 13:53:18 PDT 2014 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip \ No newline at end of file diff --git a/packages/react-native-aztec/example/android/gradlew b/packages/react-native-aztec/example/android/gradlew new file mode 100755 index 00000000000000..91a7e269e19dfc --- /dev/null +++ b/packages/react-native-aztec/example/android/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/packages/react-native-aztec/example/android/gradlew.bat b/packages/react-native-aztec/example/android/gradlew.bat new file mode 100644 index 00000000000000..8a0b282aa6885f --- /dev/null +++ b/packages/react-native-aztec/example/android/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/packages/react-native-aztec/example/android/settings.gradle b/packages/react-native-aztec/example/android/settings.gradle new file mode 100644 index 00000000000000..fd315d9847b1c8 --- /dev/null +++ b/packages/react-native-aztec/example/android/settings.gradle @@ -0,0 +1,7 @@ +rootProject.name = 'example' + +include ':react-native-aztec' +project(':react-native-aztec').projectDir = new File(rootProject.projectDir, '../../android') + +include ':app' + diff --git a/packages/react-native-aztec/example/app.json b/packages/react-native-aztec/example/app.json new file mode 100644 index 00000000000000..713b4fe00671a7 --- /dev/null +++ b/packages/react-native-aztec/example/app.json @@ -0,0 +1,4 @@ +{ + "name": "example", + "displayName": "example" + } \ No newline at end of file diff --git a/packages/react-native-aztec/example/content.js b/packages/react-native-aztec/example/content.js new file mode 100644 index 00000000000000..b52a9126728b2a --- /dev/null +++ b/packages/react-native-aztec/example/content.js @@ -0,0 +1,94 @@ + +HEADING = +"

Heading 1

" + + "

Heading 2

" + + "

Heading 3

" + + "

Heading 4

" + + "
Heading 5
" + + "
Heading 6
"; +BOLD = "Bold
"; +ITALIC = "Italic
"; +UNDERLINE = "Underline
"; +STRIKETHROUGH = "Strikethrough
" ;// or or +ORDERED = "
  1. Ordered
  2. should have color
"; +LINE = "
"; +UNORDERED = "
  • Unordered
  • Should not have color
"; +QUOTE = "
Quote
"; +LINK = "Link
"; +UNKNOWN = "
"; +COMMENT = "
"; +COMMENT_MORE = "
"; +COMMENT_PAGE = "
"; +HIDDEN = +"" + + "
" + + "
" + + "
" + + " Div
Span
Hidden" + + "
" + + "
" + + "
" + + "
" + + " " + + "
" + + "
"; +GUTENBERG_CODE_BLOCK = "\n" + +"
\"\"
\n" + +""; +PREFORMAT = +"
" +
+        "when (person) {
" + + " MOCTEZUMA -> {
" + + " print (\"friend\")
" + + " }
" + + " CORTES -> {
" + + " print (\"foe\")
" + + " }
" + + "}" + + "
"; +CODE = "if (Stringue == 5) printf(Stringue)
"; +IMG = "[caption align=\"alignright\"]Caption[/caption]"; +EMOJI = "👍"; +NON_LATIN_TEXT = "测试一个"; +LONG_TEXT = "

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. "; +VIDEO = "[video src=\"https://examplebloge.files.wordpress.com/2017/06/d7d88643-88e6-d9b5-11e6-92e03def4804.mp4\"]"; +AUDIO = "[audio src=\"https://upload.wikimedia.org/wikipedia/commons/9/94/H-Moll.ogg\"]"; +VIDEOPRESS = "[wpvideo OcobLTqC]"; +VIDEOPRESS_2 = "[wpvideo OcobLTqC w=640 h=400 autoplay=true html5only=true3]"; +QUOTE_RTL = "
לְצַטֵט
same quote but LTR
"; + +EXAMPLE_CONTENT = + IMG + + HEADING + + BOLD + + ITALIC + + UNDERLINE + + STRIKETHROUGH + + ORDERED + + LINE + + UNORDERED + + QUOTE + + PREFORMAT + + LINK + + HIDDEN + + COMMENT + + COMMENT_MORE + + COMMENT_PAGE + + CODE + + UNKNOWN + + EMOJI + + NON_LATIN_TEXT + + LONG_TEXT + + VIDEO + + VIDEOPRESS + + VIDEOPRESS_2 + + AUDIO + + GUTENBERG_CODE_BLOCK + + QUOTE_RTL; + +export function example_content() { + return EXAMPLE_CONTENT; +} + + + diff --git a/packages/react-native-aztec/example/editor.js b/packages/react-native-aztec/example/editor.js new file mode 100644 index 00000000000000..1155c654191f76 --- /dev/null +++ b/packages/react-native-aztec/example/editor.js @@ -0,0 +1,73 @@ +import React, { Component } from 'react'; +import {StyleSheet, Button, View} from 'react-native'; +import AztecView from 'react-native-aztec' + +const _minHeight = 100; + +export default class Editor extends Component { + constructor(props) { + super(props); + this.onFormatPress = this.onFormatPress.bind(this) + this.onActiveFormatsChange = this.onActiveFormatsChange.bind(this) + this.isFormatActive = this.isFormatActive.bind(this) + this.state = { activeFormats: [] }; + } + + onFormatPress( format ) { + const { _aztec } = this.refs; + _aztec.applyFormat(format); + } + + onActiveFormatsChange( formats ) { + this.setState({activeFormats: formats }); + } + + isFormatActive( format ) { + const { activeFormats } = this.state; + console.log(activeFormats); + return activeFormats.indexOf(format) != -1; + } + + render() { + const { item, onContentSizeChange } = this.props; + let myMinHeight = Math.max(_minHeight, item.height); + return ( + + +