├── jitpack.yml ├── sample ├── src │ └── main │ │ ├── assets │ │ ├── editor_custom_css.css │ │ ├── example1.html │ │ └── example2.html │ │ ├── res │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ ├── values │ │ │ ├── dimens.xml │ │ │ ├── colors.xml │ │ │ ├── themes.xml │ │ │ ├── strings.xml │ │ │ └── style.xml │ │ ├── values-night │ │ │ ├── themes.xml │ │ │ └── colors.xml │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── values-v23 │ │ │ └── themes.xml │ │ ├── color │ │ │ └── background_editor_button.xml │ │ ├── xml │ │ │ ├── network_security_config.xml │ │ │ └── data_extraction_rules.xml │ │ ├── navigation │ │ │ └── nav_graph.xml │ │ ├── drawable │ │ │ ├── ic_redo.xml │ │ │ ├── ic_undo.xml │ │ │ ├── ic_subscript.xml │ │ │ ├── ic_superscript.xml │ │ │ ├── ic_justify_full.xml │ │ │ ├── ic_justify_center.xml │ │ │ ├── ic_justify_left.xml │ │ │ ├── ic_justify_right.xml │ │ │ ├── ic_indent.xml │ │ │ ├── ic_outdent.xml │ │ │ ├── ic_unordered_list.xml │ │ │ ├── ic_launcher_foreground.xml │ │ │ ├── ic_ordered_list.xml │ │ │ └── ic_launcher_background.xml │ │ └── layout │ │ │ ├── activity_main.xml │ │ │ ├── create_link_text_input.xml │ │ │ └── fragment_editor_sample.xml │ │ ├── java │ │ └── com │ │ │ └── infomaniak │ │ │ └── lib │ │ │ └── richhtmleditor │ │ │ └── sample │ │ │ ├── EditorSampleViewModel.kt │ │ │ ├── MainActivity.kt │ │ │ └── EditorSampleFragment.kt │ │ └── AndroidManifest.xml ├── proguard-rules.pro └── build.gradle.kts ├── rich-html-editor ├── src │ └── main │ │ ├── assets │ │ ├── focus_request.js │ │ ├── attach_listeners.js │ │ ├── editor_template.html │ │ ├── link_detection.js │ │ ├── manage_links.js │ │ └── define_listeners.js │ │ └── java │ │ └── com │ │ └── infomaniak │ │ └── lib │ │ └── richhtmleditor │ │ ├── JsColor.kt │ │ ├── Justification.kt │ │ ├── DocumentInitializer.kt │ │ ├── executor │ │ ├── JsExecutor.kt │ │ ├── JsLifecycleAwareExecutor.kt │ │ ├── SpellCheckHtmlSetter.kt │ │ ├── HtmlSetter.kt │ │ ├── ScriptCssInjector.kt │ │ ├── JsExecutableMethod.kt │ │ ├── StateSubscriber.kt │ │ └── KeyboardOpener.kt │ │ ├── RichHtmlEditorWebViewClient.kt │ │ ├── Utils.kt │ │ ├── StatusCommand.kt │ │ ├── Extensions.kt │ │ ├── EditorStatuses.kt │ │ ├── EditorReloader.kt │ │ ├── JsBridge.kt │ │ └── RichHtmlEditorWebView.kt ├── proguard-rules.pro └── build.gradle.kts ├── assets ├── android-sample.png └── infomaniak-mail.webp ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── .idea └── copyright │ ├── profiles_settings.xml │ └── Editor_copyright.xml ├── .github └── workflows │ ├── auto-author-assign.yml │ ├── dependent-issues.yml │ ├── android.yml │ └── rebase-default-branch.yml ├── .gitignore ├── settings.gradle.kts ├── gradle.properties ├── gradlew.bat ├── README.md ├── gradlew └── LICENSE /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk17 3 | -------------------------------------------------------------------------------- /sample/src/main/assets/editor_custom_css.css: -------------------------------------------------------------------------------- 1 | html { 2 | background: #888888; 3 | } 4 | -------------------------------------------------------------------------------- /rich-html-editor/src/main/assets/focus_request.js: -------------------------------------------------------------------------------- 1 | function requestFocus() { 2 | getEditor().focus() 3 | } 4 | -------------------------------------------------------------------------------- /assets/android-sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infomaniak/android-rich-html-editor/HEAD/assets/android-sample.png -------------------------------------------------------------------------------- /assets/infomaniak-mail.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infomaniak/android-rich-html-editor/HEAD/assets/infomaniak-mail.webp -------------------------------------------------------------------------------- /sample/src/main/assets/example1.html: -------------------------------------------------------------------------------- 1 |

Hello World

2 | Yo 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infomaniak/android-rich-html-editor/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /sample/src/main/assets/example2.html: -------------------------------------------------------------------------------- 1 |

Hello

2 | test placeholder 3 |

Goodbye

4 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infomaniak/android-rich-html-editor/HEAD/sample/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infomaniak/android-rich-html-editor/HEAD/sample/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infomaniak/android-rich-html-editor/HEAD/sample/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infomaniak/android-rich-html-editor/HEAD/sample/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infomaniak/android-rich-html-editor/HEAD/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infomaniak/android-rich-html-editor/HEAD/sample/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infomaniak/android-rich-html-editor/HEAD/sample/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infomaniak/android-rich-html-editor/HEAD/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infomaniak/android-rich-html-editor/HEAD/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infomaniak/android-rich-html-editor/HEAD/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Mar 06 15:28:07 CET 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /.github/workflows/auto-author-assign.yml: -------------------------------------------------------------------------------- 1 | name: Auto Author Assign 2 | 3 | on: 4 | pull_request_target: 5 | types: [ opened, reopened ] 6 | 7 | permissions: 8 | pull-requests: write 9 | 10 | jobs: 11 | assign-author: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: toshimaru/auto-author-assign@v2.1.0 15 | -------------------------------------------------------------------------------- /rich-html-editor/src/main/assets/attach_listeners.js: -------------------------------------------------------------------------------- 1 | onBodyResize(() => { 2 | updateWebViewHeightWithBodyHeight() 3 | focusCursorOnScreen() 4 | }) 5 | 6 | document.addEventListener("selectionchange", () => { 7 | reportSelectionStateChangedIfNecessary() 8 | focusCursorOnScreen() 9 | }) 10 | 11 | reportEmptyBodyStatus() 12 | onEditorChildListChange(() => { 13 | reportEmptyBodyStatus() 14 | }) 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle files 2 | .gradle/ 3 | build/ 4 | 5 | # Local configuration file (sdk path, etc) 6 | local.properties 7 | 8 | # Log/OS Files 9 | *.log 10 | 11 | # Android Studio generated files and folders 12 | captures/ 13 | .externalNativeBuild/ 14 | .cxx/ 15 | *.apk 16 | *.aab 17 | output.json 18 | 19 | # IntelliJ 20 | *.iml 21 | .idea/ 22 | misc.xml 23 | deploymentTargetDropDown.xml 24 | render.experimental.xml 25 | 26 | # Keystore files 27 | *.jks 28 | *.keystore 29 | 30 | # Google Services (e.g. APIs or Firebase) 31 | google-services.json 32 | 33 | # Android Profiling 34 | *.hprof 35 | 36 | # Missing gitignore compared to mail 37 | .DS_Store 38 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google { 4 | content { 5 | includeGroupByRegex("com\\.android.*") 6 | includeGroupByRegex("com\\.google.*") 7 | includeGroupByRegex("androidx.*") 8 | } 9 | } 10 | mavenCentral() 11 | gradlePluginPortal() 12 | } 13 | } 14 | dependencyResolutionManagement { 15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 16 | repositories { 17 | google() 18 | mavenCentral() 19 | } 20 | } 21 | 22 | rootProject.name = "editor" 23 | include(":sample") 24 | include(":rich-html-editor") 25 | -------------------------------------------------------------------------------- /.github/workflows/dependent-issues.yml: -------------------------------------------------------------------------------- 1 | name: Dependent Issues 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | - edited 8 | - closed 9 | - reopened 10 | pull_request_target: 11 | types: 12 | - opened 13 | - edited 14 | - closed 15 | - reopened 16 | # Makes sure we always add status check for PRs. Useful only if 17 | # this action is required to pass before merging. Otherwise, it 18 | # can be removed. 19 | - synchronize 20 | 21 | jobs: 22 | check: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: z0al/dependent-issues@v1.5.2 26 | env: 27 | # (Required) The token to use to make API calls to GitHub. 28 | GITHUB_TOKEN: ${{ github.token }} 29 | -------------------------------------------------------------------------------- /sample/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /rich-html-editor/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /sample/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 250dp 19 | 20 | -------------------------------------------------------------------------------- /rich-html-editor/src/main/java/com/infomaniak/lib/richhtmleditor/JsColor.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Infomaniak Rich HTML Editor - Android 3 | * Copyright (C) 2024 Infomaniak Network SA 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.infomaniak.lib.richhtmleditor 18 | 19 | import androidx.annotation.ColorInt 20 | 21 | data class JsColor(@ColorInt val color: Int) 22 | -------------------------------------------------------------------------------- /sample/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 24 | 25 | -------------------------------------------------------------------------------- /sample/src/main/res/color/background_editor_button.xml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.3.2" 3 | kotlin = "2.0.0" 4 | coreKtx = "1.13.1" 5 | appcompat = "1.7.0" 6 | material = "1.12.0" 7 | constraintlayout = "2.1.4" 8 | navigationFragmentKtx = "2.7.7" 9 | navigationUiKtx = "2.7.7" 10 | 11 | [libraries] 12 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } 13 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } 14 | material = { group = "com.google.android.material", name = "material", version.ref = "material" } 15 | androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } 16 | androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigationFragmentKtx" } 17 | androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigationUiKtx" } 18 | 19 | [plugins] 20 | android-application = { id = "com.android.application", version.ref = "agp" } 21 | android-library = { id = "com.android.library", version.ref = "agp" } 22 | jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 23 | -------------------------------------------------------------------------------- /rich-html-editor/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.library) 3 | alias(libs.plugins.jetbrains.kotlin.android) 4 | id("maven-publish") 5 | } 6 | 7 | val sharedMinSdk: Int by rootProject.extra 8 | val sharedCompileSdk: Int by rootProject.extra 9 | val javaVersion: JavaVersion by rootProject.extra 10 | 11 | android { 12 | namespace = "com.infomaniak.lib.richhtmleditor" 13 | compileSdk = sharedCompileSdk 14 | 15 | defaultConfig { 16 | minSdk = sharedMinSdk 17 | 18 | consumerProguardFiles("consumer-rules.pro") 19 | } 20 | 21 | compileOptions { 22 | sourceCompatibility = javaVersion 23 | targetCompatibility = javaVersion 24 | } 25 | kotlinOptions { 26 | jvmTarget = javaVersion.toString() 27 | } 28 | } 29 | 30 | dependencies { 31 | implementation(libs.androidx.core.ktx) 32 | } 33 | 34 | afterEvaluate { 35 | publishing { 36 | publications { 37 | create("release") { 38 | from(components.findByName("release")) 39 | groupId = "com.github" 40 | artifactId = "android-rich-html-editor" 41 | version = "0.1.0" 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /sample/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 23 | 24 | 23 | 24 | 30 | 31 | -------------------------------------------------------------------------------- /rich-html-editor/src/main/java/com/infomaniak/lib/richhtmleditor/executor/JsExecutor.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Infomaniak Rich HTML Editor - Android 3 | * Copyright (C) 2024 Infomaniak Network SA 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.infomaniak.lib.richhtmleditor.executor 18 | 19 | import android.webkit.WebView 20 | 21 | internal class JsExecutor(private val webView: WebView) : JsLifecycleAwareExecutor() { 22 | 23 | override fun executeImmediately(value: JsExecutableMethod) { 24 | value.executeOn(webView) 25 | } 26 | 27 | fun executeImmediatelyAndRefreshToolbar(method: JsExecutableMethod) { 28 | method.addCallback { JsExecutableMethod("reportSelectionStateChangedIfNecessary").executeOn(webView) } 29 | executeImmediately(method) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /sample/src/main/res/navigation/nav_graph.xml: -------------------------------------------------------------------------------- 1 | 17 | 22 | 23 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /sample/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 17 | 22 | 23 | 24 | 28 | 29 | 35 | 36 | -------------------------------------------------------------------------------- /rich-html-editor/src/main/java/com/infomaniak/lib/richhtmleditor/RichHtmlEditorWebViewClient.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Infomaniak Rich HTML Editor - Android 3 | * Copyright (C) 2024 Infomaniak Network SA 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.infomaniak.lib.richhtmleditor 18 | 19 | import android.webkit.WebView 20 | import android.webkit.WebViewClient 21 | 22 | 23 | /** 24 | * A custom [WebViewClient] used to notify the [RichHtmlEditorWebView] editor of when the template has finished loading. 25 | * [RichHtmlEditorWebView.notifyPageHasLoaded] needs to be called inside [WebViewClient.onPageFinished] so the editor can work 26 | * properly. 27 | */ 28 | class RichHtmlEditorWebViewClient(private val onPageLoaded: () -> Unit) : WebViewClient() { 29 | override fun onPageFinished(webView: WebView, url: String?) = onPageLoaded() 30 | } 31 | -------------------------------------------------------------------------------- /sample/src/main/java/com/infomaniak/lib/richhtmleditor/sample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Infomaniak Rich HTML Editor - Android 3 | * Copyright (C) 2024 Infomaniak Network SA 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.infomaniak.lib.richhtmleditor.sample 18 | 19 | import android.os.Bundle 20 | import android.webkit.WebView 21 | import androidx.appcompat.app.AppCompatActivity 22 | import com.infomaniak.lib.richhtmleditor.sample.databinding.ActivityMainBinding 23 | 24 | class MainActivity : AppCompatActivity() { 25 | 26 | private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) } 27 | 28 | override fun onCreate(savedInstanceState: Bundle?) { 29 | super.onCreate(savedInstanceState) 30 | setContentView(binding.root) 31 | 32 | WebView.setWebContentsDebuggingEnabled(true) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /rich-html-editor/src/main/assets/editor_template.html: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. For more details, visit 12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | -------------------------------------------------------------------------------- /rich-html-editor/src/main/java/com/infomaniak/lib/richhtmleditor/executor/JsLifecycleAwareExecutor.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Infomaniak Rich HTML Editor - Android 3 | * Copyright (C) 2024 Infomaniak Network SA 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.infomaniak.lib.richhtmleditor.executor 18 | 19 | internal abstract class JsLifecycleAwareExecutor { 20 | 21 | private var hasDomLoaded = false 22 | private val objectsWaitingForDom = mutableListOf() 23 | 24 | fun executeWhenDomIsLoaded(value: T): Unit = synchronized(lock = this) { 25 | if (hasDomLoaded) executeImmediately(value) else objectsWaitingForDom.add(value) 26 | } 27 | 28 | fun notifyDomLoaded() = synchronized(lock = this) { 29 | hasDomLoaded = true 30 | objectsWaitingForDom.forEach(::executeImmediately) 31 | objectsWaitingForDom.clear() 32 | } 33 | 34 | abstract fun executeImmediately(value: T) 35 | } 36 | -------------------------------------------------------------------------------- /rich-html-editor/src/main/java/com/infomaniak/lib/richhtmleditor/executor/SpellCheckHtmlSetter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Infomaniak Rich HTML Editor - Android 3 | * Copyright (C) 2024 Infomaniak Network SA 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.infomaniak.lib.richhtmleditor.executor 18 | 19 | import android.webkit.WebView 20 | import com.infomaniak.lib.richhtmleditor.RichHtmlEditorWebView.Companion.EDITOR_ID 21 | import com.infomaniak.lib.richhtmleditor.encodeArgsForJs 22 | 23 | internal class SpellCheckHtmlSetter(private val webView: WebView) : JsLifecycleAwareExecutor() { 24 | 25 | override fun executeImmediately(value: Boolean) = webView.insertSpellCheckValue(value) 26 | 27 | private fun WebView.insertSpellCheckValue(enable: Boolean) { 28 | evaluateJavascript( 29 | """document.getElementById("$EDITOR_ID").setAttribute("spellcheck", ${encodeArgsForJs(enable)})""", 30 | null 31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /rich-html-editor/src/main/java/com/infomaniak/lib/richhtmleditor/executor/HtmlSetter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Infomaniak Rich HTML Editor - Android 3 | * Copyright (C) 2024 Infomaniak Network SA 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.infomaniak.lib.richhtmleditor.executor 18 | 19 | import android.webkit.WebView 20 | import com.infomaniak.lib.richhtmleditor.RichHtmlEditorWebView.Companion.EDITOR_ID 21 | import com.infomaniak.lib.richhtmleditor.looselyEscapeAsStringLiteralForJs 22 | 23 | internal class HtmlSetter(private val webView: WebView) : JsLifecycleAwareExecutor() { 24 | 25 | override fun executeImmediately(value: String) = webView.insertUserHtml(value) 26 | 27 | private fun WebView.insertUserHtml(html: String) { 28 | val escapedHtmlStringLiteral = looselyEscapeAsStringLiteralForJs(html) 29 | evaluateJavascript( 30 | """document.getElementById("$EDITOR_ID").innerHTML = $escapedHtmlStringLiteral""", 31 | null 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /sample/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.jetbrains.kotlin.android) 4 | } 5 | 6 | val sharedMinSdk: Int by rootProject.extra 7 | val sharedCompileSdk: Int by rootProject.extra 8 | val javaVersion: JavaVersion by rootProject.extra 9 | 10 | android { 11 | namespace = "com.infomaniak.lib.richhtmleditor.sample" 12 | compileSdk = sharedCompileSdk 13 | 14 | defaultConfig { 15 | applicationId = "com.infomaniak.lib.richhtmleditor.sample" 16 | minSdk = sharedMinSdk 17 | targetSdk = sharedCompileSdk 18 | versionCode = 1 19 | versionName = "1.0" 20 | } 21 | 22 | buildTypes { 23 | release { 24 | isMinifyEnabled = true 25 | isShrinkResources = true 26 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 27 | } 28 | } 29 | compileOptions { 30 | sourceCompatibility = javaVersion 31 | targetCompatibility = javaVersion 32 | } 33 | kotlinOptions { 34 | jvmTarget = javaVersion.toString() 35 | } 36 | buildFeatures { 37 | viewBinding = true 38 | } 39 | } 40 | 41 | dependencies { 42 | implementation(project(":rich-html-editor")) 43 | 44 | implementation(libs.androidx.core.ktx) 45 | implementation(libs.androidx.appcompat) 46 | implementation(libs.material) 47 | implementation(libs.androidx.constraintlayout) 48 | implementation(libs.androidx.navigation.fragment.ktx) 49 | implementation(libs.androidx.navigation.ui.ktx) 50 | } 51 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_redo.xml: -------------------------------------------------------------------------------- 1 | 17 | 24 | 25 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_undo.xml: -------------------------------------------------------------------------------- 1 | 17 | 24 | 25 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /rich-html-editor/src/main/java/com/infomaniak/lib/richhtmleditor/executor/ScriptCssInjector.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Infomaniak Rich HTML Editor - Android 3 | * Copyright (C) 2024 Infomaniak Network SA 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.infomaniak.lib.richhtmleditor.executor 18 | 19 | import android.webkit.WebView 20 | import com.infomaniak.lib.richhtmleditor.executor.ScriptCssInjector.CodeInjection 21 | import com.infomaniak.lib.richhtmleditor.executor.ScriptCssInjector.CodeInjection.InjectionType 22 | import com.infomaniak.lib.richhtmleditor.injectCss 23 | import com.infomaniak.lib.richhtmleditor.injectScript 24 | 25 | internal class ScriptCssInjector(private val webView: WebView) : JsLifecycleAwareExecutor() { 26 | 27 | override fun executeImmediately(value: CodeInjection): Unit = with(webView) { 28 | when (value.type) { 29 | InjectionType.SCRIPT -> injectScript(value.code) 30 | InjectionType.CSS -> injectCss(value.code) 31 | } 32 | } 33 | 34 | data class CodeInjection(val type: InjectionType, val code: String) { 35 | enum class InjectionType { SCRIPT, CSS } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /rich-html-editor/src/main/assets/link_detection.js: -------------------------------------------------------------------------------- 1 | const DOCUMENT_POSITION_SAME = 0 // TODO: Check usefulness 2 | 3 | function getAllLinksPartiallyContainedInsideSelection() { 4 | let elements = [...getEditor().querySelectorAll("a[href]")] 5 | const range = getSelectionRangeOrNull() 6 | if (range === null) return [] 7 | 8 | const { startContainer, endContainer } = range 9 | 10 | // TODO: Investigate RoosterJs issues with startContainer and endContainer https://github.com/microsoft/roosterjs/blob/b1d4bab67dcae342cfdc043a8cbe2b96bb823a44/packages/roosterjs-editor-dom/lib/utils/queryElements.ts#L30 11 | 12 | elements = elements.filter(element => 13 | doesIntersectWithNodeRange(element, startContainer, endContainer) 14 | ) 15 | 16 | return elements 17 | } 18 | 19 | function doesIntersectWithNodeRange(node, startNode, endNode) { 20 | const startPosition = node.compareDocumentPosition(startNode) 21 | const endPosition = node.compareDocumentPosition(endNode) 22 | const targetPositions = [DOCUMENT_POSITION_SAME, Node.DOCUMENT_POSITION_CONTAINS, Node.DOCUMENT_POSITION_CONTAINED_BY] 23 | 24 | return ( 25 | doesPositionMatchTargets(startPosition, targetPositions) || // intersectStart 26 | doesPositionMatchTargets(endPosition, targetPositions) || // intersectEnd 27 | (doesPositionMatchTargets(startPosition, [Node.DOCUMENT_POSITION_PRECEDING]) && // Contains 28 | doesPositionMatchTargets(endPosition, [Node.DOCUMENT_POSITION_FOLLOWING]) && 29 | !doesPositionMatchTargets(endPosition, [Node.DOCUMENT_POSITION_CONTAINED_BY])) 30 | ) 31 | } 32 | 33 | function doesPositionMatchTargets(position, targets) { 34 | return targets.some(target => 35 | target === DOCUMENT_POSITION_SAME ? position === DOCUMENT_POSITION_SAME : (position & target) === target 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 17 | 24 | 25 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_subscript.xml: -------------------------------------------------------------------------------- 1 | 17 | 23 | 24 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_superscript.xml: -------------------------------------------------------------------------------- 1 | 17 | 23 | 24 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /rich-html-editor/src/main/java/com/infomaniak/lib/richhtmleditor/executor/JsExecutableMethod.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Infomaniak Rich HTML Editor - Android 3 | * Copyright (C) 2024 Infomaniak Network SA 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.infomaniak.lib.richhtmleditor.executor 18 | 19 | import android.webkit.WebView 20 | import com.infomaniak.lib.richhtmleditor.encodeArgsForJs 21 | 22 | class JsExecutableMethod( 23 | private val methodName: String, 24 | private vararg val args: Any?, 25 | resultCallback: ((String) -> Unit)? = null, 26 | ) { 27 | private val callbacks: MutableList<(String) -> Unit> = resultCallback?.let { mutableListOf(it) } ?: mutableListOf() 28 | 29 | fun executeOn(webView: WebView) { 30 | val formattedArgs = args.joinToString(transform = ::encodeArgsForJs) 31 | val jsCode = "$methodName($formattedArgs)" 32 | 33 | val evaluationCallback: ((String) -> Unit)? = if (callbacks.isEmpty()) { 34 | null 35 | } else { 36 | { jsExecutionOutput -> 37 | callbacks.forEach { callback -> callback(jsExecutionOutput) } 38 | } 39 | } 40 | 41 | webView.evaluateJavascript(jsCode, evaluationCallback) 42 | } 43 | 44 | fun addCallback(callback: (String) -> Unit) { 45 | callbacks.add(callback) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /rich-html-editor/src/main/java/com/infomaniak/lib/richhtmleditor/Utils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Infomaniak Rich HTML Editor - Android 3 | * Copyright (C) 2024 Infomaniak Network SA 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.infomaniak.lib.richhtmleditor 18 | 19 | import androidx.annotation.ColorInt 20 | 21 | // TODO: This method might not be enough to escape user inputs and prevent access to JS code execution 22 | fun looselyEscapeAsStringLiteralForJs(string: String): String { 23 | val stringBuilder = StringBuilder("`") 24 | 25 | string.forEach { 26 | val char = when (it) { 27 | '`' -> "\\`" 28 | '\\' -> "\\\\" 29 | '$' -> "\\$" 30 | else -> it 31 | } 32 | stringBuilder.append(char) 33 | } 34 | 35 | return stringBuilder.append("`").toString() 36 | } 37 | 38 | fun encodeArgsForJs(value: Any?): String { 39 | return when (value) { 40 | null -> "null" 41 | is String -> looselyEscapeAsStringLiteralForJs(value) 42 | is Boolean, is Number -> value.toString() 43 | is JsColor -> "'${colorToRgbHex(value.color)}'" 44 | else -> throw NotImplementedError("Encoding ${value::class} for JS is not yet implemented") 45 | } 46 | } 47 | 48 | @OptIn(ExperimentalStdlibApi::class) 49 | private fun colorToRgbHex(@ColorInt color: Int) = color.toHexString(HexFormat.UpperCase).takeLast(6) 50 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_justify_full.xml: -------------------------------------------------------------------------------- 1 | 17 | 23 | 24 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_justify_center.xml: -------------------------------------------------------------------------------- 1 | 17 | 23 | 24 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_justify_left.xml: -------------------------------------------------------------------------------- 1 | 17 | 24 | 25 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_justify_right.xml: -------------------------------------------------------------------------------- 1 | 17 | 24 | 25 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 17 | 19 | 20 | 21 | 22 | 34 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_indent.xml: -------------------------------------------------------------------------------- 1 | 17 | 24 | 25 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_outdent.xml: -------------------------------------------------------------------------------- 1 | 17 | 24 | 25 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /rich-html-editor/src/main/java/com/infomaniak/lib/richhtmleditor/StatusCommand.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Infomaniak Rich HTML Editor - Android 3 | * Copyright (C) 2024 Infomaniak Network SA 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.infomaniak.lib.richhtmleditor 18 | 19 | enum class StatusCommand(override val argumentName: String, val statusType: StatusType) : ExecCommand { 20 | BOLD("bold", StatusType.STATE), 21 | ITALIC("italic", StatusType.STATE), 22 | STRIKE_THROUGH("strikeThrough", StatusType.STATE), 23 | UNDERLINE("underline", StatusType.STATE), 24 | ORDERED_LIST("insertOrderedList", StatusType.STATE), 25 | UNORDERED_LIST("insertUnorderedList", StatusType.STATE), 26 | SUBSCRIPT("subscript", StatusType.STATE), 27 | SUPERSCRIPT("superscript", StatusType.STATE), 28 | JUSTIFY_LEFT("justifyLeft", StatusType.STATE), 29 | JUSTIFY_CENTER("justifyCenter", StatusType.STATE), 30 | JUSTIFY_RIGHT("justifyRight", StatusType.STATE), 31 | JUSTIFY_FULL("justifyFull", StatusType.STATE), 32 | FONT_NAME("fontName", StatusType.VALUE), 33 | FONT_SIZE("fontSize", StatusType.VALUE), 34 | TEXT_COLOR("foreColor", StatusType.VALUE), 35 | BACKGROUND_COLOR("backColor", StatusType.VALUE), 36 | CREATE_LINK("", StatusType.COMPLEX), // This value is not meant to be called by JsBride's execCommand() 37 | } 38 | 39 | enum class OtherCommand(override val argumentName: String) : ExecCommand { 40 | REMOVE_FORMAT("removeFormat"), 41 | INDENT("indent"), 42 | OUTDENT("outdent"), 43 | UNDO("undo"), 44 | REDO("redo"), 45 | } 46 | 47 | interface ExecCommand { 48 | val argumentName: String 49 | } 50 | 51 | enum class StatusType { STATE, VALUE, COMPLEX } 52 | -------------------------------------------------------------------------------- /.github/workflows/rebase-default-branch.yml: -------------------------------------------------------------------------------- 1 | # Rebases a pull request on the repo's default branch when the "rebase" label is added 2 | # Link: https://github.com/Infomaniak/.github/blob/main/workflow-templates/rebase-default-branch.yml 3 | 4 | name: Rebase Pull Request 5 | 6 | on: 7 | pull_request: 8 | types: [ labeled ] 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | env: 15 | DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} 16 | 17 | jobs: 18 | main: 19 | if: ${{ contains(github.event.*.labels.*.name, 'rebase') }} 20 | name: Rebase 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v5.0.0 25 | with: 26 | ref: ${{ github.event.pull_request.head.ref }} 27 | fetch-depth: 0 28 | 29 | # Context: https://httgp.com/signing-commits-in-github-actions 30 | # Link: https://github.com/crazy-max/ghaction-import-gpg/releases 31 | - name: Import bot's GPG key for signing commits 32 | id: import-gpg 33 | uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6.3.0 34 | with: 35 | gpg_private_key: ${{ secrets.BOT_MOBILE_GPG_PRIVATE_KEY }} 36 | passphrase: ${{ secrets.BOT_MOBILE_GPG_PASSPHRASE }} 37 | git_config_global: true 38 | git_user_signingkey: true 39 | git_commit_gpgsign: true 40 | 41 | - name: perform rebase 42 | run: | 43 | git config --global user.name "dev-mobile-bot" 44 | git config --global user.email "mobile+github-bot@infomaniak-dev.ch" 45 | git status 46 | git pull 47 | git checkout "$DEFAULT_BRANCH" 48 | git status 49 | git pull 50 | git checkout "$GITHUB_HEAD_REF" 51 | git rebase "$DEFAULT_BRANCH" 52 | git push --force-with-lease 53 | git status 54 | 55 | # Context: https://github.com/marketplace/actions/actions-ecosystem-remove-labels 56 | # Link: https://github.com/actions-ecosystem/action-remove-labels/releases 57 | - name: remove label 58 | if: always() 59 | uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0 # v1.3.0 60 | with: 61 | labels: rebase 62 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/create_link_text_input.xml: -------------------------------------------------------------------------------- 1 | 17 | 22 | 23 | 29 | 30 | 35 | 36 | 37 | 38 | 43 | 44 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /rich-html-editor/src/main/assets/manage_links.js: -------------------------------------------------------------------------------- 1 | // Create link 2 | 3 | function createLink(displayText, url) { 4 | let range = getSelectionRangeOrNull() 5 | if (range === null) return 6 | 7 | if (range.collapsed) { 8 | // There's no selection, only a cursor. We can add the link manually 9 | let anchor = getAnchorNodeAtCursor() 10 | 11 | // If there is already a link, just change its href 12 | if (anchor) { 13 | anchor.href = url; 14 | // Change text content if it is specified 15 | updateAnchorDisplayText(anchor, displayText); 16 | } else { 17 | anchor = document.createElement('A') 18 | anchor.textContent = displayText || url; 19 | anchor.href = url; 20 | range.insertNode(anchor) 21 | } 22 | 23 | setCaretAtEndOfAnchor(anchor) 24 | } else { 25 | // There's already a selection so use execCommand to create a new link 26 | document.execCommand("createLink", false, url) 27 | 28 | // Update the newly created link's display text if we have a custom text 29 | if (displayText) { 30 | let anchor = getAnchorNodeAtCursor() 31 | updateAnchorDisplayText(anchor, displayText) 32 | } 33 | } 34 | } 35 | 36 | function getAnchorNodeAtCursor() { 37 | let anchors = getAllLinksPartiallyContainedInsideSelection() 38 | return anchors.length > 0 ? anchors[0] : null 39 | } 40 | 41 | function updateAnchorDisplayText(anchor, displayText) { 42 | if (displayText && anchor.textContent != displayText) { 43 | anchor.textContent = displayText; 44 | } 45 | } 46 | 47 | function setCaretAtEndOfAnchor(anchor) { 48 | const range = new Range(); 49 | range.setStart(anchor, 1); 50 | range.setEnd(anchor, 1); 51 | range.collapsed = true; 52 | 53 | const selection = document.getSelection(); 54 | selection.removeAllRanges(); 55 | selection.addRange(range); 56 | } 57 | 58 | // Unlink 59 | 60 | function unlink() { 61 | getAllLinksPartiallyContainedInsideSelection().forEach(anchor => unlinkAnchorNode(anchor)) 62 | } 63 | 64 | function unlinkAnchorNode(anchor) { 65 | let selection = document.getSelection() 66 | if (selection.rangeCount === 0) return 67 | 68 | let range = selection.getRangeAt(0) 69 | let rangeBackup = range.cloneRange() 70 | range.selectNodeContents(anchor) 71 | document.execCommand("unlink") 72 | 73 | selection.removeAllRanges() 74 | selection.addRange(rangeBackup) 75 | } 76 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_unordered_list.xml: -------------------------------------------------------------------------------- 1 | 17 | 22 | 25 | 28 | 31 | 34 | 37 | 40 | 41 | -------------------------------------------------------------------------------- /rich-html-editor/src/main/java/com/infomaniak/lib/richhtmleditor/executor/StateSubscriber.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Infomaniak Rich HTML Editor - Android 3 | * Copyright (C) 2024 Infomaniak Network SA 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.infomaniak.lib.richhtmleditor.executor 18 | 19 | import android.webkit.WebView 20 | import com.infomaniak.lib.richhtmleditor.StatusCommand 21 | import com.infomaniak.lib.richhtmleditor.StatusType 22 | import com.infomaniak.lib.richhtmleditor.injectScript 23 | 24 | internal class StateSubscriber(private val webView: WebView) : JsLifecycleAwareExecutor?>() { 25 | 26 | override fun executeImmediately(value: Set?) { 27 | webView.injectScript(createSubscribedStatesScript(value), "subscribedStates") 28 | } 29 | 30 | private fun createSubscribedStatesScript(inputSubscribedStates: Set?): String { 31 | val subscribedStates = inputSubscribedStates ?: StatusCommand.entries 32 | 33 | val stateCommands = mutableListOf() 34 | val valueCommands = mutableListOf() 35 | 36 | subscribedStates.forEach { 37 | when (it.statusType) { 38 | StatusType.STATE -> stateCommands.add(it) 39 | StatusType.VALUE -> valueCommands.add(it) 40 | StatusType.COMPLEX -> Unit 41 | } 42 | } 43 | 44 | val firstLine = generateConstTable("stateCommands", stateCommands) 45 | val secondLine = generateConstTable("valueCommands", valueCommands) 46 | 47 | val areLinksSubscribedTo = subscribedStates.contains(StatusCommand.CREATE_LINK) 48 | val reportLinkStatusLine = "var REPORT_LINK_STATUS = $areLinksSubscribedTo" 49 | 50 | return "$firstLine\n$secondLine\n$reportLinkStatusLine" 51 | } 52 | 53 | private fun generateConstTable(name: String, commands: Collection): String { 54 | return commands.joinToString(prefix = "var $name = [ ", postfix = " ]") { "'${it.argumentName}'" } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 17 | 23 | 24 | 25 | 31 | 34 | 37 | 38 | 39 | 40 | 46 | 47 | -------------------------------------------------------------------------------- /rich-html-editor/src/main/java/com/infomaniak/lib/richhtmleditor/executor/KeyboardOpener.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Infomaniak Rich HTML Editor - Android 3 | * Copyright (C) 2024 Infomaniak Network SA 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.infomaniak.lib.richhtmleditor.executor 18 | 19 | import android.app.Activity 20 | import android.view.View 21 | import android.view.ViewTreeObserver 22 | import android.view.inputmethod.InputMethodManager 23 | 24 | internal class KeyboardOpener(private val view: View) : JsLifecycleAwareExecutor() { 25 | 26 | private var listener: ViewTreeObserver.OnWindowFocusChangeListener? = null 27 | 28 | override fun executeImmediately(value: Unit) { 29 | if (view.requestFocus()) { 30 | if (view.hasWindowFocus()) { 31 | openKeyboard() 32 | } else { 33 | // The window won't have the focus most of the time when the configuration changes and we want to reopen the 34 | // keyboard right away. When this happen, we need to wait for the window to get the focus before opening the 35 | // keyboard. 36 | listener = object : ViewTreeObserver.OnWindowFocusChangeListener { 37 | override fun onWindowFocusChanged(hasFocus: Boolean) { 38 | if (hasFocus) { 39 | openKeyboard() 40 | view.viewTreeObserver.removeOnWindowFocusChangeListener(this) 41 | } 42 | } 43 | } 44 | 45 | view.viewTreeObserver.addOnWindowFocusChangeListener(listener) 46 | } 47 | } 48 | } 49 | 50 | fun removePendingListener() { 51 | view.viewTreeObserver.removeOnWindowFocusChangeListener(listener) 52 | listener = null 53 | } 54 | 55 | private fun openKeyboard() { 56 | val inputMethodManager = view.context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager 57 | inputMethodManager.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /rich-html-editor/src/main/java/com/infomaniak/lib/richhtmleditor/Extensions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Infomaniak Rich HTML Editor - Android 3 | * Copyright (C) 2024 Infomaniak Network SA 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.infomaniak.lib.richhtmleditor 18 | 19 | import android.content.Context 20 | import android.os.Build 21 | import android.os.Bundle 22 | import android.view.AbsSavedState 23 | import android.webkit.WebView 24 | import java.io.BufferedReader 25 | 26 | internal fun Context.readAsset(fileName: String): String { 27 | return assets 28 | .open(fileName) 29 | .bufferedReader() 30 | .use(BufferedReader::readText) 31 | } 32 | 33 | internal fun WebView.injectScript(scriptCode: String, id: String? = null) { 34 | val escapedStringLiteralId = id?.let { looselyEscapeAsStringLiteralForJs(it) } 35 | 36 | val removePreviousId = escapedStringLiteralId?.let { 37 | """ 38 | var previousScript = document.getElementById($it) 39 | if (previousScript) previousScript.remove() 40 | """.trimIndent() 41 | } ?: "" 42 | val setId = escapedStringLiteralId?.let { "script.id = ${it};" } ?: "" 43 | 44 | val escapedStringLiteralScriptCode = looselyEscapeAsStringLiteralForJs(scriptCode) 45 | val addScriptJs = """ 46 | var script = document.createElement('script'); 47 | script.type = 'text/javascript'; 48 | script.text = $escapedStringLiteralScriptCode; 49 | $setId 50 | 51 | document.head.appendChild(script); 52 | """.trimIndent() 53 | 54 | val code = removePreviousId + "\n" + addScriptJs 55 | 56 | evaluateJavascript(code, null) 57 | } 58 | 59 | internal fun WebView.injectCss(css: String) { 60 | val escapedStringLiteralCss = looselyEscapeAsStringLiteralForJs(css) 61 | val addCssJs = """ 62 | var style = document.createElement('style'); 63 | style.textContent = $escapedStringLiteralCss; 64 | 65 | document.head.appendChild(style); 66 | """.trimIndent() 67 | 68 | evaluateJavascript(addCssJs, null) 69 | } 70 | 71 | fun Bundle.getParcelableCompat(key: String, clazz: Class): AbsSavedState? { 72 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 73 | getParcelable(key, clazz) 74 | } else { 75 | getParcelable(key) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /rich-html-editor/src/main/java/com/infomaniak/lib/richhtmleditor/EditorStatuses.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Infomaniak Rich HTML Editor - Android 3 | * Copyright (C) 2024 Infomaniak Network SA 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.infomaniak.lib.richhtmleditor 18 | 19 | import androidx.annotation.ColorInt 20 | import kotlinx.coroutines.sync.Mutex 21 | import kotlinx.coroutines.sync.withLock 22 | 23 | data class EditorStatuses( 24 | var isBold: Boolean = false, 25 | var isItalic: Boolean = false, 26 | var isStrikeThrough: Boolean = false, 27 | var isUnderlined: Boolean = false, 28 | var fontName: String? = null, 29 | var fontSize: Float? = null, 30 | var textColor: Int? = null, 31 | var backgroundColor: Int? = null, 32 | var isLinkSelected: Boolean = false, 33 | var isOrderedListSelected: Boolean = false, 34 | var isUnorderedListSelected: Boolean = false, 35 | var isSubscript: Boolean = false, 36 | var isSuperscript: Boolean = false, 37 | var justification: Justification? = null, 38 | ) { 39 | private val mutex = Mutex() 40 | 41 | suspend fun updateStatusesAtomically( 42 | isBold: Boolean, 43 | isItalic: Boolean, 44 | isStrikeThrough: Boolean, 45 | isUnderlined: Boolean, 46 | fontName: String, 47 | fontSize: Float?, 48 | @ColorInt textColor: Int?, 49 | @ColorInt backgroundColor: Int?, 50 | isLinkSelected: Boolean, 51 | isOrderedListSelected: Boolean, 52 | isUnorderedListSelected: Boolean, 53 | isSubscript: Boolean, 54 | isSuperscript: Boolean, 55 | justification: Justification?, 56 | ) { 57 | mutex.withLock { 58 | this.isBold = isBold 59 | this.isItalic = isItalic 60 | this.isStrikeThrough = isStrikeThrough 61 | this.isUnderlined = isUnderlined 62 | this.fontName = fontName 63 | this.fontSize = fontSize 64 | this.textColor = textColor 65 | this.backgroundColor = backgroundColor 66 | this.isLinkSelected = isLinkSelected 67 | this.isOrderedListSelected = isOrderedListSelected 68 | this.isUnorderedListSelected = isUnorderedListSelected 69 | this.isSubscript = isSubscript 70 | this.isSuperscript = isSuperscript 71 | this.justification = justification 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_ordered_list.xml: -------------------------------------------------------------------------------- 1 | 17 | 22 | 25 | 28 | 31 | 34 | 37 | 40 | 41 | -------------------------------------------------------------------------------- /rich-html-editor/src/main/java/com/infomaniak/lib/richhtmleditor/EditorReloader.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Infomaniak Rich HTML Editor - Android 3 | * Copyright (C) 2024 Infomaniak Network SA 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.infomaniak.lib.richhtmleditor 18 | 19 | import android.os.Looper 20 | import kotlinx.coroutines.CoroutineScope 21 | import kotlinx.coroutines.flow.MutableStateFlow 22 | import kotlinx.coroutines.flow.collectLatest 23 | import kotlinx.coroutines.launch 24 | 25 | /** 26 | * A utility class to help easily reload the html content of a [RichHtmlEditorWebView] on configuration changes. 27 | * 28 | * This class is meant to be instantiated inside a ViewModel to be able to retain the HTML content through configuration changes. 29 | * 30 | * @param coroutineScope A coroutine scope where the exportation of the HTML will be processed. Preferably the viewModelScope of 31 | * the ViewModel where this class has been instantiated. 32 | * 33 | * @see load 34 | * @see save 35 | */ 36 | class EditorReloader(private val coroutineScope: CoroutineScope) { 37 | 38 | private var needToReloadHtml: Boolean = false 39 | private var savedHtml = MutableStateFlow(null) 40 | 41 | /** 42 | * An alternative for loading the HTML content of the [RichHtmlEditorWebView]. 43 | * 44 | * This method can be called within the `onViewCreated()` method of the Fragment containing your [RichHtmlEditorWebView]. 45 | * On the initial call, it loads the default HTML content. For all subsequent calls, it reloads the previously loaded HTML 46 | * content. 47 | * 48 | * @param editor The editor to load content into. 49 | * @param defaultHtml The HTML to load on the initial call. On subsequent calls, this value won't be taken into account. 50 | * 51 | * Usage: 52 | * ``` 53 | * override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 54 | * super.onViewCreated(view, savedInstanceState) 55 | * lifecycleScope.launch { 56 | * viewModel.editorReloader.load(editor, "

Hello World

") 57 | * } 58 | * } 59 | * ``` 60 | * 61 | * @throws IllegalStateException If the method is not called on the main thread. 62 | */ 63 | suspend fun load(editor: RichHtmlEditorWebView, defaultHtml: String) { 64 | if (Looper.myLooper() != Looper.getMainLooper()) error("The load method needs to be called on the main thread") 65 | 66 | if (needToReloadHtml) { 67 | savedHtml.collectLatest { 68 | if (it == null) return@collectLatest 69 | 70 | resetSavedHtml() 71 | editor.setHtml(it) 72 | } 73 | } else { 74 | editor.setHtml(defaultHtml) 75 | } 76 | 77 | enableHtmlReload() 78 | } 79 | 80 | /** 81 | * Exports and saves the editor's HTML content for later reloading. 82 | * 83 | * This method should be called within the `onSaveInstanceState()` method of the Fragment. 84 | * 85 | * @param editor The editor whose content needs to be saved. 86 | * 87 | * Usage: 88 | * ``` 89 | * override fun onSaveInstanceState(outState: Bundle) { 90 | * super.onSaveInstanceState(outState) 91 | * viewModel.editorReloader.save(editor) 92 | * } 93 | * ``` 94 | */ 95 | fun save(editor: RichHtmlEditorWebView) { 96 | editor.exportHtml { 97 | coroutineScope.launch { savedHtml.emit(it) } 98 | } 99 | } 100 | 101 | private suspend fun resetSavedHtml() { 102 | savedHtml.emit(null) 103 | } 104 | 105 | private fun enableHtmlReload() { 106 | needToReloadHtml = true 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /rich-html-editor/src/main/assets/define_listeners.js: -------------------------------------------------------------------------------- 1 | let currentSelectionState = {} 2 | let previousEmptyStat 3 | 4 | // Helper functions 5 | 6 | function getBody() { 7 | return document.body 8 | } 9 | 10 | function getEditor() { 11 | // The id of this HTML tag is shared across multiple files and needs to remain the same 12 | return document.getElementById("editor") 13 | } 14 | 15 | function onBodyResize(callback) { 16 | let resizeObserver = new ResizeObserver(callback) 17 | resizeObserver.observe(document.documentElement) 18 | } 19 | 20 | function getSelectionRangeOrNull() { 21 | const selection = document.getSelection() 22 | return (selection.rangeCount > 0) ? selection.getRangeAt(0) : null 23 | } 24 | 25 | function onEditorChildListChange(callback) { 26 | const config = { childList: true } 27 | const observer = new MutationObserver(callback) 28 | observer.observe(getEditor(), config) 29 | } 30 | 31 | // Core logic 32 | 33 | function exportHtml() { 34 | window.editor.exportHtml(getEditor().innerHTML) 35 | } 36 | 37 | function focusCursorOnScreen() { 38 | let rect = getCaretRect() 39 | if (rect) window.editor.focusCursorOnScreen(rect.left, rect.top, rect.right, rect.bottom) 40 | } 41 | 42 | function findElementNode(element) { 43 | if (element === null) return null 44 | if (element.nodeType === Node.ELEMENT_NODE) return element 45 | return findElementNode(element.parentNode) 46 | } 47 | 48 | function getCaretRect() { 49 | const selection = window.getSelection() 50 | const lastSelectedNode = selection.focusNode 51 | 52 | if (selection.rangeCount === 0) return 53 | 54 | const range = selection.getRangeAt(0).cloneRange() 55 | 56 | // Create a range around the last selected node so the webview can scroll and follow the cursor even if the whole range is 57 | // bigger than the screen 58 | range.selectNodeContents(lastSelectedNode) 59 | 60 | const rangeRects = range.getClientRects() 61 | 62 | let rect 63 | switch (rangeRects.length) { 64 | case 0: 65 | rect = findElementNode(lastSelectedNode).getBoundingClientRect() 66 | break; 67 | case 1: 68 | rect = rangeRects[0] 69 | break; 70 | default: 71 | rect = range.getBoundingClientRect() 72 | break; 73 | } 74 | 75 | return rect 76 | } 77 | 78 | function reportSelectionStateChangedIfNecessary() { 79 | const newSelectionState = getCurrentSelectionState() 80 | if (!areSelectionStatesTheSame(currentSelectionState, newSelectionState)) { 81 | currentSelectionState = newSelectionState 82 | console.log("New selection changed:", currentSelectionState) 83 | window.editor.reportCommandDataChange( 84 | newSelectionState["bold"], 85 | newSelectionState["italic"], 86 | newSelectionState["strikeThrough"], 87 | newSelectionState["underline"], 88 | newSelectionState["fontName"], 89 | newSelectionState["fontSize"], 90 | newSelectionState["foreColor"], 91 | newSelectionState["backColor"], 92 | newSelectionState["isLinkSelected"], 93 | newSelectionState["insertOrderedList"], 94 | newSelectionState["insertUnorderedList"], 95 | newSelectionState["subscript"], 96 | newSelectionState["superscript"], 97 | newSelectionState["justifyLeft"], 98 | newSelectionState["justifyCenter"], 99 | newSelectionState["justifyRight"], 100 | newSelectionState["justifyFull"] 101 | ) 102 | } 103 | } 104 | 105 | function getCurrentSelectionState() { 106 | let currentState = {} 107 | 108 | for (const property of stateCommands) { 109 | currentState[property] = document.queryCommandState(property) 110 | } 111 | for (const property of valueCommands) { 112 | currentState[property] = document.queryCommandValue(property) 113 | } 114 | 115 | if (REPORT_LINK_STATUS) currentState["isLinkSelected"] = computeLinkStatus() 116 | 117 | return currentState 118 | } 119 | 120 | function computeLinkStatus() { 121 | return getAllLinksPartiallyContainedInsideSelection().length > 0 122 | } 123 | 124 | function areSelectionStatesTheSame(state1, state2) { 125 | let isLinkTheSame = (REPORT_LINK_STATUS) ? state1["isLinkSelected"] === state2["isLinkSelected"] : true 126 | 127 | return stateCommands.every(property => state1[property] === state2[property]) 128 | && valueCommands.every(property => state1[property] === state2[property]) 129 | && isLinkTheSame 130 | } 131 | 132 | function updateWebViewHeightWithBodyHeight() { 133 | let documentElement = document.documentElement 134 | let paddingTop = parseInt(window.getComputedStyle(documentElement)["margin-top"]) 135 | let paddingBottom = parseInt(window.getComputedStyle(documentElement)["margin-bottom"]) 136 | 137 | window.editor.reportNewDocumentHeight((documentElement.offsetHeight + paddingTop + paddingBottom) * window.devicePixelRatio) 138 | } 139 | 140 | function reportEmptyBodyStatus() { 141 | const isEditorEmpty = getEditor().childNodes.length === 0 142 | if (previousEmptyStat === isEditorEmpty) return 143 | previousEmptyStat = isEditorEmpty 144 | window.editor.onEmptyBodyChanges(isEditorEmpty) 145 | } 146 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Infomaniak Rich HTML Editor 2 | 3 | ![JitPack](https://jitpack.io/v/Infomaniak/android-rich-html-editor.svg) 4 | 5 | The **Infomaniak Rich HTML Editor** is an Android library designed to display HTML and easily modify it on the fly. It relies on 6 | the power of the `contenteditable` HTML attribute inside a WebView. 7 | 8 | 9 | Android sample app 10 | 11 | Looking for an iOS or macOS equivalent? Check out the Swift version of this editor here: [Infomaniak/swift-rich-html-editor](https://github.com/Infomaniak/swift-rich-html-editor) 12 | 13 | ## ✍️ About 14 | 15 | ### Features 16 | 17 | - **HTML Content Editing**: Full support for viewing and editing HTML content directly. 18 | - **Wide range of formatting commands**: Many commands are available to format text, from simple commands like bold to more 19 | advanced ones like link creation. 20 | - **Useful API to control the editor**: It lets you: 21 | - Listen to format option's activated status when the cursor moves around the html content 22 | - Add custom css to the editor 23 | - Add custom scripts to the editor 24 | 25 | ### Installation 26 | 27 | Add this dependency to your project: 28 | 29 | Using version catalog: 30 | 31 | ```toml 32 | rich-html-editor = { module = "com.github.infomaniak:android-rich-html-editor", version.ref = "richHtmlEditorVersion" } 33 | ``` 34 | 35 | Directly inside gradle dependencies: 36 | 37 | ```gradle 38 | dependencies { 39 | implementation "com.github.infomaniak:android-rich-html-editor:$richHtmlEditorVersion" 40 | } 41 | ``` 42 | 43 | The latest version is: ![JitPack](https://jitpack.io/v/Infomaniak/android-rich-html-editor.svg) 44 | 45 | ## ⚙️ Usage 46 | 47 | ### Simplest usage 48 | 49 | In your layout: 50 | 51 | ```xml 52 | 56 | ``` 57 | 58 | In your fragment: 59 | 60 | ```kt 61 | val html = """ 62 | 63 | 64 |

Hello World

65 | 66 | 67 | """.trimMargin() 68 | editor.setHtml(html) 69 | ``` 70 | 71 | ### Change format and reacte to its changes 72 | 73 | Add a button to your xml: 74 | 75 | ```xml 76 |