├── .swift-version ├── .mise.toml ├── Assets ├── ios.webp ├── macos.webp ├── visionos.webp └── infomaniak-mail.webp ├── Examples ├── Example iOS │ ├── Example iOS │ │ ├── Resources │ │ │ ├── editor.css │ │ │ ├── Assets.xcassets │ │ │ │ ├── Contents.json │ │ │ │ ├── AccentColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ └── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ ├── Info.plist │ │ │ └── Base.lproj │ │ │ │ └── LaunchScreen.storyboard │ │ ├── UI │ │ │ ├── Views │ │ │ │ └── UIDivider.swift │ │ │ └── Controllers │ │ │ │ ├── ScrollableEditorViewController.swift │ │ │ │ ├── EditorViewController │ │ │ │ ├── EditorViewController+RichEditorViewDelegate.swift │ │ │ │ ├── EditorViewController.swift │ │ │ │ └── EditorViewController+Toolbar.swift │ │ │ │ ├── FixedSizeEditorViewController.swift │ │ │ │ └── NotScrollableViewController.swift │ │ ├── AppDelegate.swift │ │ ├── SceneDelegate.swift │ │ └── Model │ │ │ └── ToolbarAction.swift │ └── Example iOS.xcodeproj │ │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── swiftpm │ │ └── Package.resolved ├── Example macOS │ ├── Example macOS │ │ ├── Resources │ │ │ ├── style.css │ │ │ ├── Assets.xcassets │ │ │ │ ├── Contents.json │ │ │ │ ├── AccentColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ └── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ └── Example_macOS.entitlements │ │ ├── AppDelegate.swift │ │ └── UI │ │ │ ├── ViewController.swift │ │ │ └── WindowController.swift │ └── Example macOS.xcodeproj │ │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── swiftpm │ │ │ └── Package.resolved │ │ └── project.pbxproj └── Example SwiftUI │ ├── Example SwiftUI │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Example_SwiftUI.entitlements │ ├── Views │ │ ├── Editors │ │ │ ├── NotScrollableEditorView.swift │ │ │ ├── FixedSizeEditorView.swift │ │ │ └── ScrollableEditorView.swift │ │ └── RootView.swift │ └── Example_SwiftUIApp.swift │ └── Example SwiftUI.xcodeproj │ ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── swiftpm │ │ └── Package.resolved │ └── project.pbxproj ├── .spi.yml ├── NOTICE.txt ├── Sources └── InfomaniakRichHTMLEditor │ ├── Resources │ ├── css │ │ └── style.css │ ├── js │ │ ├── utils │ │ │ ├── captureLog.js │ │ │ ├── javascriptBridge.js │ │ │ └── utils.js │ │ ├── main.js │ │ └── editor │ │ │ ├── observer.js │ │ │ ├── commands.js │ │ │ ├── focus.js │ │ │ ├── text-attributes.js │ │ │ ├── links.js │ │ │ └── selection.js │ └── index.html │ ├── Models │ ├── EditorError.swift │ ├── CaretPosition.swift │ ├── TextJustification.swift │ ├── ExecCommand.swift │ ├── UITextAttributes.swift │ ├── UserScript.swift │ └── JavaScriptFunction.swift │ ├── Utils │ ├── Constants.swift │ └── JavaScriptFormatterHelper.swift │ ├── RichHTMLWebView.swift │ ├── Extensions │ ├── UIView+Extension.swift │ ├── WKUserContentController+Extension.swift │ ├── String+Escaped.swift │ └── PlateformColor+Extension.swift │ ├── SwiftUI │ ├── Views │ │ ├── RichHTMLEditorCoordinator.swift │ │ ├── RichHTMLEditor+Environment.swift │ │ ├── RichEditor+Modifier.swift │ │ └── RichHTMLEditor.swift │ └── Models │ │ ├── TextAttributes.swift │ │ └── TextAttributes+Commands.swift │ ├── WebViewBridge │ ├── JavaScriptManager.swift │ └── ScriptMessageHandler.swift │ ├── RichHTMLEditorView+Commands.swift │ ├── RichHTMLEditorViewDelegate.swift │ └── RichHTMLEditorView.swift ├── Tests └── InfomaniakRichHTMLEditorTests │ └── InfomaniakRichHTMLEditorTests.swift ├── Package.swift ├── .github └── workflows │ └── ci.yml ├── .swiftformat ├── .gitignore ├── README.md └── LICENSE /.swift-version: -------------------------------------------------------------------------------- 1 | 5.10 2 | -------------------------------------------------------------------------------- /.mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | swiftformat = "latest" 3 | swiftlint = "latest" 4 | -------------------------------------------------------------------------------- /Assets/ios.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infomaniak/swift-rich-html-editor/HEAD/Assets/ios.webp -------------------------------------------------------------------------------- /Assets/macos.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infomaniak/swift-rich-html-editor/HEAD/Assets/macos.webp -------------------------------------------------------------------------------- /Assets/visionos.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infomaniak/swift-rich-html-editor/HEAD/Assets/visionos.webp -------------------------------------------------------------------------------- /Examples/Example iOS/Example iOS/Resources/editor.css: -------------------------------------------------------------------------------- 1 | #swift-rich-html-editor { 2 | padding: 16px; 3 | } 4 | -------------------------------------------------------------------------------- /Examples/Example macOS/Example macOS/Resources/style.css: -------------------------------------------------------------------------------- 1 | #swift-rich-html-editor { 2 | padding: 16px; 3 | } 4 | -------------------------------------------------------------------------------- /Assets/infomaniak-mail.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infomaniak/swift-rich-html-editor/HEAD/Assets/infomaniak-mail.webp -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [InfomaniakRichHTMLEditor] 5 | platform: ios 6 | -------------------------------------------------------------------------------- /Examples/Example SwiftUI/Example SwiftUI/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/Example iOS/Example iOS/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/Example macOS/Example macOS/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Infomaniak RichHTMLEditor 2 | Copyright [2013-2014] Infomaniak Network SA 3 | 4 | This product includes software developed at 5 | Infomaniak Network SA. 6 | -------------------------------------------------------------------------------- /Examples/Example iOS/Example iOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/Example SwiftUI/Example SwiftUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/Example macOS/Example macOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/Example SwiftUI/Example SwiftUI/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Examples/Example iOS/Example iOS/Resources/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Examples/Example macOS/Example macOS/Resources/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/Resources/css/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | color-scheme: light dark; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | padding: 0; 8 | font-family: -apple-system; 9 | } 10 | 11 | #swift-rich-html-editor { 12 | outline-style: none; 13 | } 14 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/Resources/js/utils/captureLog.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function captureLog(message) { 4 | window.webkit.messageHandlers.scriptLog.postMessage(message); 5 | } 6 | 7 | window.console.log = captureLog; 8 | window.console.info = captureLog; 9 | window.console.error = captureLog; 10 | -------------------------------------------------------------------------------- /Examples/Example iOS/Example iOS/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/Resources/js/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | document.addEventListener("DOMContentLoaded", () => { 4 | reportEditorDidLoad(); 5 | 6 | observeResize(document.documentElement); 7 | observeContentMutation(document, getEditor()); 8 | observeSelectionChange(document); 9 | }); 10 | -------------------------------------------------------------------------------- /Examples/Example SwiftUI/Example SwiftUI/Example_SwiftUI.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/Resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /Tests/InfomaniakRichHTMLEditorTests/InfomaniakRichHTMLEditorTests.swift: -------------------------------------------------------------------------------- 1 | @testable import InfomaniakRichHTMLEditor 2 | import XCTest 3 | 4 | final class InfomaniakRichHTMLEditorTests: XCTestCase { 5 | func testExample() throws { 6 | // XCTest Documentation 7 | // https://developer.apple.com/documentation/xctest 8 | 9 | // Defining Test Cases and Test Methods 10 | // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Examples/Example macOS/Example macOS/Resources/Example_macOS.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.network.client 10 | 11 | com.apple.security.network.server 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Examples/Example iOS/Example iOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "fec33e31a0db89d6b395721c853f0ef51e23853df6f52b5209e7b75608d50ac3", 3 | "pins" : [ 4 | { 5 | "identity" : "swift-rich-html-editor", 6 | "kind" : "remoteSourceControl", 7 | "location" : "git@github.com:Infomaniak/swift-rich-html-editor.git", 8 | "state" : { 9 | "branch" : "main", 10 | "revision" : "eba94eddbb0a0e57ee78227ef790e7ecc02148ce" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /Examples/Example macOS/Example macOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "388ab5b28d94248ac91d70a91ddcf17e4989b657943badfa737a4328b759cd67", 3 | "pins" : [ 4 | { 5 | "identity" : "swift-rich-html-editor", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/Infomaniak/swift-rich-html-editor", 8 | "state" : { 9 | "branch" : "main", 10 | "revision" : "eba94eddbb0a0e57ee78227ef790e7ecc02148ce" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /Examples/Example SwiftUI/Example SwiftUI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "388ab5b28d94248ac91d70a91ddcf17e4989b657943badfa737a4328b759cd67", 3 | "pins" : [ 4 | { 5 | "identity" : "swift-rich-html-editor", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/Infomaniak/swift-rich-html-editor", 8 | "state" : { 9 | "branch" : "main", 10 | "revision" : "eba94eddbb0a0e57ee78227ef790e7ecc02148ce" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /Examples/Example SwiftUI/Example SwiftUI/Views/Editors/NotScrollableEditorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotScrollableEditorView.swift 3 | // Example SwiftUI 4 | // 5 | // Created by Valentin Perignon on 01/08/2024. 6 | // 7 | 8 | import InfomaniakRichHTMLEditor 9 | import SwiftUI 10 | 11 | struct NotScrollableEditorView: View { 12 | @State private var html = "" 13 | @StateObject private var textAttributes = TextAttributes() 14 | 15 | var body: some View { 16 | ScrollView { 17 | RichHTMLEditor(html: $html, textAttributes: textAttributes) 18 | } 19 | } 20 | } 21 | 22 | #Preview { 23 | NotScrollableEditorView() 24 | } 25 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/Models/EditorError.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | enum EditorError: Error, Sendable { 15 | case impossibleToLoadWKUserScript(filename: String) 16 | } 17 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/Utils/Constants.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | enum Constants: Sendable { 15 | static let packageID = "com.infomaniak.swift-rich-html-editor" 16 | } 17 | -------------------------------------------------------------------------------- /Examples/Example iOS/Example iOS/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Examples/Example SwiftUI/Example SwiftUI/Views/Editors/FixedSizeEditorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FixedSizeEditorView.swift 3 | // Example SwiftUI 4 | // 5 | // Created by Valentin Perignon on 01/08/2024. 6 | // 7 | 8 | import InfomaniakRichHTMLEditor 9 | import SwiftUI 10 | 11 | struct FixedSizeEditorView: View { 12 | @State private var html = "" 13 | @StateObject private var textAttributes = TextAttributes() 14 | 15 | var body: some View { 16 | RichHTMLEditor(html: $html, textAttributes: textAttributes) 17 | .editorScrollable(true) 18 | .overlay( 19 | RoundedRectangle(cornerRadius: 8) 20 | .stroke(Color.blue, lineWidth: 1) 21 | ) 22 | .padding(16) 23 | .frame(height: 200) 24 | } 25 | } 26 | 27 | #Preview { 28 | FixedSizeEditorView() 29 | } 30 | -------------------------------------------------------------------------------- /Examples/Example SwiftUI/Example SwiftUI/Example_SwiftUIApp.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import SwiftUI 15 | 16 | @main 17 | struct Example_SwiftUIApp: App { 18 | var body: some Scene { 19 | WindowGroup { 20 | RootView() 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Examples/Example macOS/Example macOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import Cocoa 15 | 16 | @main 17 | class AppDelegate: NSObject, NSApplicationDelegate { 18 | func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { 19 | return true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "InfomaniakRichHTMLEditor", 8 | platforms: [ 9 | .iOS(.v14), 10 | .visionOS(.v1), 11 | .macOS(.v11) 12 | ], 13 | products: [ 14 | .library( 15 | name: "InfomaniakRichHTMLEditor", 16 | targets: ["InfomaniakRichHTMLEditor"] 17 | ) 18 | ], 19 | targets: [ 20 | .target( 21 | name: "InfomaniakRichHTMLEditor", 22 | resources: [ 23 | .process("Resources/") 24 | ] 25 | ), 26 | .testTarget( 27 | name: "InfomaniakRichHTMLEditorTests", 28 | dependencies: ["InfomaniakRichHTMLEditor"] 29 | ) 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/Resources/js/utils/javascriptBridge.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function reportEditorDidLoad() { 4 | window.webkit.messageHandlers.editorDidLoad.postMessage(null); 5 | } 6 | 7 | function reportContentDidChange(content) { 8 | window.webkit.messageHandlers.contentDidChange.postMessage(content); 9 | } 10 | 11 | function reportContentHeightDidChange(height) { 12 | window.webkit.messageHandlers.contentHeightDidChange.postMessage(height); 13 | } 14 | 15 | function reportSelectedTextAttributesDidChange(textAttributes) { 16 | const json = JSON.stringify(textAttributes); 17 | window.webkit.messageHandlers.selectedTextAttributesDidChange.postMessage(json); 18 | } 19 | 20 | function reportCaretPositionDidChange(caretRect) { 21 | window.webkit.messageHandlers.caretPositionDidChange.postMessage([caretRect.x, caretRect.y, caretRect.width, caretRect.height]); 22 | } 23 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/Resources/js/editor/observer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // MARK: - Observation methods 4 | 5 | function observeContentMutation(target, contentContainer) { 6 | const mutationObserver = new MutationObserver(() => { 7 | reportContentDidChange(contentContainer.innerHTML); 8 | }); 9 | mutationObserver.observe(target, { subtree: true, childList: true, characterData: true }); 10 | } 11 | 12 | function observeResize(target) { 13 | const sizeObserver = new ResizeObserver(() => { 14 | let newContentHeight = document.documentElement.offsetHeight; 15 | reportContentHeightDidChange(newContentHeight); 16 | }); 17 | sizeObserver.observe(target); 18 | } 19 | 20 | function observeSelectionChange(target) { 21 | target.addEventListener("selectionchange", () => { 22 | setTimeout(computeAndReportCaretPosition, 120); 23 | reportSelectedTextAttributesIfNecessary(); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /Examples/Example iOS/Example iOS/UI/Views/UIDivider.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import UIKit 15 | 16 | final class UIDivider: UIView { 17 | init() { 18 | super.init(frame: .zero) 19 | backgroundColor = .systemGray4 20 | } 21 | 22 | @available(*, unavailable) 23 | required init?(coder: NSCoder) { 24 | fatalError("init(coder:) has not been implemented") 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI workflow 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 9 | cancel-in-progress: true 10 | 11 | jobs: 12 | build_and_test_iOS: 13 | name: Build and Test project on iOS 14 | runs-on: [ self-hosted, iOS ] 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | - name: Build 20 | run: xcodebuild -scheme InfomaniakRichHTMLEditor build -destination "generic/platform=iOS" 21 | - name: Test 22 | run: xcodebuild -scheme InfomaniakRichHTMLEditor test -destination "platform=iOS Simulator,name=iPhone 17,OS=latest" 23 | 24 | build_and_test_macOS: 25 | name: Build and Test project on macOS 26 | runs-on: [ self-hosted, macOS ] 27 | 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v2 31 | - name: Build 32 | run: swift build 33 | - name: Test 34 | run: swift test 35 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/Models/CaretPosition.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import Foundation 15 | 16 | /// Describes a position where the caret can be set. 17 | public enum CaretPosition: Sendable { 18 | /// At the beginning of the document, before any content. 19 | case beginningOfDocument 20 | /// At the beginning of the document, after all content. 21 | case endOfDocument 22 | /// At the beginning of an HTML element. 23 | case selector(String) 24 | } 25 | -------------------------------------------------------------------------------- /Examples/Example SwiftUI/Example SwiftUI/Views/Editors/ScrollableEditorView.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import InfomaniakRichHTMLEditor 15 | import SwiftUI 16 | 17 | struct ScrollableEditorView: View { 18 | @State private var html = "" 19 | @StateObject private var textAttributes = TextAttributes() 20 | 21 | var body: some View { 22 | RichHTMLEditor(html: $html, textAttributes: textAttributes) 23 | .editorScrollable(true) 24 | } 25 | } 26 | 27 | #Preview { 28 | ScrollableEditorView() 29 | } 30 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/RichHTMLWebView.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import WebKit 15 | 16 | public class RichHTMLWebView: WKWebView { 17 | #if canImport(UIKit) && !os(visionOS) 18 | public override var inputAccessoryView: UIView? { 19 | get { 20 | return richHTMLEditorInputAccessoryView 21 | } 22 | set { 23 | richHTMLEditorInputAccessoryView = newValue 24 | } 25 | } 26 | 27 | private var richHTMLEditorInputAccessoryView: UIView? 28 | #endif 29 | } 30 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/Resources/js/editor/commands.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Executes a command with document.execCommand(). 5 | * If the command changes the selected text, the WKWebView will be notified. 6 | * 7 | * @param {string} command - The name of the command to execute 8 | * @param {string|null} argument - An optional argument for the command 9 | */ 10 | function execCommand(command, argument) { 11 | document.execCommand(command, false, argument); 12 | reportSelectedTextAttributesIfNecessary(); 13 | } 14 | 15 | 16 | /** 17 | * Sets the HTML content of the editor. 18 | * The current content will be replaced by the new content. 19 | * 20 | * @param {string} content - The new HTML content of the editor 21 | */ 22 | function setContent(content) { 23 | getEditor().innerHTML = content; 24 | } 25 | 26 | /** 27 | * Injects new CSS rules to the editor to change its style. 28 | * 29 | * @param {string} content - The new CSS rules to add to the editor 30 | */ 31 | function injectCSS(content) { 32 | const styleElement = document.createElement("style"); 33 | styleElement.textContent = content; 34 | document.head.appendChild(styleElement); 35 | } 36 | -------------------------------------------------------------------------------- /Examples/Example macOS/Example macOS/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/Models/TextJustification.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | /// Describes how the text is aligned. 15 | public enum TextJustification: String, Codable, Sendable { 16 | /// Fully justified. 17 | case full 18 | /// Aligned to the left. 19 | case left 20 | /// Centered. 21 | case center 22 | /// Aligned to the right. 23 | case right 24 | 25 | var command: ExecCommand { 26 | switch self { 27 | case .full: 28 | return .justifyFull 29 | case .left: 30 | return .justifyLeft 31 | case .center: 32 | return .justifyCenter 33 | case .right: 34 | return .justifyRight 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Examples/Example SwiftUI/Example SwiftUI/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "1x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "2x", 16 | "size" : "16x16" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "1x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "2x", 26 | "size" : "32x32" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "2x", 36 | "size" : "128x128" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "1x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "2x", 46 | "size" : "256x256" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "1x", 51 | "size" : "512x512" 52 | }, 53 | { 54 | "idiom" : "mac", 55 | "scale" : "2x", 56 | "size" : "512x512" 57 | } 58 | ], 59 | "info" : { 60 | "author" : "xcode", 61 | "version" : 1 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Examples/Example iOS/Example iOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import UIKit 15 | 16 | @main 17 | class AppDelegate: UIResponder, UIApplicationDelegate { 18 | func application( 19 | _ application: UIApplication, 20 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 21 | ) -> Bool { 22 | return true 23 | } 24 | 25 | // MARK: UISceneSession Lifecycle 26 | 27 | func application( 28 | _ application: UIApplication, 29 | configurationForConnecting connectingSceneSession: UISceneSession, 30 | options: UIScene.ConnectionOptions 31 | ) -> UISceneConfiguration { 32 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/Extensions/UIView+Extension.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | #if canImport(UIKit) 15 | import UIKit 16 | 17 | extension UIView { 18 | var containsFirstResponder: Bool { 19 | if isFirstResponder { 20 | return true 21 | } 22 | 23 | for view in subviews { 24 | if view.containsFirstResponder { 25 | return true 26 | } 27 | } 28 | return false 29 | } 30 | 31 | func findClosestScrollView() -> UIScrollView? { 32 | if let scrollView = self as? UIScrollView { 33 | return scrollView 34 | } 35 | if let superview { 36 | return superview.findClosestScrollView() 37 | } 38 | return nil 39 | } 40 | } 41 | #endif 42 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/Extensions/WKUserContentController+Extension.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import WebKit 15 | 16 | extension WKUserContentController { 17 | func add(_ scriptMessageHandler: any WKScriptMessageHandler, scriptMessage: ScriptMessageHandler.ScriptMessage) { 18 | add(scriptMessageHandler, name: scriptMessage.rawValue) 19 | } 20 | 21 | func addUserScript(named filename: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool) throws { 22 | guard let url = Bundle.module.url(forResource: filename, withExtension: "js"), let document = try? String(contentsOf: url) 23 | else { throw EditorError.impossibleToLoadWKUserScript(filename: filename) } 24 | 25 | addUserScript(WKUserScript(source: document, injectionTime: injectionTime, forMainFrameOnly: true)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Examples/Example iOS/Example iOS/UI/Controllers/ScrollableEditorViewController.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import UIKit 15 | 16 | final class ScrollableEditorViewController: EditorViewController { 17 | override func setupEditor() { 18 | super.setupEditor() 19 | 20 | editor.isScrollEnabled = true 21 | editor.webView.scrollView.keyboardDismissMode = .interactive 22 | view.addSubview(editor) 23 | 24 | NSLayoutConstraint.activate([ 25 | editor.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), 26 | editor.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), 27 | editor.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), 28 | editor.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor) 29 | ]) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/Models/ExecCommand.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | enum ExecCommand: String, CaseIterable, Sendable { 15 | // Commands that return a state 16 | case bold 17 | case italic 18 | case underline 19 | case strikeThrough 20 | case toggleSubscript = "subscript" 21 | case toggleSuperscript = "superscript" 22 | case orderedList = "insertOrderedList" 23 | case unorderedList = "insertUnorderedList" 24 | case justifyLeft 25 | case justifyCenter 26 | case justifyRight 27 | case justifyFull 28 | 29 | // Commands that return a value 30 | case fontName 31 | case fontSize 32 | case backgroundColor = "backColor" 33 | case foregroundColor = "foreColor" 34 | 35 | // Commands that return nothing 36 | case removeFormat 37 | case undo 38 | case redo 39 | case indent 40 | case outdent 41 | } 42 | -------------------------------------------------------------------------------- /Examples/Example iOS/Example iOS/UI/Controllers/EditorViewController/EditorViewController+RichEditorViewDelegate.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY∆ 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import InfomaniakRichHTMLEditor 15 | import UIKit 16 | 17 | extension EditorViewController: RichHTMLEditorViewDelegate { 18 | func richHTMLEditorViewDidLoad(_ richHTMLEditorView: RichHTMLEditorView) { 19 | _ = richHTMLEditorView.becomeFirstResponder() 20 | } 21 | 22 | func richHTMLEditorView( 23 | _ richHTMLEditorView: RichHTMLEditorView, 24 | selectedTextAttributesDidChange textAttributes: UITextAttributes 25 | ) { 26 | for element in toolbarButtons { 27 | guard let button = element as? UIButton, let action = ToolbarAction(rawValue: button.tag) else { 28 | continue 29 | } 30 | button.isSelected = action.isSelected(textAttributes) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/Utils/JavaScriptFormatterHelper.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | enum JavaScriptFormatterHelper { 15 | static func format(_ arg: Any?, mustEscapeString: Bool = true) -> String { 16 | if arg == nil { 17 | return "null" 18 | } else if let values = arg as? [Any?] { 19 | let formattedValues = values.map { format($0, mustEscapeString: mustEscapeString) } 20 | return "[\(formattedValues.joined(separator: ", "))]" 21 | } else if let value = arg as? String { 22 | let escapedStringIfNeeded = mustEscapeString ? value.escapedForJavaScript : value 23 | return "`\(escapedStringIfNeeded)`" 24 | } else if let value = arg as? LosslessStringConvertible { 25 | return String(value) 26 | } else { 27 | fatalError("Error while encoding \(type(of: arg)) for JavaScript: type not yet implemented.") 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/Resources/js/editor/focus.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // MARK: - General 4 | 5 | /** 6 | * Sets focus on the editor. 7 | */ 8 | function focus() { 9 | getEditor().focus(); 10 | } 11 | 12 | /** 13 | * Removes focus from the editor. 14 | */ 15 | function blur() { 16 | getEditor().blur(); 17 | } 18 | 19 | // MARK: - Position caret at a precise point 20 | 21 | /** 22 | * Set the caret at the beginning of the editor. 23 | * The editor must be focused. 24 | */ 25 | function setCaretAtBeginningOfDocument() { 26 | const editor = getEditor(); 27 | setCaretAtElement(editor, 0); 28 | } 29 | 30 | /** 31 | * Set the caret at the end of the editor. 32 | * The editor must be focused. 33 | */ 34 | function setCaretAtEndOfDocument() { 35 | const editor = getEditor(); 36 | setCaretAtElement(editor, editor.childNodes.length); 37 | } 38 | 39 | /** 40 | * Set the caret at the beginning of the queried element. 41 | * The editor must be focused. 42 | * 43 | * @param {string} selector The selector to query the HTML element. 44 | */ 45 | function setCaretAtSelector(selector) { 46 | const element = getEditor().querySelector(selector); 47 | setCaretAtElement(element, 0); 48 | } 49 | 50 | // MARK: - Utils 51 | 52 | function setCaretAtElement(item, offset) { 53 | const range = document.createRange(); 54 | range.setStart(item, offset); 55 | range.collapse(true); 56 | 57 | const selection = window.getSelection(); 58 | selection.removeAllRanges(); 59 | selection.addRange(range); 60 | 61 | setTimeout(computeAndReportCaretPosition, 300); 62 | } 63 | -------------------------------------------------------------------------------- /Examples/Example iOS/Example iOS/UI/Controllers/FixedSizeEditorViewController.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import UIKit 15 | 16 | final class FixedSizeEditorViewController: EditorViewController { 17 | override func setupEditor() { 18 | super.setupEditor() 19 | 20 | editor.isScrollEnabled = true 21 | editor.webView.scrollView.keyboardDismissMode = .interactive 22 | view.addSubview(editor) 23 | 24 | editor.layer.borderColor = UIColor.blue.cgColor 25 | editor.layer.borderWidth = 1 26 | editor.layer.cornerRadius = 8 27 | 28 | NSLayoutConstraint.activate([ 29 | editor.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), 30 | editor.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 200), 31 | editor.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16), 32 | editor.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16) 33 | ]) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Examples/Example iOS/Example iOS/UI/Controllers/EditorViewController/EditorViewController.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import InfomaniakRichHTMLEditor 15 | import UIKit 16 | 17 | class EditorViewController: UIViewController { 18 | var editor: RichHTMLEditorView! 19 | var toolbarButtons = [UIView]() 20 | 21 | var toolbarCurrentColorPicker: ToolbarAction? 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | title = "Infomaniak - RichHTMLEditor (iOS)" 27 | view.backgroundColor = .systemBackground 28 | 29 | setupEditor() 30 | setupToolbar() 31 | } 32 | 33 | func setupEditor() { 34 | createEditor() 35 | } 36 | 37 | func createEditor() { 38 | editor = RichHTMLEditorView() 39 | if let cssURL = Bundle.main.url(forResource: "editor", withExtension: "css"), let css = try? String(contentsOf: cssURL) { 40 | editor.injectAdditionalCSS(css) 41 | } 42 | editor.translatesAutoresizingMaskIntoConstraints = false 43 | editor.delegate = self 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/Extensions/String+Escaped.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | extension String { 15 | /// When we include a string as an argument of a JavaScript function, 16 | /// we must escape it, so it won't be broken. 17 | /// 18 | /// We include our string between backticks so it can includes a wide range of 19 | /// characters, including line breaks. 20 | /// We should escape the following characters: backtick, backslash and dollar. 21 | /// JavaScript documentation: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals 22 | var escapedForJavaScript: String { 23 | var escapedString = "" 24 | for letter in self { 25 | switch letter { 26 | case "\\": 27 | escapedString.append("\\\\") 28 | case "`": 29 | escapedString.append("\\`") 30 | case "$": 31 | escapedString.append("\\$") 32 | default: 33 | escapedString.append(letter) 34 | } 35 | } 36 | 37 | return escapedString 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Examples/Example iOS/Example iOS/Resources/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Examples/Example iOS/Example iOS/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import UIKit 15 | 16 | enum EditorState { 17 | case scrollable 18 | case notScrollable 19 | case fixedSize 20 | } 21 | 22 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 23 | var window: UIWindow? 24 | 25 | // Edit this property to test the different implementations of the editor 26 | var editorState: EditorState = .fixedSize 27 | 28 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 29 | guard let windowScene = (scene as? UIWindowScene) else { return } 30 | 31 | window = UIWindow(windowScene: windowScene) 32 | 33 | let viewController: UIViewController 34 | switch editorState { 35 | case .scrollable: 36 | viewController = ScrollableEditorViewController() 37 | case .notScrollable: 38 | viewController = NotScrollableEditorViewController() 39 | case .fixedSize: 40 | viewController = FixedSizeEditorViewController() 41 | } 42 | 43 | window?.rootViewController = UINavigationController(rootViewController: viewController) 44 | window?.makeKeyAndVisible() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/Models/UITextAttributes.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | #if canImport(UIKit) 15 | import UIKit 16 | #elseif canImport(AppKit) 17 | import AppKit 18 | #endif 19 | 20 | /// This struct contains the current state of the text selected or at the insertion point. 21 | public struct UITextAttributes: Codable, Sendable { 22 | public var hasBold = false 23 | public var hasItalic = false 24 | public var hasUnderline = false 25 | public var hasStrikeThrough = false 26 | public var hasSubscript = false 27 | public var hasSuperscript = false 28 | public var hasOrderedList = false 29 | public var hasUnorderedList = false 30 | 31 | public var hasLink = false 32 | public var textJustification: TextJustification? 33 | 34 | public var fontName = "" 35 | public var fontSize: Int? { 36 | return Int(rawFontSize) 37 | } 38 | 39 | public var foregroundColor: PlatformColor? { 40 | return PlatformColor(rgba: rawForegroundColor) 41 | } 42 | 43 | public var backgroundColor: PlatformColor? { 44 | return PlatformColor(rgba: rawBackgroundColor) 45 | } 46 | 47 | private var rawFontSize = "" 48 | private var rawForegroundColor = "" 49 | private var rawBackgroundColor = "" 50 | } 51 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/Models/UserScript.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import WebKit 15 | 16 | struct UserScript: Sendable { 17 | let name: String 18 | let injectionTime: WKUserScriptInjectionTime 19 | 20 | @MainActor func load(to webView: WKWebView) throws { 21 | try webView.configuration.userContentController.addUserScript( 22 | named: name, 23 | injectionTime: injectionTime, 24 | forMainFrameOnly: true 25 | ) 26 | } 27 | } 28 | 29 | extension UserScript { 30 | static let allCases = [ 31 | // Utils 32 | UserScript(name: "captureLog", injectionTime: .atDocumentStart), 33 | UserScript(name: "javascriptBridge", injectionTime: .atDocumentStart), 34 | UserScript(name: "utils", injectionTime: .atDocumentStart), 35 | 36 | // Editor 37 | UserScript(name: "text-attributes", injectionTime: .atDocumentStart), 38 | UserScript(name: "commands", injectionTime: .atDocumentStart), 39 | UserScript(name: "selection", injectionTime: .atDocumentStart), 40 | UserScript(name: "links", injectionTime: .atDocumentStart), 41 | UserScript(name: "observer", injectionTime: .atDocumentStart), 42 | UserScript(name: "focus", injectionTime: .atDocumentStart), 43 | 44 | // Main 45 | UserScript(name: "main", injectionTime: .atDocumentStart) 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /Examples/Example SwiftUI/Example SwiftUI/Views/RootView.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import SwiftUI 15 | 16 | enum EditorState: String, CaseIterable { 17 | case scrollable = "Scrollable" 18 | case notScrollable = "Not Scrollable" 19 | case fixedSize = "Fixed Size" 20 | } 21 | 22 | struct RootView: View { 23 | @State private var editorState = EditorState.scrollable 24 | 25 | var body: some View { 26 | NavigationStack { 27 | Group { 28 | switch editorState { 29 | case .scrollable: 30 | ScrollableEditorView() 31 | case .notScrollable: 32 | NotScrollableEditorView() 33 | case .fixedSize: 34 | FixedSizeEditorView() 35 | } 36 | } 37 | .navigationTitle("Infomaniak - RichHTMLEditor (SwiftUI)") 38 | .toolbarTitleDisplayMode(.inline) 39 | .toolbar { 40 | ToolbarItem(placement: .topBarTrailing) { 41 | Picker("Switch Editor State", selection: $editorState) { 42 | ForEach(EditorState.allCases, id: \.self) { state in 43 | Text(state.rawValue) 44 | } 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | 52 | #Preview { 53 | RootView() 54 | } 55 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/Extensions/PlateformColor+Extension.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | #if canImport(AppKit) 15 | import AppKit 16 | #elseif canImport(UIKit) 17 | import UIKit 18 | #endif 19 | 20 | extension PlatformColor { 21 | public var hexadecimal: String? { 22 | guard let colorComponents = cgColor.components, colorComponents.count >= 3 else { 23 | return nil 24 | } 25 | 26 | let red = colorComponents[0] * 255 27 | let green = colorComponents[1] * 255 28 | let blue = colorComponents[2] * 255 29 | 30 | return String(format: "#%02lX%02lX%02lX", lroundf(Float(red)), lroundf(Float(green)), lroundf(Float(blue))) 31 | } 32 | 33 | convenience init?(rgba: String) { 34 | let rgbValues = rgba 35 | .trimmingCharacters(in: CharacterSet(charactersIn: "rgba()")) 36 | .split(separator: ",") 37 | .compactMap { Float($0.trimmingCharacters(in: .whitespaces)) } 38 | 39 | guard rgbValues.count >= 3 else { 40 | return nil 41 | } 42 | 43 | let red = CGFloat(rgbValues[0] / 255) 44 | let green = CGFloat(rgbValues[1] / 255) 45 | let blue = CGFloat(rgbValues[2] / 255) 46 | 47 | var alpha: CGFloat = 1 48 | if rgbValues.count >= 4 { 49 | alpha = CGFloat(rgbValues[3] / 255) 50 | } 51 | 52 | self.init(red: red, green: green, blue: blue, alpha: alpha) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/Resources/js/utils/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const DOCUMENT_POSITION_SAME = 0; 4 | 5 | // MARK: - Editor 6 | 7 | function getEditor() { 8 | return document.getElementById("swift-rich-html-editor"); 9 | } 10 | 11 | // MARK: - Current selection 12 | 13 | function getRange() { 14 | const selection = document.getSelection(); 15 | if (selection.rangeCount <= 0) { 16 | return null; 17 | } 18 | return selection.getRangeAt(0); 19 | } 20 | 21 | // MARK: - Check element positions 22 | 23 | function doesElementInteractWithRange(element, range) { 24 | const startPosition = element.compareDocumentPosition(range.startContainer); 25 | const endPosition = element.compareDocumentPosition(range.endContainer); 26 | 27 | const targetPositions = [DOCUMENT_POSITION_SAME, Node.DOCUMENT_POSITION_CONTAINS, Node.DOCUMENT_POSITION_CONTAINED_BY]; 28 | 29 | const doesElementIntersectStart = doesPositionMatchTargets(startPosition, targetPositions); 30 | const doesElementIntersectEnd = doesPositionMatchTargets(endPosition, targetPositions); 31 | const doesElementContainsRange = ( 32 | doesPositionMatchTargets(startPosition, [Node.DOCUMENT_POSITION_PRECEDING]) && 33 | doesPositionMatchTargets(endPosition, [Node.DOCUMENT_POSITION_FOLLOWING]) && 34 | !doesPositionMatchTargets(endPosition, [Node.DOCUMENT_POSITION_CONTAINED_BY]) 35 | ); 36 | return doesElementIntersectStart || doesElementIntersectEnd || doesElementContainsRange; 37 | } 38 | 39 | function doesPositionMatchTargets(position, targets) { 40 | return targets.some(target => { 41 | if (target === DOCUMENT_POSITION_SAME) { 42 | return position === DOCUMENT_POSITION_SAME; 43 | } 44 | return (position & target) === target; 45 | }); 46 | } 47 | 48 | // MARK: - Compare objects 49 | 50 | function compareObjectProperties(lhs, rhs) { 51 | let lhsKeys = Object.keys(lhs); 52 | let rhsKeys = Object.keys(rhs); 53 | 54 | if (lhsKeys.length !== rhsKeys.length) { 55 | return false; 56 | } 57 | 58 | for (const key of lhsKeys) { 59 | if (lhs[key] !== rhs[key]) { 60 | return false; 61 | } 62 | } 63 | 64 | return true; 65 | } 66 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/SwiftUI/Views/RichHTMLEditorCoordinator.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import SwiftUI 15 | 16 | public final class RichHTMLEditorCoordinator: RichHTMLEditorViewDelegate { 17 | private let parent: RichHTMLEditor 18 | 19 | init(parent: RichHTMLEditor) { 20 | self.parent = parent 21 | } 22 | 23 | public func richHTMLEditorViewDidLoad(_ richHTMLEditorView: RichHTMLEditorView) { 24 | parent.onEditorLoaded?() 25 | } 26 | 27 | public func richHTMLEditorViewDidChange(_ richHTMLEditorView: RichHTMLEditorView) { 28 | if parent.html != richHTMLEditorView.html { 29 | parent.html = richHTMLEditorView.html 30 | } 31 | } 32 | 33 | public func richHTMLEditorView(_ richHTMLEditorView: RichHTMLEditorView, caretPositionDidChange caretPosition: CGRect) { 34 | parent.onCaretPositionChange?(caretPosition) 35 | } 36 | 37 | public func richHTMLEditorView( 38 | _ richHTMLEditorView: RichHTMLEditorView, 39 | selectedTextAttributesDidChange textAttributes: UITextAttributes 40 | ) { 41 | parent.textAttributes.update(from: textAttributes) 42 | } 43 | 44 | public func richHTMLEditorView( 45 | _ richHTMLEditorView: RichHTMLEditorView, 46 | javascriptFunctionDidFail javascriptError: any Error, 47 | whileExecutingFunction function: String 48 | ) { 49 | parent.onJavaScriptFunctionFail?(javascriptError, function) 50 | } 51 | 52 | public func richHTMLEditorView(_ richHTMLEditorView: RichHTMLEditorView, shouldHandleLink link: URL) -> Bool { 53 | return parent.handleLinkOpening?(link) ?? false 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Examples/Example iOS/Example iOS/UI/Controllers/NotScrollableViewController.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import UIKit 15 | 16 | final class NotScrollableEditorViewController: EditorViewController { 17 | override func setupEditor() { 18 | super.setupEditor() 19 | 20 | let scrollView = createScrollView() 21 | scrollView.addSubview(editor) 22 | 23 | editor.isScrollEnabled = false 24 | 25 | NSLayoutConstraint.activate([ 26 | editor.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor), 27 | editor.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), 28 | editor.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), 29 | editor.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), 30 | editor.widthAnchor.constraint(equalTo: scrollView.widthAnchor) 31 | ]) 32 | } 33 | 34 | private func createScrollView() -> UIScrollView { 35 | let scrollView = UIScrollView() 36 | scrollView.translatesAutoresizingMaskIntoConstraints = false 37 | scrollView.keyboardDismissMode = .interactive 38 | 39 | view.addSubview(scrollView) 40 | 41 | NSLayoutConstraint.activate([ 42 | scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), 43 | scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), 44 | scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), 45 | scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor) 46 | ]) 47 | 48 | return scrollView 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --acronyms ID,URL,UUID 2 | --allman false 3 | --assetliterals visual-width 4 | --asynccapturing 5 | --beforemarks 6 | --binarygrouping 4,8 7 | --categorymark "MARK: %c" 8 | --classthreshold 0 9 | --closingparen balanced 10 | --closurevoid remove 11 | --commas always 12 | --conflictmarkers reject 13 | --decimalgrouping 3,6 14 | --elseposition same-line 15 | --emptybraces no-space 16 | --enumnamespaces always 17 | --enumthreshold 0 18 | --exponentcase lowercase 19 | --exponentgrouping disabled 20 | --extensionacl on-extension 21 | --extensionlength 0 22 | --extensionmark "MARK: - %t + %c" 23 | --fractiongrouping disabled 24 | --fragment false 25 | --funcattributes preserve 26 | --generictypes 27 | --groupedextension "MARK: %c" 28 | --guardelse auto 29 | --header ignore 30 | --hexgrouping 4,8 31 | --hexliteralcase uppercase 32 | --ifdef no-indent 33 | --importgrouping alpha 34 | --indent 4 35 | --indentcase false 36 | --indentstrings false 37 | --lifecycle 38 | --lineaftermarks true 39 | --linebreaks lf 40 | --markcategories true 41 | --markextensions always 42 | --marktypes always 43 | --maxwidth 130 44 | --modifierorder 45 | --nevertrailing 46 | --nospaceoperators 47 | --nowrapoperators 48 | --octalgrouping 4,8 49 | --operatorfunc spaced 50 | --organizetypes actor,class,enum,struct 51 | --patternlet inline 52 | --ranges spaced 53 | --redundanttype inferred 54 | --self remove 55 | --selfrequired 56 | --semicolons inline 57 | --shortoptionals always 58 | --smarttabs enabled 59 | --someany true 60 | --stripunusedargs unnamed-only 61 | --structthreshold 0 62 | --tabwidth unspecified 63 | --throwcapturing 64 | --trailingclosures 65 | --trimwhitespace always 66 | --typeattributes preserve 67 | --typeblanklines remove 68 | --typemark "MARK: - %t" 69 | --varattributes preserve 70 | --voidtype void 71 | --wraparguments preserve 72 | --wrapcollections preserve 73 | --wrapconditions preserve 74 | --wrapeffects preserve 75 | --wrapenumcases always 76 | --wrapparameters default 77 | --wrapreturntype preserve 78 | --wrapternary default 79 | --wraptypealiases preserve 80 | --xcodeindentation disabled 81 | --yodaswap always 82 | --disable andOperator,opaqueGenericParameters,preferKeyPath,redundantReturn,strongOutlets,trailingCommas,unusedArguments,wrapMultilineStatementBraces 83 | --enable blankLineAfterImports,blankLinesBetweenImports 84 | --exclude DerivedData,Derived,Tuist,Project.swift,Build,.build -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/Resources/js/editor/text-attributes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // MARK: - Variables 4 | 5 | /** Information about the current selection */ 6 | let currentSelectedTextAttributes = {}; 7 | 8 | /** Dictionary of commands that return a boolean state with the function `document.queryCommandState()` */ 9 | const stateCommands = { 10 | hasBold: "bold", 11 | hasItalic: "italic", 12 | hasUnderline: "underline", 13 | hasStrikeThrough: "strikeThrough", 14 | hasSubscript: "subscript", 15 | hasSuperscript: "superscript", 16 | hasOrderedList: "insertOrderedList", 17 | hasUnorderedList: "insertUnorderedList" 18 | }; 19 | /** Dictionary of commands that return a value with the function `document.queryCommandValue()` */ 20 | const valueCommands = { 21 | fontName: "fontName", 22 | rawFontSize: "fontSize", 23 | rawForegroundColor: "foreColor", 24 | rawBackgroundColor: "backColor" 25 | }; 26 | 27 | // MARK: - Compute and report TextAttributes 28 | 29 | function reportSelectedTextAttributesIfNecessary() { 30 | const newSelectedTextAttributes = getSelectedTextAttributes(); 31 | if (compareObjectProperties(currentSelectedTextAttributes, newSelectedTextAttributes)) { 32 | return; 33 | } 34 | 35 | currentSelectedTextAttributes = newSelectedTextAttributes; 36 | reportSelectedTextAttributesDidChange(currentSelectedTextAttributes); 37 | } 38 | 39 | function getSelectedTextAttributes() { 40 | let textAttributes = {}; 41 | getTextAttributesFromStateCommands(textAttributes); 42 | getTextAttributesFromValueCommands(textAttributes); 43 | getTextAttributesFromCustomCommands(textAttributes); 44 | 45 | return textAttributes; 46 | } 47 | 48 | // MARK: - Utils 49 | 50 | function getTextAttributesFromStateCommands(textAttributes) { 51 | for (const command in stateCommands) { 52 | const commandName = stateCommands[command]; 53 | textAttributes[command] = document.queryCommandState(commandName); 54 | } 55 | } 56 | 57 | function getTextAttributesFromValueCommands(textAttributes) { 58 | for (const command in valueCommands) { 59 | const commandName = valueCommands[command]; 60 | textAttributes[command] = document.queryCommandValue(commandName); 61 | } 62 | } 63 | 64 | function getTextAttributesFromCustomCommands(textAttributes) { 65 | textAttributes["hasLink"] = hasLink(); 66 | textAttributes["textJustification"] = computeTextJustification(); 67 | } 68 | 69 | function computeTextJustification() { 70 | const sides = { 71 | "left": "justifyLeft", 72 | "center": "justifyCenter", 73 | "right": "justifyRight", 74 | "full": "justifyFull" 75 | } 76 | 77 | for (const side in sides) { 78 | if (document.queryCommandState(sides[side])) { 79 | return side; 80 | } 81 | } 82 | return null; 83 | } 84 | 85 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/Models/JavaScriptFunction.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | enum JavaScriptFunction: Sendable { 15 | case execCommand(command: String, argument: Sendable? = nil) 16 | case setContent(content: String) 17 | case injectCSS(content: String) 18 | case createLink(url: String, text: String?) 19 | case unlink 20 | case focus 21 | case blur 22 | case setCaretAtBeginningOfDocument 23 | case setCaretAtEndOfDocument 24 | case setCaretAtSelector(selector: String) 25 | 26 | var identifier: String { 27 | switch self { 28 | case .execCommand: 29 | return "execCommand" 30 | case .setContent: 31 | return "setContent" 32 | case .injectCSS: 33 | return "injectCSS" 34 | case .createLink: 35 | return "createLink" 36 | case .unlink: 37 | return "unlink" 38 | case .focus: 39 | return "focus" 40 | case .blur: 41 | return "blur" 42 | case .setCaretAtBeginningOfDocument: 43 | return "setCaretAtBeginningOfDocument" 44 | case .setCaretAtEndOfDocument: 45 | return "setCaretAtEndOfDocument" 46 | case .setCaretAtSelector: 47 | return "setCaretAtSelector" 48 | } 49 | } 50 | 51 | private var args: [Any?] { 52 | switch self { 53 | case .execCommand(let command, let argument): 54 | return [command, argument] 55 | case .setContent(let content): 56 | return [content] 57 | case .injectCSS(let content): 58 | return [content] 59 | case .createLink(let url, let text): 60 | return [url, text] 61 | case .setCaretAtSelector(let selector): 62 | return [selector] 63 | case .unlink, .focus, .blur, .setCaretAtBeginningOfDocument, .setCaretAtEndOfDocument: 64 | return [] 65 | } 66 | } 67 | 68 | func call() -> String { 69 | let formattedArgs = formatArgs(args) 70 | return "\(identifier)(\(formattedArgs));" 71 | } 72 | 73 | private func formatArgs(_ args: [Any?]) -> String { 74 | guard !args.isEmpty else { 75 | return "" 76 | } 77 | 78 | let formattedArgs = args.map { JavaScriptFormatterHelper.format($0) } 79 | return formattedArgs.joined(separator: ", ") 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/Resources/js/editor/links.js: -------------------------------------------------------------------------------- 1 | // MARK: - Detect links 2 | 3 | function hasLink() { 4 | return getAllAnchorsOfSelection().length > 0; 5 | } 6 | 7 | function getAllAnchorsOfSelection() { 8 | const range = getRange(); 9 | if (range === null) { 10 | return []; 11 | } 12 | 13 | const anchorElements = [...getEditor().querySelectorAll("a[href]")]; 14 | return anchorElements.filter(element => doesElementInteractWithRange(element, range)); 15 | } 16 | 17 | function getFirstAnchorOfSelection() { 18 | const anchors = getAllAnchorsOfSelection(); 19 | if (anchors.length <= 0) { 20 | return null; 21 | } 22 | return anchors[0]; 23 | } 24 | 25 | // MARK: - Create and edit links 26 | 27 | function createLink(url, text) { 28 | const range = getRange(); 29 | if (range === null) { 30 | return; 31 | } 32 | 33 | const trimmedText = text.trim(); 34 | const formattedText = trimmedText === "" ? null : trimmedText; 35 | 36 | if (range.collapsed) { 37 | createLinkForCaret(url, formattedText, range); 38 | } else { 39 | createLinkForRange(url, formattedText); 40 | } 41 | } 42 | 43 | function createLinkForCaret(url, text, range) { 44 | let anchor = getFirstAnchorOfSelection(); 45 | if (anchor !== null) { 46 | anchor.href = url; 47 | updateAnchorText(anchor, text); 48 | } else { 49 | anchor = document.createElement("a"); 50 | anchor.textContent = text || url; 51 | anchor.href = url; 52 | range.insertNode(anchor); 53 | } 54 | 55 | setCaretAtEndOfAnchor(anchor); 56 | } 57 | 58 | function createLinkForRange(url, text) { 59 | document.execCommand("createLink", false, url); 60 | 61 | if (text !== null) { 62 | const anchor = getFirstAnchorOfSelection(); 63 | updateAnchorText(anchor, text); 64 | } 65 | } 66 | 67 | // MARK: - Remove link 68 | 69 | function unlink() { 70 | const anchorNodes = getAllAnchorsOfSelection(); 71 | anchorNodes.forEach(unlinkAnchorNode); 72 | } 73 | 74 | function unlinkAnchorNode(anchor) { 75 | const selection = document.getSelection(); 76 | if (selection.rangeCount <= 0) { 77 | return; 78 | } 79 | 80 | const range = selection.getRangeAt(0); 81 | const rangeBackup = range.cloneRange(); 82 | 83 | range.selectNodeContents(anchor); 84 | document.execCommand("unlink"); 85 | 86 | selection.removeAllRanges(); 87 | selection.addRange(rangeBackup); 88 | } 89 | 90 | // MARK: - Utils 91 | 92 | function updateAnchorText(anchor, text) { 93 | if (text !== null && anchor.textContent !== text) { 94 | anchor.textContent = text; 95 | } 96 | } 97 | 98 | function setCaretAtEndOfAnchor(anchor) { 99 | const range = new Range(); 100 | range.setStart(anchor, 1); 101 | range.setEnd(anchor, 1); 102 | range.collapsed = true; 103 | 104 | const selection = document.getSelection(); 105 | selection.removeAllRanges(); 106 | selection.addRange(range); 107 | } 108 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/SwiftUI/Models/TextAttributes.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License") 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import SwiftUI 15 | 16 | /// This object contains the current state of the text selected or at the insertion point. 17 | /// 18 | /// The properties are read-only and are automatically updated by the editor. 19 | /// If you want to update the style, you should use the available functions. 20 | @MainActor 21 | public final class TextAttributes: ObservableObject { 22 | @Published public private(set) var hasBold = false 23 | @Published public private(set) var hasItalic = false 24 | @Published public private(set) var hasUnderline = false 25 | @Published public private(set) var hasStrikethrough = false 26 | @Published public private(set) var hasSubscript = false 27 | @Published public private(set) var hasSuperscript = false 28 | @Published public private(set) var hasOrderedList = false 29 | @Published public private(set) var hasUnorderedList = false 30 | 31 | @Published public private(set) var hasLink = false 32 | @Published public private(set) var textJustification: TextJustification? 33 | 34 | @Published public private(set) var fontName = "" 35 | @Published public private(set) var fontSize: Int? 36 | 37 | @Published public private(set) var foregroundColor: Color? 38 | @Published public private(set) var backgroundColor: Color? 39 | 40 | weak var editor: RichHTMLEditorView? 41 | 42 | public init() {} 43 | 44 | func update(from uiTextAttributes: UITextAttributes) { 45 | hasBold = uiTextAttributes.hasBold 46 | hasItalic = uiTextAttributes.hasItalic 47 | hasUnderline = uiTextAttributes.hasUnderline 48 | hasStrikethrough = uiTextAttributes.hasStrikeThrough 49 | hasSubscript = uiTextAttributes.hasSubscript 50 | hasSuperscript = uiTextAttributes.hasSuperscript 51 | hasOrderedList = uiTextAttributes.hasOrderedList 52 | hasUnorderedList = uiTextAttributes.hasUnorderedList 53 | 54 | hasLink = uiTextAttributes.hasLink 55 | textJustification = uiTextAttributes.textJustification 56 | 57 | fontName = uiTextAttributes.fontName 58 | fontSize = uiTextAttributes.fontSize 59 | 60 | if let uiForegroundColor = uiTextAttributes.foregroundColor { 61 | foregroundColor = Color(uiForegroundColor) 62 | } else { 63 | foregroundColor = nil 64 | } 65 | if let uiBackgroundColor = uiTextAttributes.backgroundColor { 66 | backgroundColor = Color(uiBackgroundColor) 67 | } else { 68 | backgroundColor = nil 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Examples/Example macOS/Example macOS/UI/ViewController.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import Cocoa 15 | import InfomaniakRichHTMLEditor 16 | 17 | final class ViewController: NSViewController { 18 | private var editor: RichHTMLEditorView! 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | 23 | editor = RichHTMLEditorView() 24 | editor.delegate = self 25 | if let cssURL = Bundle.main.url(forResource: "style", withExtension: "css"), 26 | let styleCSS = try? String(contentsOf: cssURL) { 27 | editor.injectAdditionalCSS(styleCSS) 28 | } 29 | editor.translatesAutoresizingMaskIntoConstraints = false 30 | view.addSubview(editor) 31 | 32 | NSLayoutConstraint.activate([ 33 | editor.topAnchor.constraint(equalTo: view.topAnchor), 34 | editor.trailingAnchor.constraint(equalTo: view.trailingAnchor), 35 | editor.bottomAnchor.constraint(equalTo: view.bottomAnchor), 36 | editor.leadingAnchor.constraint(equalTo: view.leadingAnchor) 37 | ]) 38 | 39 | NotificationCenter.default.addObserver(self, selector: #selector(didTapToolbarItem), name: .didTapToolbar, object: nil) 40 | } 41 | 42 | @objc func didTapToolbarItem(notification: Notification) { 43 | guard let item = notification.object as? NSToolbarItem.Identifier else { 44 | return 45 | } 46 | 47 | switch item { 48 | case .bold: 49 | editor.bold() 50 | case .italic: 51 | editor.italic() 52 | case .underline: 53 | editor.underline() 54 | case .addLink: 55 | presentLinkAlert() 56 | default: 57 | print("Action not handled.") 58 | } 59 | } 60 | 61 | private func presentLinkAlert() { 62 | let alert = NSAlert() 63 | alert.alertStyle = .informational 64 | alert.messageText = "Insert a new link" 65 | let validateButton = alert.addButton(withTitle: "Validate") 66 | alert.addButton(withTitle: "Cancel") 67 | 68 | let textField = NSTextField(frame: NSRect(x: 0, y: 0, width: 200, height: 24)) 69 | alert.accessoryView = textField 70 | 71 | let response = alert.runModal() 72 | 73 | guard response.rawValue == validateButton.tag, 74 | !textField.stringValue.isEmpty, 75 | let url = URL(string: textField.stringValue) 76 | else { return } 77 | 78 | editor.addLink(url: url, text: url.absoluteString) 79 | } 80 | } 81 | 82 | // MARK: - RichHTMLEditorViewDelegate 83 | 84 | extension ViewController: RichHTMLEditorViewDelegate { 85 | func richHTMLEditorView(_ richHTMLEditorView: RichHTMLEditorView, shouldHandleLink link: URL) -> Bool { 86 | NSWorkspace.shared.open(link) 87 | return true 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/SwiftUI/Views/RichHTMLEditor+Environment.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import SwiftUI 15 | 16 | // MARK: - Environment Keys 17 | 18 | #if canImport(UIKit) 19 | public struct EditorScrollDisable: EnvironmentKey { 20 | public static let defaultValue = false 21 | } 22 | #endif 23 | 24 | #if canImport(UIKit) && !os(visionOS) 25 | public struct EditorInputAccessoryViewKey: EnvironmentKey { 26 | public static let defaultValue: UIView? = nil 27 | } 28 | #endif 29 | 30 | public struct EditorCSSKey: EnvironmentKey { 31 | public static let defaultValue: String? = nil 32 | } 33 | 34 | public struct OnEditorLoadedKey: EnvironmentKey { 35 | public static let defaultValue: (@Sendable () -> Void)? = nil 36 | } 37 | 38 | public struct OnCaretPositionChangeKey: EnvironmentKey { 39 | public static let defaultValue: (@Sendable (CGRect) -> Void)? = nil 40 | } 41 | 42 | public struct OnJavaScriptFunctionFailKey: EnvironmentKey { 43 | public static let defaultValue: (@Sendable (any Error, String) -> Void)? = nil 44 | } 45 | 46 | public struct IntrospectEditorKey: EnvironmentKey { 47 | public static let defaultValue: (@MainActor (RichHTMLEditorView) -> Void)? = nil 48 | } 49 | 50 | public struct HandleLinkOpeningKey: EnvironmentKey { 51 | public static let defaultValue: (@Sendable (URL) -> Bool)? = nil 52 | } 53 | 54 | // MARK: - Environment Values 55 | 56 | public extension EnvironmentValues { 57 | #if canImport(UIKit) 58 | var editorScrollable: Bool { 59 | get { self[EditorScrollDisable.self] } 60 | set { self[EditorScrollDisable.self] = newValue } 61 | } 62 | #endif 63 | 64 | #if canImport(UIKit) && !os(visionOS) 65 | var editorInputAccessoryView: UIView? { 66 | get { self[EditorInputAccessoryViewKey.self] } 67 | set { self[EditorInputAccessoryViewKey.self] = newValue } 68 | } 69 | #endif 70 | 71 | var editorCSS: String? { 72 | get { self[EditorCSSKey.self] } 73 | set { self[EditorCSSKey.self] = newValue } 74 | } 75 | 76 | var onEditorLoaded: (@Sendable () -> Void)? { 77 | get { self[OnEditorLoadedKey.self] } 78 | set { self[OnEditorLoadedKey.self] = newValue } 79 | } 80 | 81 | var onCaretPositionChange: (@Sendable (CGRect) -> Void)? { 82 | get { self[OnCaretPositionChangeKey.self] } 83 | set { self[OnCaretPositionChangeKey.self] = newValue } 84 | } 85 | 86 | var onJavaScriptFunctionFail: (@Sendable (any Error, String) -> Void)? { 87 | get { self[OnJavaScriptFunctionFailKey.self] } 88 | set { self[OnJavaScriptFunctionFailKey.self] = newValue } 89 | } 90 | 91 | var introspectEditor: (@MainActor (RichHTMLEditorView) -> Void)? { 92 | get { self[IntrospectEditorKey.self] } 93 | set { self[IntrospectEditorKey.self] = newValue } 94 | } 95 | 96 | var handleLinkOpening: (@Sendable (URL) -> Bool)? { 97 | get { self[HandleLinkOpeningKey.self] } 98 | set { self[HandleLinkOpeningKey.self] = newValue } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/WebViewBridge/JavaScriptManager.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import WebKit 15 | 16 | @MainActor 17 | protocol JavaScriptManagerDelegate: AnyObject { 18 | func javascriptFunctionDidFail(error: any Error, function: String) 19 | } 20 | 21 | @MainActor 22 | final class JavaScriptManager { 23 | var isDOMContentLoaded = false { 24 | didSet { 25 | evaluateWaitingFunctions() 26 | } 27 | } 28 | 29 | weak var delegate: JavaScriptManagerDelegate? 30 | 31 | private weak var webView: WKWebView? 32 | private var functionsWaitingForDOM = [JavaScriptFunction]() 33 | 34 | init(webView: WKWebView) { 35 | self.webView = webView 36 | } 37 | 38 | func setHTMLContent(_ content: String) { 39 | let setContent = JavaScriptFunction.setContent(content: content) 40 | evaluateWhenDOMIsReady(function: setContent) 41 | } 42 | 43 | func injectCSS(_ content: String) { 44 | let injectCSS = JavaScriptFunction.injectCSS(content: content) 45 | evaluateWhenDOMIsReady(function: injectCSS) 46 | } 47 | 48 | func execCommand(_ command: ExecCommand, argument: Sendable? = nil) { 49 | let execCommand = JavaScriptFunction.execCommand(command: command.rawValue, argument: argument) 50 | evaluate(function: execCommand) 51 | } 52 | 53 | func addLink(text: String?, path: String) { 54 | let createLink = JavaScriptFunction.createLink(url: path, text: text) 55 | evaluate(function: createLink) 56 | } 57 | 58 | func unlink() { 59 | evaluate(function: .unlink) 60 | } 61 | 62 | func focus() { 63 | evaluate(function: .focus) 64 | } 65 | 66 | func blur() { 67 | evaluate(function: .blur) 68 | } 69 | 70 | func setCaretAtBeginningOfDocument() { 71 | evaluate(function: .setCaretAtEndOfDocument) 72 | } 73 | 74 | func setCaretAtEndOfDocument() { 75 | evaluate(function: .setCaretAtEndOfDocument) 76 | } 77 | 78 | func setCaretAtSelector(selector: String) { 79 | evaluate(function: .setCaretAtSelector(selector: selector)) 80 | } 81 | 82 | private func evaluateWaitingFunctions() { 83 | guard isDOMContentLoaded else { 84 | return 85 | } 86 | 87 | for function in functionsWaitingForDOM { 88 | evaluate(function: function) 89 | } 90 | functionsWaitingForDOM.removeAll() 91 | } 92 | 93 | private func evaluateWhenDOMIsReady(function: JavaScriptFunction) { 94 | guard isDOMContentLoaded else { 95 | functionsWaitingForDOM.append(function) 96 | return 97 | } 98 | evaluate(function: function) 99 | } 100 | 101 | private func evaluate(function: JavaScriptFunction) { 102 | webView?.evaluateJavaScript(function.call()) { [weak self] _, error in 103 | if let error { 104 | self?.delegate?.javascriptFunctionDidFail(error: error, function: function.identifier) 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Examples/Example macOS/Example macOS/UI/WindowController.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import AppKit 15 | 16 | extension NSToolbarItem.Identifier { 17 | static let bold = NSToolbarItem.Identifier(rawValue: "Bold") 18 | static let italic = NSToolbarItem.Identifier(rawValue: "Italic") 19 | static let underline = NSToolbarItem.Identifier(rawValue: "Underline") 20 | static let addLink = NSToolbarItem.Identifier(rawValue: "AddLink") 21 | } 22 | 23 | extension Notification.Name { 24 | static let didTapToolbar = Notification.Name("didTapToolbar") 25 | } 26 | 27 | final class WindowController: NSWindowController { 28 | @IBOutlet weak var toolbar: NSToolbar! 29 | 30 | override func windowDidLoad() { 31 | super.windowDidLoad() 32 | 33 | toolbar.allowsUserCustomization = true 34 | toolbar.autosavesConfiguration = true 35 | toolbar.displayMode = .iconOnly 36 | } 37 | } 38 | 39 | extension WindowController: NSToolbarDelegate { 40 | func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { 41 | return [ 42 | NSToolbarItem.Identifier.bold, 43 | NSToolbarItem.Identifier.italic, 44 | NSToolbarItem.Identifier.underline, 45 | NSToolbarItem.Identifier.addLink 46 | ] 47 | } 48 | 49 | func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { 50 | return [ 51 | NSToolbarItem.Identifier.flexibleSpace, 52 | NSToolbarItem.Identifier.bold, 53 | NSToolbarItem.Identifier.italic, 54 | NSToolbarItem.Identifier.underline, 55 | NSToolbarItem.Identifier.addLink 56 | ] 57 | } 58 | 59 | func toolbar( 60 | _ toolbar: NSToolbar, 61 | itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, 62 | willBeInsertedIntoToolbar flag: Bool 63 | ) -> NSToolbarItem? { 64 | switch itemIdentifier { 65 | case .bold: 66 | return createToolbarItem(itemIdentifier: itemIdentifier, image: "bold", label: "Bold") 67 | case .italic: 68 | return createToolbarItem(itemIdentifier: itemIdentifier, image: "italic", label: "Italic") 69 | case .underline: 70 | return createToolbarItem(itemIdentifier: itemIdentifier, image: "underline", label: "Underline") 71 | case .addLink: 72 | return createToolbarItem(itemIdentifier: itemIdentifier, image: "link", label: "Link") 73 | default: 74 | return nil 75 | } 76 | } 77 | 78 | private func createToolbarItem(itemIdentifier: NSToolbarItem.Identifier, image: String, label: String) -> NSToolbarItem { 79 | let toolbarItem = NSToolbarItem(itemIdentifier: itemIdentifier) 80 | toolbarItem.isBordered = true 81 | 82 | toolbarItem.image = NSImage(systemSymbolName: image, accessibilityDescription: nil) 83 | toolbarItem.label = label 84 | toolbarItem.paletteLabel = label 85 | toolbarItem.toolTip = label 86 | 87 | toolbarItem.target = self 88 | toolbarItem.action = #selector(didTapToolbarItem) 89 | 90 | return toolbarItem 91 | } 92 | 93 | @objc func didTapToolbarItem(sender: NSToolbarItem) { 94 | NotificationCenter.default.post(name: .didTapToolbar, object: sender.itemIdentifier) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/Resources/js/editor/selection.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let lastSelectionRange = null; 4 | let lastFocusedSelectionGrabber = null; 5 | 6 | // MARK: - Compute caret position 7 | 8 | function computeAndReportCaretPosition() { 9 | const caretRect = computeCaretRect(); 10 | if (caretRect == null) { 11 | return; 12 | } 13 | 14 | reportCaretPositionDidChange(caretRect); 15 | } 16 | 17 | function computeCaretRect() { 18 | const selection = window.getSelection(); 19 | if (selection.rangeCount <= 0) { 20 | return null; 21 | } 22 | 23 | let caretRect = null; 24 | if (selection.isCollapsed) { 25 | caretRect = getCaretRect(); 26 | } else { 27 | const selectionNodeToFocus = getSelectionNodeToTarget(selection); 28 | lastFocusedSelectionGrabber = selectionNodeToFocus; 29 | 30 | caretRect = (selectionNodeToFocus == null) ? null : getCaretRect(selectionNodeToFocus); 31 | } 32 | lastSelectionRange = selection.getRangeAt(0).cloneRange(); 33 | 34 | return caretRect; 35 | } 36 | 37 | // MARK: - Utils 38 | 39 | const SelectionGrabber = { 40 | start: "Start", 41 | end: "End", 42 | unknown: "Unknown" 43 | }; 44 | 45 | function getCaretRect(anchorNode) { 46 | const range = getRange()?.cloneRange(); 47 | if (range == null) { 48 | return null; 49 | } 50 | 51 | if (anchorNode != undefined) { 52 | range.selectNodeContents(anchorNode); 53 | } 54 | 55 | const rangeRects = range.getClientRects(); 56 | switch (rangeRects.length) { 57 | case 0: 58 | const node = anchorNode || window.getSelection().anchorNode; 59 | const closestParentElement = getClosestParentNodeElement(node); 60 | return closestParentElement.getBoundingClientRect(); 61 | case 1: 62 | return rangeRects[0]; 63 | default: 64 | return range.getBoundingClientRect(); 65 | } 66 | } 67 | 68 | function getClosestParentNodeElement(node) { 69 | if (node == null) { 70 | return null; 71 | } else if (node.nodeType === Node.ELEMENT_NODE) { 72 | return node; 73 | } else { 74 | return getClosestParentNodeElement(node.parentNode); 75 | } 76 | } 77 | 78 | function getSelectionNodeToTarget(selection) { 79 | const movingGrabber = guessMostProbableMovingSelectionGrabber(selection.getRangeAt(0).cloneRange()); 80 | 81 | let selectionNodeToFocus = null; 82 | switch (movingGrabber) { 83 | case SelectionGrabber.start: 84 | selectionNodeToFocus = selection.anchorNode; 85 | break; 86 | case SelectionGrabber.end: 87 | selectionNodeToFocus = selection.focusNode; 88 | break; 89 | case SelectionGrabber.unknown: 90 | if (lastFocusedSelectionGrabber != null) { 91 | selectionNodeToFocus = lastFocusedSelectionGrabber; 92 | } 93 | break; 94 | } 95 | 96 | return selectionNodeToFocus; 97 | } 98 | 99 | function guessMostProbableMovingSelectionGrabber(selectionRange) { 100 | if (lastSelectionRange == null) { 101 | return SelectionGrabber.unknown; 102 | } 103 | 104 | if (lastSelectionRange.startContainer === selectionRange.startContainer && lastSelectionRange.endContainer === selectionRange.endContainer) { 105 | if (lastSelectionRange.startOffset === selectionRange.startOffset && lastSelectionRange.endOffset === selectionRange.endOffset) { 106 | return SelectionGrabber.unknown; 107 | } else { 108 | return (lastSelectionRange.endOffset !== selectionRange.endOffset) ? SelectionGrabber.end : SelectionGrabber.start; 109 | } 110 | } else { 111 | return (lastSelectionRange.endContainer !== selectionRange.endContainer) ? SelectionGrabber.end : SelectionGrabber.start; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/SwiftUI/Views/RichEditor+Modifier.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import SwiftUI 15 | 16 | public extension View { 17 | #if canImport(UIKit) 18 | /// Configures whether the editor can use its inner scrollview. 19 | /// 20 | /// - Parameter scrollable: A Boolean that indicates whether scrolling is enabled or not. 21 | /// 22 | /// - Returns: A view that disables or enables the inner scroll of the `RichHTMLEditor` view. 23 | func editorScrollable(_ scrollable: Bool) -> some View { 24 | environment(\.editorScrollable, scrollable) 25 | } 26 | #endif 27 | 28 | #if canImport(UIKit) && !os(visionOS) 29 | /// Appends the specified view as an input accessory view to the editor. 30 | /// 31 | /// - Parameter inputAccessoryView: A UIView to use as an input accessory 32 | /// view. 33 | /// 34 | /// - Returns: A view that uses the specified view as an input accessory view. 35 | func editorInputAccessoryView(_ inputAccessoryView: UIView?) -> some View { 36 | environment(\.editorInputAccessoryView, inputAccessoryView) 37 | } 38 | #endif 39 | 40 | func editorCSS(_ css: String) -> some View { 41 | environment(\.editorCSS, css) 42 | } 43 | 44 | /// Performs an action when the editor is loaded. 45 | /// 46 | /// - Parameter action: A closure to run when the editor is loaded. The closure 47 | /// takes a `newValue` parameter that indicates the updated value. 48 | /// 49 | /// - Returns: A view that fires an action when the editor is loaded. 50 | func onEditorLoaded(perform action: @escaping @Sendable () -> Void) -> some View { 51 | environment(\.onEditorLoaded, action) 52 | } 53 | 54 | /// Performs an action when the position of the caret in the editor changes. 55 | /// 56 | /// - Parameters action: A closure to run when the caret moves. The closure 57 | /// takes a `newPosition` parameter that indicates the updated position. 58 | /// 59 | /// - Returns: A view that fires an action when the position of the caret changes. 60 | func onCaretPositionChange(perform action: @escaping @Sendable (_ newPosition: CGRect) -> Void) -> some View { 61 | environment(\.onCaretPositionChange, action) 62 | } 63 | 64 | /// Performs an action when a JavaScript function executed by the editor fails. 65 | /// 66 | /// - Parameter action: A closure to run when a JavaScript function fails. 67 | /// 68 | /// - Returns: A view that fires an action when a JavaScript function fails. 69 | func onJavaScriptFunctionFail(perform action: @escaping @Sendable (any Error, String) -> Void) -> some View { 70 | environment(\.onJavaScriptFunctionFail, action) 71 | } 72 | 73 | /// Performs an action when the editor is initialized, to customize the underlying ``RichHTMLEditorView``. 74 | /// 75 | /// - Parameter action: A closure to run when the view is initialized, to customize 76 | /// the editor. 77 | /// 78 | /// - Returns: A view with the customizations applied to editor. 79 | func introspectEditor(perform action: @MainActor @escaping (RichHTMLEditorView) -> Void) -> some View { 80 | environment(\.introspectEditor, action) 81 | } 82 | 83 | /// Tells the editor whether to handle the opening a link or perform an action to open it. 84 | /// 85 | /// The default behavior is to let the editor handle opening links. 86 | /// 87 | /// - Parameter action: A closure to run when the editor tries to open a link. The closure 88 | /// should return `false` if the editor should handle the opening, `true` if you intend to manage 89 | /// this task yourself. 90 | /// 91 | /// - Returns: A view with the customizations applied to editor. 92 | func handleLinkOpening(perform action: @escaping @Sendable (URL) -> Bool) -> some View { 93 | environment(\.handleLinkOpening, action) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/WebViewBridge/ScriptMessageHandler.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import OSLog 15 | import WebKit 16 | 17 | @MainActor 18 | protocol ScriptMessageHandlerDelegate: AnyObject { 19 | func editorDidLoad() 20 | func contentDidChange(_ text: String) 21 | func contentHeightDidChange(_ contentHeight: CGFloat) 22 | func selectedTextAttributesDidChange(_ selectedTextAttributes: UITextAttributes?) 23 | func caretPositionDidChange(_ caretRect: CGRect) 24 | } 25 | 26 | final class ScriptMessageHandler: NSObject, WKScriptMessageHandler { 27 | enum ScriptMessage: String, CaseIterable { 28 | case editorDidLoad 29 | case contentDidChange 30 | case contentHeightDidChange 31 | case caretPositionDidChange 32 | case selectedTextAttributesDidChange 33 | case scriptLog 34 | } 35 | 36 | weak var delegate: ScriptMessageHandlerDelegate? 37 | 38 | private let logger = Logger(subsystem: Constants.packageID, category: "ScriptMessageHandler") 39 | 40 | func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { 41 | guard let messageName = ScriptMessage(rawValue: message.name) else { 42 | return 43 | } 44 | 45 | switch messageName { 46 | case .editorDidLoad: 47 | editorDidLoad() 48 | case .contentDidChange: 49 | contentDidChange(message) 50 | case .contentHeightDidChange: 51 | contentHeightDidChange(message) 52 | case .selectedTextAttributesDidChange: 53 | selectedTextAttributesDidChange(message) 54 | case .caretPositionDidChange: 55 | caretPositionDidChange(message) 56 | case .scriptLog: 57 | scriptLog(message) 58 | } 59 | } 60 | 61 | private func editorDidLoad() { 62 | delegate?.editorDidLoad() 63 | } 64 | 65 | private func contentDidChange(_ message: WKScriptMessage) { 66 | guard let newContent = message.body as? String else { 67 | return 68 | } 69 | delegate?.contentDidChange(newContent) 70 | } 71 | 72 | private func contentHeightDidChange(_ message: WKScriptMessage) { 73 | guard let height = message.body as? CGFloat else { 74 | return 75 | } 76 | delegate?.contentHeightDidChange(height) 77 | } 78 | 79 | private func selectedTextAttributesDidChange(_ message: WKScriptMessage) { 80 | guard let json = message.body as? String, let data = json.data(using: .utf8) else { 81 | return 82 | } 83 | 84 | do { 85 | let decoder = JSONDecoder() 86 | let selectedTextAttributes = try decoder.decode(UITextAttributes.self, from: data) 87 | 88 | delegate?.selectedTextAttributesDidChange(selectedTextAttributes) 89 | } catch { 90 | logger.error("Error while trying to decode TextAttributes: \(error)") 91 | delegate?.selectedTextAttributesDidChange(nil) 92 | } 93 | } 94 | 95 | private func caretPositionDidChange(_ message: WKScriptMessage) { 96 | guard let caretData = message.body as? [Double], caretData.count >= 4 else { 97 | return 98 | } 99 | 100 | // Sometimes, the JavaScript function returns a width and height equal to 0 101 | let caretPosition = CGRect( 102 | x: caretData[0], 103 | y: caretData[1], 104 | width: max(1, caretData[2]), 105 | height: max(1, caretData[3]) 106 | ) 107 | delegate?.caretPositionDidChange(caretPosition) 108 | } 109 | 110 | private func scriptLog(_ message: WKScriptMessage) { 111 | guard let log = message.body as? String else { 112 | return 113 | } 114 | logger.info("[ScriptLog] \(log)") 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Examples/Example iOS/Example iOS/Model/ToolbarAction.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import InfomaniakRichHTMLEditor 15 | import UIKit 16 | 17 | enum ToolbarAction: Int { 18 | case dismissKeyboard, bold, italic, underline, strikethrough, link, toggleSubscript, toggleSuperscript, orderedList, 19 | unorderedList, justifyFull, justifyLeft, justifyCenter, justifyRight, fontName, fontSize, foregroundColor, 20 | backgroundColor, outdent, indent, undo, redo, removeFormat 21 | 22 | static let actionGroups: [[Self]] = [ 23 | [.dismissKeyboard], 24 | [.bold, .italic, .underline, .strikethrough], 25 | [.link], 26 | [.toggleSubscript, .toggleSuperscript], 27 | [.orderedList, .unorderedList], 28 | [.justifyFull, .justifyLeft, .justifyCenter, .justifyRight], 29 | [.fontName, .fontSize], 30 | [.foregroundColor, .backgroundColor], 31 | [.outdent, .indent], 32 | [.undo, .redo], 33 | [.removeFormat] 34 | ] 35 | 36 | var icon: UIImage? { 37 | let systemName = switch self { 38 | case .dismissKeyboard: 39 | "keyboard.chevron.compact.down" 40 | case .bold: 41 | "bold" 42 | case .italic: 43 | "italic" 44 | case .underline: 45 | "underline" 46 | case .strikethrough: 47 | "strikethrough" 48 | case .link: 49 | "link" 50 | case .toggleSubscript: 51 | "textformat.subscript" 52 | case .toggleSuperscript: 53 | "textformat.superscript" 54 | case .orderedList: 55 | "list.number" 56 | case .unorderedList: 57 | "list.star" 58 | case .justifyFull: 59 | "text.justify" 60 | case .justifyLeft: 61 | "text.justify.left" 62 | case .justifyCenter: 63 | "text.aligncenter" 64 | case .justifyRight: 65 | "text.justify.right" 66 | case .fontName: 67 | "textformat.alt" 68 | case .fontSize: 69 | "textformat.size" 70 | case .foregroundColor: 71 | "scribble.variable" 72 | case .backgroundColor: 73 | "paintbrush" 74 | case .outdent: 75 | "decrease.indent" 76 | case .indent: 77 | "increase.indent" 78 | case .undo: 79 | "arrow.uturn.backward" 80 | case .redo: 81 | "arrow.uturn.forward" 82 | case .removeFormat: 83 | "xmark.circle" 84 | } 85 | 86 | return UIImage(systemName: systemName) 87 | } 88 | 89 | func isSelected(_ textAttributes: UITextAttributes) -> Bool { 90 | switch self { 91 | case .bold: 92 | return textAttributes.hasBold 93 | case .italic: 94 | return textAttributes.hasItalic 95 | case .underline: 96 | return textAttributes.hasUnderline 97 | case .strikethrough: 98 | return textAttributes.hasStrikeThrough 99 | case .link: 100 | return textAttributes.hasLink 101 | case .toggleSubscript: 102 | return textAttributes.hasSubscript 103 | case .toggleSuperscript: 104 | return textAttributes.hasSuperscript 105 | case .orderedList: 106 | return textAttributes.hasOrderedList 107 | case .unorderedList: 108 | return textAttributes.hasUnorderedList 109 | case .justifyFull: 110 | return textAttributes.textJustification == .full 111 | case .justifyLeft: 112 | return textAttributes.textJustification == .left 113 | case .justifyCenter: 114 | return textAttributes.textJustification == .center 115 | case .justifyRight: 116 | return textAttributes.textJustification == .right 117 | case .dismissKeyboard, .fontName, .fontSize, .foregroundColor, .backgroundColor, .outdent, .indent, .undo, .redo, 118 | .removeFormat: 119 | return false 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/SwiftUI/Views/RichHTMLEditor.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import SwiftUI 15 | 16 | #if canImport(UIKit) 17 | public typealias PlateformViewRepresentable = UIViewRepresentable 18 | #elseif canImport(AppKit) 19 | public typealias PlateformViewRepresentable = NSViewRepresentable 20 | #endif 21 | 22 | public struct RichHTMLEditor: PlateformViewRepresentable { 23 | #if canImport(UIKit) 24 | @Environment(\.editorScrollable) private var isEditorScrollable 25 | #endif 26 | #if canImport(UIKit) && !os(visionOS) 27 | @Environment(\.editorInputAccessoryView) private var editorInputAccessoryView 28 | #endif 29 | 30 | @Environment(\.editorCSS) var editorCSS 31 | @Environment(\.onEditorLoaded) var onEditorLoaded 32 | @Environment(\.onCaretPositionChange) var onCaretPositionChange 33 | @Environment(\.onJavaScriptFunctionFail) var onJavaScriptFunctionFail 34 | @Environment(\.introspectEditor) var introspectEditor 35 | @Environment(\.handleLinkOpening) var handleLinkOpening 36 | 37 | @Binding public var html: String 38 | @ObservedObject public var textAttributes: TextAttributes 39 | 40 | public init(html: Binding, textAttributes: TextAttributes) { 41 | _html = html 42 | _textAttributes = ObservedObject(wrappedValue: textAttributes) 43 | } 44 | 45 | // MARK: - Platform functions 46 | 47 | private func createPlatformView(context: Context) -> RichHTMLEditorView { 48 | let richHTMLEditorView = RichHTMLEditorView() 49 | richHTMLEditorView.delegate = context.coordinator 50 | richHTMLEditorView.html = html 51 | 52 | if let css = editorCSS { 53 | richHTMLEditorView.injectAdditionalCSS(css) 54 | } 55 | introspectEditor?(richHTMLEditorView) 56 | 57 | textAttributes.editor = richHTMLEditorView 58 | 59 | return richHTMLEditorView 60 | } 61 | 62 | private func updatePlatformView(_ richHTMLEditorView: RichHTMLEditorView) { 63 | if richHTMLEditorView.html != html { 64 | richHTMLEditorView.html = html 65 | } 66 | 67 | #if canImport(UIKit) 68 | if richHTMLEditorView.isScrollEnabled != isEditorScrollable { 69 | richHTMLEditorView.isScrollEnabled = isEditorScrollable 70 | } 71 | #endif 72 | #if canImport(UIKit) && !os(visionOS) 73 | if richHTMLEditorView.inputAccessoryView != editorInputAccessoryView { 74 | richHTMLEditorView.inputAccessoryView = editorInputAccessoryView 75 | } 76 | #endif 77 | } 78 | 79 | @available(iOS 16.0, macOS 13.0, *) 80 | private func sizeThatFits(_ proposal: ProposedViewSize, editor: RichHTMLEditorView, context: Context) -> CGSize? { 81 | return proposal.replacingUnspecifiedDimensions(by: editor.intrinsicContentSize) 82 | } 83 | 84 | public func makeCoordinator() -> RichHTMLEditorCoordinator { 85 | return RichHTMLEditorCoordinator(parent: self) 86 | } 87 | 88 | // MARK: - UIView 89 | 90 | public func makeUIView(context: Context) -> RichHTMLEditorView { 91 | return createPlatformView(context: context) 92 | } 93 | 94 | public func updateUIView(_ richHTMLEditorView: RichHTMLEditorView, context: Context) { 95 | updatePlatformView(richHTMLEditorView) 96 | } 97 | 98 | @available(iOS 16.0, macOS 13.0, *) 99 | public func sizeThatFits(_ proposal: ProposedViewSize, uiView: RichHTMLEditorView, context: Context) -> CGSize? { 100 | return sizeThatFits(proposal, editor: uiView, context: context) 101 | } 102 | 103 | // MARK: - NSView 104 | 105 | public func makeNSView(context: Context) -> RichHTMLEditorView { 106 | return createPlatformView(context: context) 107 | } 108 | 109 | public func updateNSView(_ richHTMLEditorView: RichHTMLEditorView, context: Context) { 110 | updatePlatformView(richHTMLEditorView) 111 | } 112 | 113 | @available(iOS 16.0, macOS 13.0, visionOS 1.0, *) 114 | public func sizeThatFits(_ proposal: ProposedViewSize, nsView: RichHTMLEditorView, context: Context) -> CGSize? { 115 | return sizeThatFits(proposal, editor: nsView, context: context) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/SwiftUI/Models/TextAttributes+Commands.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License") 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import Foundation 15 | 16 | public extension TextAttributes { 17 | /// Removes all formatting from the current selection. 18 | func removeFormat() { 19 | editor?.removeFormat() 20 | } 21 | 22 | /// Toggles bold for the current selection or at the insertion point. 23 | func bold() { 24 | editor?.bold() 25 | } 26 | 27 | /// Toggles italic for the current selection or at the insertion point. 28 | func italic() { 29 | editor?.italic() 30 | } 31 | 32 | /// Toggles underline for the current selection or at the insertion point. 33 | func underline() { 34 | editor?.underline() 35 | } 36 | 37 | /// Toggles strikethrough for the current selection or at the insertion point. 38 | func strikethrough() { 39 | editor?.strikethrough() 40 | } 41 | 42 | /// Toggles subscript for the current selection or at the insertion point. 43 | func toggleSubscript() { 44 | editor?.toggleSubscript() 45 | } 46 | 47 | /// Toggles superscript for the current selection or at the insertion point. 48 | func toggleSuperscript() { 49 | editor?.toggleSuperscript() 50 | } 51 | 52 | /// Creates or remove a numbered ordered list for the current selection or at the insertion point. 53 | func orderedList() { 54 | editor?.orderedList() 55 | } 56 | 57 | /// Creates or remove a bulleted unordered list for the current selection or at the insertion point. 58 | func unorderedList() { 59 | editor?.unorderedList() 60 | } 61 | 62 | /// Creates a new link for the current selection or at the insertion point. 63 | /// 64 | /// - Parameters: 65 | /// - url: The destination of the link, it is the value of the `href` attribute. 66 | /// - text: The optional label of the link, if nil the url will be used. 67 | func addLink(url: URL, text: String? = nil) { 68 | editor?.addLink(url: url, text: text) 69 | } 70 | 71 | /// Removes all the links of the current selection or at the insertion point. 72 | func unlink() { 73 | editor?.unlink() 74 | } 75 | 76 | /// Indents the lines containing the current selection or the insertion point. 77 | func indent() { 78 | editor?.indent() 79 | } 80 | 81 | /// Outdents the lines containing the current selection or the insertion point. 82 | func outdent() { 83 | editor?.outdent() 84 | } 85 | 86 | /// Justifies the selection or the insertion point to the `RECommandJustifySide` side. 87 | /// 88 | /// - Parameter side: The side to align the text to. 89 | func justify(_ side: TextJustification) { 90 | editor?.justify(side) 91 | } 92 | 93 | /// Sets the font for the selection or at the insertion point. 94 | /// The font must be available on the platform, you can refer to https://developer.apple.com/fonts/system-fonts/ 95 | /// 96 | /// - Parameter name: Font name. 97 | func setFontName(_ name: String) { 98 | editor?.setFontName(name) 99 | } 100 | 101 | /// Sets the foreground color for the selection or at the insertion point. 102 | /// 103 | /// - Parameter color: The color of the foreground. 104 | func setForegroundColor(_ color: PlatformColor) { 105 | editor?.setForegroundColor(color) 106 | } 107 | 108 | /// Sets the background color for the selection or at the insertion point. 109 | /// 110 | /// - Parameter color: The color of the background. 111 | func setBackgroundColor(_ color: PlatformColor) { 112 | editor?.setBackgroundColor(color) 113 | } 114 | 115 | /// Changes the font size for the selection or at the insertion point. 116 | /// 117 | /// - Parameter size: The size should be included in the interval [1-7]. 118 | func setFontSize(_ size: Int) { 119 | editor?.setFontSize(size) 120 | } 121 | 122 | /// Undoes the last executed command. 123 | func undo() { 124 | editor?.undo() 125 | } 126 | 127 | /// Redoes the last executed command. 128 | func redo() { 129 | editor?.redo() 130 | } 131 | 132 | /// Position the caret at a precise position. 133 | /// 134 | /// - Parameter position: The position where the caret should be placed. 135 | func setCaretAt(_ position: CaretPosition) { 136 | editor?.setCaretAt(position) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Infomaniak Rich HTML Editor 2 | 3 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FInfomaniak%2Fswift-rich-html-editor%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/Infomaniak/swift-rich-html-editor) 4 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FInfomaniak%2Fswift-rich-html-editor%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/Infomaniak/swift-rich-html-editor) 5 | 6 | The **Infomaniak Rich HTML Editor** is a powerful Swift package designed to provide a seamless WYSIWYG (What You See Is What You Get) text editing experience across iOS, macOS, and visionOS platforms. Leveraging the power of the `contenteditable` HTML attribute, this editor allows you to effortlessly edit HTML content. 7 | 8 | Built with **UIKit** and **AppKit** thanks to WebKit, it also includes a **SwiftUI** port, making it easy to integrate into modern Swift apps. 9 | 10 | 11 | 12 | 15 | 18 | 21 | 22 |
13 | iOS sample app 14 | 16 | iOS sample app 17 | 19 | iOS sample app 20 |
23 | 24 | Looking for an Android equivalent? Check out the Kotlin version of the editor here: [Infomaniak/android-rich-html-editor](https://github.com/Infomaniak/android-rich-html-editor) 25 | 26 | ## ✍️ About 27 | 28 | ### Features 29 | 30 | - **HTML Content Editing**: Full support for viewing and editing HTML content directly. 31 | - **Wide range of commands**: Many commands are available to format text, from simple commands like bold to more advanced ones like link creation. 32 | - **Cross-Platform Support:** Compatible with iOS, macOS, and visionOS. 33 | - **SwiftUI API**: A dedicated port for SwiftUI, ensuring modern and declarative UI design compatibility. 34 | 35 | ### Installation 36 | 37 | You can install the package via Swift Package Manager. Add the following line to your Package.swift file: 38 | ```swift 39 | .package(url: "https://github.com/Infomaniak/swift-rich-html-editor.git", from: "1.0.0") 40 | ``` 41 | 42 | ### Usage 43 | 44 | #### UIKit and AppKit 45 | 46 | You can create the editor view and then add it to the view hierarchy. 47 | ```swift 48 | import InfomaniakRichHTMLEditor 49 | import UIKit 50 | 51 | let editor = RichHTMLEditorView() 52 | view.addSubview(editor) 53 | ``` 54 | 55 | To respond to editor's events, you can conform to `RichHTMLEditorViewDelegate`. 56 | 57 | #### SwiftUI 58 | 59 | The SwiftUI view is called `RichHTMLEditor` and takes two arguments: 60 | - `html: Binding` the HTML content of the editor 61 | - `textAttributes: TextAttributes` the objects that contains the current state selected text (or the text at the insertion point) and is responsible to update the style 62 | 63 | ```swift 64 | import InfomaniakRichHTMLEditor 65 | import SwiftUI 66 | 67 | struct ContentView: View { 68 | @State private var html = "" 69 | @StateObject private var textAttributes = TextAttributes() 70 | 71 | var body: some View { 72 | RichHTMLEditor(html: $html, textAttributes: textAttributes) 73 | } 74 | } 75 | ``` 76 | 77 | The object `TextAttributes` contains various attributes about the current style of the selected text. Theses properties are read-only and are automatically updated by the editor. 78 | To update the style, you should call the corresponding functions such as `bold()`. 79 | 80 | Many modifiers are available to customize the editor and respond to editor's events. 81 | Here is a non-exhaustive list of modifiers: 82 | ```swift 83 | RichHTMLEditor(html: $html, textAttributes: textAttributes) 84 | .editorScrollable(true) 85 | .editorInputAccessoryView(myToolbarView) 86 | .editorCSS("h1 { foreground-color: red; }") 87 | .onEditorLoaded { 88 | // Perform action when editor is loaded 89 | } 90 | .onCaretPositionChange { newPosition in 91 | // Perform action when caret moves 92 | } 93 | .onJavaScriptFunctionFail { error, function in 94 | // Perform action when an editor JavaScript function has failed 95 | } 96 | .introspectEditor { richEditorView in 97 | // Perform action on the editor (UI|NS)View 98 | } 99 | ``` 100 | 101 | ### Customize the editor 102 | 103 | You can customize the editor with CSS. 104 | To target the editor, you should use the `#swift-rich-html-editor` selector. 105 | 106 | For example: 107 | ```css 108 | #swift-rich-html-editor { 109 | padding: 16px; 110 | } 111 | ``` 112 | 113 | ## 📖 Documentation 114 | 115 | Public types are documented, and three sample projects are available to help you implement the editor. 116 | 117 | ## 🔍 Sample Projects 118 | 119 | You can find 3 sample projects in the [Examples](Examples) folder: 120 | - A [project built with UIKit](Examples/Example%20iOS/) for iOS 121 | - A [project built with AppKit](Examples/Example%20macOS/) for macOS 122 | - A [project built with SwiftUI](Examples/Example%20SwiftUI/) for iOS/macOS/visionOS 123 | 124 | ## 📱 Apps using InfomaniakRichHTMLEditor 125 | 126 | 127 | Find App 128 | 129 | 130 | [Infomaniak Mail](https://github.com/Infomaniak/ios-kMail) allows you to manage your Infomaniak addresses in a completely secure environment. 131 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/RichHTMLEditorView+Commands.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import Foundation 15 | 16 | // MARK: - Editor Commands 17 | 18 | public extension RichHTMLEditorView { 19 | /// Removes all formatting from the current selection. 20 | func removeFormat() { 21 | execCommand(.removeFormat) 22 | } 23 | 24 | /// Toggles bold for the current selection or at the insertion point. 25 | func bold() { 26 | execCommand(.bold) 27 | } 28 | 29 | /// Toggles italic for the current selection or at the insertion point. 30 | func italic() { 31 | execCommand(.italic) 32 | } 33 | 34 | /// Toggles underline for the current selection or at the insertion point. 35 | func underline() { 36 | execCommand(.underline) 37 | } 38 | 39 | /// Toggles strikethrough for the current selection or at the insertion point. 40 | func strikethrough() { 41 | execCommand(.strikeThrough) 42 | } 43 | 44 | /// Toggles subscript for the current selection or at the insertion point. 45 | func toggleSubscript() { 46 | execCommand(.toggleSubscript) 47 | } 48 | 49 | /// Toggles superscript for the current selection or at the insertion point. 50 | func toggleSuperscript() { 51 | execCommand(.toggleSuperscript) 52 | } 53 | 54 | /// Creates or remove a numbered ordered list for the current selection or at the insertion point. 55 | func orderedList() { 56 | execCommand(.orderedList) 57 | } 58 | 59 | /// Creates or remove a bulleted unordered list for the current selection or at the insertion point. 60 | func unorderedList() { 61 | execCommand(.unorderedList) 62 | } 63 | 64 | /// Creates a new link for the current selection or at the insertion point. 65 | /// 66 | /// - Parameters: 67 | /// - url: The destination of the link, it is the value of the `href` attribute. 68 | /// - text: The optional label of the link, if nil the url will be used. 69 | func addLink(url: URL, text: String? = nil) { 70 | javaScriptManager.addLink(text: text, path: url.absoluteString) 71 | } 72 | 73 | /// Removes all the links of the current selection or at the insertion point. 74 | func unlink() { 75 | javaScriptManager.unlink() 76 | } 77 | 78 | /// Indents the lines containing the current selection or the insertion point. 79 | func indent() { 80 | execCommand(.indent) 81 | } 82 | 83 | /// Outdents the lines containing the current selection or the insertion point. 84 | func outdent() { 85 | execCommand(.outdent) 86 | } 87 | 88 | /// Justifies the selection or the insertion point to the `RECommandJustifySide` side. 89 | /// 90 | /// - Parameter side: The side to align the text to. 91 | func justify(_ side: TextJustification) { 92 | execCommand(side.command) 93 | } 94 | 95 | /// Sets the font for the selection or at the insertion point. 96 | /// The font must be available on the platform, you can refer to https://developer.apple.com/fonts/system-fonts/ 97 | /// 98 | /// - Parameter name: Font name. 99 | func setFontName(_ name: String) { 100 | execCommand(.fontName, argument: name) 101 | } 102 | 103 | /// Sets the foreground color for the selection or at the insertion point. 104 | /// 105 | /// - Parameter color: The color of the foreground. 106 | func setForegroundColor(_ color: PlatformColor) { 107 | execCommand(.foregroundColor, argument: color.hexadecimal) 108 | } 109 | 110 | /// Sets the background color for the selection or at the insertion point. 111 | /// 112 | /// - Parameter color: The color of the background. 113 | func setBackgroundColor(_ color: PlatformColor) { 114 | execCommand(.backgroundColor, argument: color.hexadecimal) 115 | } 116 | 117 | /// Changes the font size for the selection or at the insertion point. 118 | /// 119 | /// - Parameter size: The size should be included in the interval [1-7]. 120 | func setFontSize(_ size: Int) { 121 | execCommand(.fontSize, argument: size) 122 | } 123 | 124 | /// Undoes the last executed command. 125 | func undo() { 126 | execCommand(.undo) 127 | } 128 | 129 | /// Redoes the last executed command. 130 | func redo() { 131 | execCommand(.redo) 132 | } 133 | 134 | /// Position the caret at a precise position. 135 | /// 136 | /// - Parameter position: The position where the caret should be placed. 137 | func setCaretAt(_ position: CaretPosition) { 138 | switch position { 139 | case .beginningOfDocument: 140 | javaScriptManager.setCaretAtBeginningOfDocument() 141 | case .endOfDocument: 142 | javaScriptManager.setCaretAtEndOfDocument() 143 | case .selector(let selector): 144 | javaScriptManager.setCaretAtSelector(selector: selector) 145 | } 146 | } 147 | 148 | private func execCommand(_ command: ExecCommand, argument: Sendable? = nil) { 149 | javaScriptManager.execCommand(command, argument: argument) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/RichHTMLEditorViewDelegate.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import Foundation 15 | 16 | /// The methods for receiving editing-related messages for editor view objects. 17 | /// 18 | /// All of the methods in this protocol are optional. 19 | @MainActor 20 | public protocol RichHTMLEditorViewDelegate: AnyObject { 21 | /// Tells the delegate when the specified editor view is fully loaded and ready to be used. 22 | /// 23 | /// The editor must load a ``WebKit/WKWebView`` prior to be editable. 24 | /// You can inject CSS or set the initial HTML at any time, but the content will 25 | /// only be visible once the editor is loaded. 26 | /// 27 | /// Implementation of this method is optional. 28 | /// 29 | /// - Parameter richHTMLEditorView: The editor which is loaded. 30 | func richHTMLEditorViewDidLoad(_ richHTMLEditorView: RichHTMLEditorView) 31 | 32 | /// Tells the delegate when the user changes the content or format in the specified editor view. 33 | /// 34 | /// The editor calls this method whenever the content of the editor change. 35 | /// 36 | /// Implementation of this method is optional. 37 | /// 38 | /// - Parameter richHTMLEditorView: The editor which is loaded. 39 | func richHTMLEditorViewDidChange(_ richHTMLEditorView: RichHTMLEditorView) 40 | 41 | /// Tells the delegate when the position of the carte or the selection range of the specified 42 | /// editor moves. 43 | /// 44 | /// Implementation of this method is optional. 45 | /// 46 | /// - Parameters: 47 | /// - richHTMLEditorView: The editor which is loaded. 48 | /// - caretPosition: The new position of the caret or of the selection range. 49 | func richHTMLEditorView(_ richHTMLEditorView: RichHTMLEditorView, caretPositionDidChange caretPosition: CGRect) 50 | 51 | /// Tells the delegate when the attributes of the selected text changes in the specified editor view. 52 | /// 53 | /// When the carte moves, the editor calls this method if the attributes of the selected text is 54 | /// different from the old one. 55 | /// For example, the method will be called if the newly selected text is bold but the previous one 56 | /// was not. 57 | /// The object ``UITextAttributes`` contains all the information about the selected text. 58 | /// 59 | /// Implementation of this method is optional. 60 | /// 61 | /// - Parameters: 62 | /// - richHTMLEditorView: The editor which is loaded. 63 | /// - textAttributes: The new attributes of the selected text. 64 | func richHTMLEditorView( 65 | _ richHTMLEditorView: RichHTMLEditorView, 66 | selectedTextAttributesDidChange textAttributes: UITextAttributes 67 | ) 68 | 69 | /// Tells the delegate when a JavaScript function executed in the specified editor has failed. 70 | /// 71 | /// When the editor calls a JavaScript to update the content, the CSS, or format the text, an 72 | /// error may potentially be raised by JavaScript. 73 | /// 74 | /// Implementation of this method is optional. 75 | /// 76 | /// - Parameters: 77 | /// - richHTMLEditorView: The editor which is loaded. 78 | /// - javascriptError: The `Error` containing information about the error raised by JavaScript. 79 | /// - function: The name of the failing function. 80 | func richHTMLEditorView( 81 | _ richHTMLEditorView: RichHTMLEditorView, 82 | javascriptFunctionDidFail javascriptError: any Error, 83 | whileExecuting function: String 84 | ) 85 | 86 | /// Asks the delegate if the editor should handle opening the link itself. 87 | /// 88 | /// The user can open the link thanks to the contextual menu. If the editor handles this task itself, 89 | /// the link will open in the default browser on iOS but in the editor on macOS. 90 | /// You may need to customize this behaviour. If the method returns `true`, the editor won't 91 | /// open the link and you will be responsible for doing so. 92 | /// 93 | /// Implementation of this method is optional. Default return value is `false`. 94 | /// 95 | /// - Parameters: 96 | /// - richHTMLEditorView: The editor which is loaded. 97 | /// - shouldHandleLink: The URL the user clicked on. 98 | /// 99 | /// - Returns: `false` if the editor should handle the link opening itself. 100 | func richHTMLEditorView(_ richHTMLEditorView: RichHTMLEditorView, shouldHandleLink link: URL) -> Bool 101 | } 102 | 103 | // Default implementation for optional functions 104 | public extension RichHTMLEditorViewDelegate { 105 | func richHTMLEditorViewDidLoad(_ richHTMLEditorView: RichHTMLEditorView) {} 106 | func richHTMLEditorViewDidChange(_ richHTMLEditorView: RichHTMLEditorView) {} 107 | func richHTMLEditorView(_ richHTMLEditorView: RichHTMLEditorView, caretPositionDidChange caretPosition: CGRect) {} 108 | func richHTMLEditorView( 109 | _ richHTMLEditorView: RichHTMLEditorView, 110 | selectedTextAttributesDidChange textAttributes: UITextAttributes 111 | ) {} 112 | func richHTMLEditorView( 113 | _ richHTMLEditorView: RichHTMLEditorView, 114 | javascriptFunctionDidFail javascriptError: any Error, 115 | whileExecuting function: String 116 | ) {} 117 | func richHTMLEditorView(_ richHTMLEditorView: RichHTMLEditorView, shouldHandleLink link: URL) -> Bool { 118 | return false 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Examples/Example iOS/Example iOS/UI/Controllers/EditorViewController/EditorViewController+Toolbar.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | import InfomaniakRichHTMLEditor 15 | import UIKit 16 | 17 | // MARK: - Set up toolbar 18 | 19 | extension EditorViewController { 20 | func setupToolbar() { 21 | let toolbarView = UIView(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: 56)) 22 | toolbarView.backgroundColor = .systemGray6 23 | 24 | let scrollView = setupScrollView(to: toolbarView) 25 | setupAllButtons() 26 | setupStackView(to: scrollView) 27 | 28 | editor.inputAccessoryView = toolbarView 29 | } 30 | 31 | private func setupScrollView(to view: UIView) -> UIScrollView { 32 | let scrollView = UIScrollView() 33 | scrollView.showsHorizontalScrollIndicator = false 34 | scrollView.translatesAutoresizingMaskIntoConstraints = false 35 | view.addSubview(scrollView) 36 | 37 | NSLayoutConstraint.activate([ 38 | scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 39 | scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 40 | scrollView.topAnchor.constraint(equalTo: view.topAnchor), 41 | scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor) 42 | ]) 43 | 44 | return scrollView 45 | } 46 | 47 | private func setupStackView(to scrollView: UIScrollView) { 48 | let stackView = UIStackView(arrangedSubviews: toolbarButtons) 49 | stackView.translatesAutoresizingMaskIntoConstraints = false 50 | stackView.axis = .horizontal 51 | stackView.alignment = .center 52 | stackView.spacing = 8 53 | stackView.layoutMargins = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8) 54 | stackView.isLayoutMarginsRelativeArrangement = true 55 | scrollView.addSubview(stackView) 56 | 57 | NSLayoutConstraint.activate([ 58 | stackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor), 59 | stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor), 60 | stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor), 61 | stackView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor), 62 | 63 | scrollView.frameLayoutGuide.heightAnchor.constraint(equalTo: stackView.heightAnchor) 64 | ]) 65 | } 66 | 67 | private func setupAllButtons() { 68 | for group in ToolbarAction.actionGroups { 69 | for action in group { 70 | let button = createButton(action: action) 71 | button.addTarget(self, action: #selector(didTapToolbarButton), for: .touchUpInside) 72 | toolbarButtons.append(button) 73 | } 74 | 75 | if group != ToolbarAction.actionGroups.last { 76 | let divider = createDivider() 77 | toolbarButtons.append(divider) 78 | } 79 | } 80 | } 81 | 82 | private func createButton(action: ToolbarAction) -> UIButton { 83 | let button = UIButton(configuration: .borderless()) 84 | button.setImage(action.icon, for: .normal) 85 | button.tag = action.rawValue 86 | button.translatesAutoresizingMaskIntoConstraints = false 87 | 88 | NSLayoutConstraint.activate([ 89 | button.heightAnchor.constraint(equalToConstant: 40), 90 | button.widthAnchor.constraint(equalToConstant: 40) 91 | ]) 92 | return button 93 | } 94 | 95 | private func createDivider() -> UIDivider { 96 | let divider = UIDivider() 97 | divider.translatesAutoresizingMaskIntoConstraints = false 98 | 99 | NSLayoutConstraint.activate([ 100 | divider.widthAnchor.constraint(equalToConstant: 1), 101 | divider.heightAnchor.constraint(equalToConstant: 30) 102 | ]) 103 | return divider 104 | } 105 | } 106 | 107 | // MARK: - Handle toolbar buttons 108 | 109 | extension EditorViewController { 110 | @objc func didTapToolbarButton(_ sender: UIButton) { 111 | guard let action = ToolbarAction(rawValue: sender.tag) else { 112 | return 113 | } 114 | 115 | switch action { 116 | case .dismissKeyboard: 117 | _ = editor.resignFirstResponder() 118 | case .bold: 119 | editor.bold() 120 | case .italic: 121 | editor.italic() 122 | case .underline: 123 | editor.underline() 124 | case .strikethrough: 125 | editor.strikethrough() 126 | case .link: 127 | handleLink() 128 | case .toggleSubscript: 129 | editor.toggleSubscript() 130 | case .toggleSuperscript: 131 | editor.toggleSuperscript() 132 | case .orderedList: 133 | editor.orderedList() 134 | case .unorderedList: 135 | editor.unorderedList() 136 | case .justifyFull: 137 | editor.justify(.full) 138 | case .justifyLeft: 139 | editor.justify(.left) 140 | case .justifyCenter: 141 | editor.justify(.center) 142 | case .justifyRight: 143 | editor.justify(.right) 144 | case .fontName: 145 | presentFontNameAlert() 146 | case .fontSize: 147 | presentFontSizeAlert() 148 | case .foregroundColor: 149 | presentColorPicker(title: "Text Color", action: .foregroundColor) 150 | case .backgroundColor: 151 | presentColorPicker(title: "Background Color", action: .backgroundColor) 152 | case .outdent: 153 | editor.outdent() 154 | case .indent: 155 | editor.indent() 156 | case .undo: 157 | editor.undo() 158 | case .redo: 159 | editor.redo() 160 | case .removeFormat: 161 | editor.removeFormat() 162 | } 163 | } 164 | 165 | private func handleLink() { 166 | if editor.selectedTextAttributes.hasLink { 167 | editor.unlink() 168 | } else { 169 | presentCreateLinkAlert() 170 | } 171 | } 172 | 173 | private func presentCreateLinkAlert() { 174 | let alertController = UIAlertController(title: "Create Link", message: nil, preferredStyle: .alert) 175 | 176 | alertController.addTextField { nameTextField in 177 | nameTextField.placeholder = "Label (Optional)" 178 | } 179 | alertController.addTextField { urlTextField in 180 | urlTextField.placeholder = "URL" 181 | urlTextField.keyboardType = .URL 182 | } 183 | 184 | alertController.addAction(UIAlertAction(title: "Add", style: .default) { _ in 185 | guard let rawURL = alertController.textFields?[1].text, 186 | let url = URL(string: rawURL) 187 | else { return } 188 | 189 | self.editor.addLink(url: url, text: alertController.textFields?[0].text) 190 | }) 191 | alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in 192 | _ = self.editor.becomeFirstResponder() 193 | }) 194 | 195 | present(alertController, animated: true) 196 | } 197 | 198 | private func presentFontNameAlert() { 199 | let alertController = UIAlertController(title: "Choose Font", message: nil, preferredStyle: .actionSheet) 200 | 201 | let fontOptions = ["-apple-system", "serif", "sans-serif", "Savoye Let"] 202 | for fontOption in fontOptions { 203 | alertController.addAction(UIAlertAction(title: fontOption, style: .default) { _ in 204 | self.editor.setFontName(fontOption) 205 | }) 206 | } 207 | alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel)) 208 | 209 | present(alertController, animated: true) 210 | } 211 | 212 | private func presentFontSizeAlert() { 213 | let alertController = UIAlertController( 214 | title: "Choose Font Size", 215 | message: "Choose a font size between 1 and 7", 216 | preferredStyle: .alert 217 | ) 218 | 219 | alertController.addTextField { textField in 220 | textField.keyboardType = .numberPad 221 | if let fontSize = self.editor.selectedTextAttributes.fontSize { 222 | textField.text = "\(fontSize)" 223 | } 224 | } 225 | 226 | alertController.addAction(UIAlertAction(title: "OK", style: .default) { _ in 227 | guard let text = alertController.textFields?[0].text, let newSize = Int(text) else { return } 228 | self.editor.setFontSize(newSize) 229 | }) 230 | alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel)) 231 | 232 | present(alertController, animated: true) 233 | } 234 | 235 | private func presentColorPicker(title: String, action: ToolbarAction) { 236 | let colorPicker = UIColorPickerViewController() 237 | colorPicker.title = title 238 | colorPicker.supportsAlpha = false 239 | colorPicker.delegate = self 240 | colorPicker.modalPresentationStyle = .pageSheet 241 | colorPicker.popoverPresentationController?.sourceItem = toolbarButtons[action.rawValue] 242 | 243 | toolbarCurrentColorPicker = action 244 | 245 | present(colorPicker, animated: true) 246 | } 247 | } 248 | 249 | // MARK: - UIColorPickerViewControllerDelegate 250 | 251 | extension EditorViewController: UIColorPickerViewControllerDelegate { 252 | func colorPickerViewController(_ viewController: UIColorPickerViewController, didSelect color: UIColor, continuously: Bool) { 253 | guard let toolbarCurrentColorPicker else { 254 | return 255 | } 256 | 257 | if toolbarCurrentColorPicker == .foregroundColor { 258 | editor.setForegroundColor(viewController.selectedColor) 259 | } else if toolbarCurrentColorPicker == .backgroundColor { 260 | editor.setBackgroundColor(viewController.selectedColor) 261 | } 262 | } 263 | 264 | func colorPickerViewControllerDidFinish(_ viewController: UIColorPickerViewController) { 265 | toolbarCurrentColorPicker = nil 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Sources/InfomaniakRichHTMLEditor/RichHTMLEditorView.swift: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, 8 | // software distributed under the License is distributed on an 9 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 10 | // KIND, either express or implied. See the License for the 11 | // specific language governing permissions and limitations 12 | // under the License. 13 | 14 | #if canImport(UIKit) 15 | import UIKit 16 | 17 | public typealias PlatformView = UIView 18 | public typealias PlatformColor = UIColor 19 | #elseif canImport(AppKit) 20 | import AppKit 21 | 22 | public typealias PlatformView = NSView 23 | public typealias PlatformColor = NSColor 24 | #endif 25 | 26 | import OSLog 27 | import WebKit 28 | 29 | /// An editor to edit HTML content. 30 | /// 31 | /// `RichHTMLEditorView` supports the display of HTML content using 32 | /// using custom style and also supports text editing. You typically use an 33 | /// editor to display multiple lines of text, such as when displaying the body 34 | /// of an email. 35 | /// 36 | /// The appearance of the editor can be customized thanks to the ``RichHTMLEditorView/injectAdditionalCSS(_:)-406n9`` method. 37 | /// The editor provides information about the style of the currently selected text or of 38 | /// the text at the insertion point in the ``RichHTMLEditorView/selectedTextAttributes`` 39 | /// property. 40 | /// Many functions are available to update the style, such as ``RichHTMLEditorView/bold()``. 41 | public class RichHTMLEditorView: PlatformView { 42 | // MARK: - Public Properties 43 | 44 | /// The HTML code that the editor view contains. 45 | public var html: String { 46 | get { 47 | return rawHTMLContent 48 | } 49 | set { 50 | setHTMLContent(newValue) 51 | } 52 | } 53 | 54 | #if canImport(UIKit) 55 | /// A Boolean value that indicates whether the responder accepts first responder status. 56 | override public var canBecomeFirstResponder: Bool { 57 | return true 58 | } 59 | 60 | #elseif canImport(AppKit) 61 | /// A Boolean value that indicates whether the responder accepts first responder status. 62 | override public var acceptsFirstResponder: Bool { 63 | return true 64 | } 65 | #endif 66 | 67 | #if canImport(UIKit) 68 | /// Returns a Boolean value indicating whether this object is the first responder. 69 | override public var isFirstResponder: Bool { 70 | return webView.containsFirstResponder 71 | } 72 | #endif 73 | 74 | #if canImport(UIKit) && !os(visionOS) 75 | /// The custom accessory view to display when the editor view becomes the first responder. 76 | override public var inputAccessoryView: UIView? { 77 | get { 78 | return webView.inputAccessoryView 79 | } 80 | set { 81 | webView.inputAccessoryView = newValue 82 | } 83 | } 84 | #endif 85 | 86 | /// The natural size for the receiving view, considering only properties of the view itself. 87 | override public var intrinsicContentSize: CGSize { 88 | CGSize(width: PlatformView.noIntrinsicMetric, height: rawContentHeight) 89 | } 90 | 91 | #if canImport(UIKit) 92 | /// A Boolean value that indicates whether the editor view use its inner scrollview. 93 | /// 94 | /// When the Boolean is `false`, the editor will use the first parent 95 | /// `UIScrollView` to keep the caret always visible when the 96 | /// caret moves below the keyboard or off the screen. 97 | /// However, when the Boolean is `true`, the editor will use the 98 | /// `UIScrollView` of the inner ``WebKit/WKWebView`` 99 | /// to keep the caret visible. 100 | /// 101 | /// The default value is `false`. 102 | public var isScrollEnabled: Bool { 103 | get { 104 | rawIsScrollEnabled 105 | } 106 | set { 107 | setScrollableBehavior(newValue) 108 | } 109 | } 110 | #endif 111 | 112 | /// A Boolean value that indicates whether the DOM of the underlying ``WebKit/WKWebView`` 113 | /// is loaded. 114 | /// 115 | /// If an initial content has been set to the editor, it will be displayed once the editor is loaded. 116 | public var isEditorLoaded: Bool { 117 | return javaScriptManager.isDOMContentLoaded 118 | } 119 | 120 | /// The object you use to react to editor's events. 121 | public weak var delegate: RichHTMLEditorViewDelegate? 122 | 123 | /// The style of the text currently selected in the editor view. 124 | public private(set) var selectedTextAttributes = UITextAttributes() 125 | 126 | /// The web view that displays the HTML and handle the input. 127 | public private(set) var webView: RichHTMLWebView! 128 | 129 | // MARK: - Private properties 130 | 131 | var rawHTMLContent = "" 132 | var rawIsScrollEnabled = false 133 | var rawContentHeight = CGFloat.zero 134 | 135 | var javaScriptManager: JavaScriptManager! 136 | var scriptMessageHandler: ScriptMessageHandler! 137 | 138 | let logger = Logger(subsystem: Constants.packageID, category: "ScriptMessageHandler") 139 | 140 | override init(frame: CGRect) { 141 | super.init(frame: frame) 142 | 143 | scriptMessageHandler = ScriptMessageHandler() 144 | scriptMessageHandler.delegate = self 145 | 146 | setUpWebView() 147 | javaScriptManager = JavaScriptManager(webView: webView) 148 | javaScriptManager.delegate = self 149 | } 150 | 151 | @available(*, unavailable) 152 | required init?(coder: NSCoder) { 153 | fatalError("init(coder:) has not been implemented") 154 | } 155 | 156 | /// Notifies the receiver that it’s about to become first responder in its window. 157 | override public func becomeFirstResponder() -> Bool { 158 | javaScriptManager.focus() 159 | return true 160 | } 161 | 162 | /// Notifies this object that it has been asked to relinquish its status as first responder in its window. 163 | override public func resignFirstResponder() -> Bool { 164 | javaScriptManager.blur() 165 | return true 166 | } 167 | } 168 | 169 | // MARK: - Customize Editor 170 | 171 | public extension RichHTMLEditorView { 172 | /// Injects CSS code to customize the appearance of the editor view. 173 | /// 174 | /// This method is additive. Each call adds the CSS to the editor and does not replace 175 | /// previously added code. 176 | /// 177 | /// - Parameter css: CSS code to append to the editor. 178 | func injectAdditionalCSS(_ css: String) { 179 | javaScriptManager.injectCSS(css) 180 | } 181 | 182 | /// Injects CSS code to customize the appearance of the editor view. 183 | /// 184 | /// This method is additive. Each call adds the CSS to the editor and does not replace 185 | /// previously added code. 186 | /// 187 | /// - Parameter cssURL: URL to the CSS file. 188 | func injectAdditionalCSS(_ cssURL: URL) { 189 | guard let css = try? String(contentsOf: cssURL) else { return } 190 | injectAdditionalCSS(css) 191 | } 192 | } 193 | 194 | // MARK: - WKWebView 195 | 196 | public extension RichHTMLEditorView { 197 | private func setUpWebView() { 198 | webView = RichHTMLWebView(frame: .zero, configuration: WKWebViewConfiguration()) 199 | webView.translatesAutoresizingMaskIntoConstraints = false 200 | #if canImport(UIKit) 201 | webView.scrollView.delegate = self 202 | #endif 203 | webView.navigationDelegate = self 204 | addSubview(webView) 205 | 206 | NSLayoutConstraint.activate([ 207 | webView.topAnchor.constraint(equalTo: topAnchor), 208 | webView.trailingAnchor.constraint(equalTo: trailingAnchor), 209 | webView.bottomAnchor.constraint(equalTo: bottomAnchor), 210 | webView.leadingAnchor.constraint(equalTo: leadingAnchor) 211 | ]) 212 | 213 | configureWebView() 214 | enableWebViewDebug() 215 | 216 | #if canImport(UIKit) 217 | setScrollableBehavior(false) 218 | #endif 219 | 220 | loadWebViewPage() 221 | loadScripts() 222 | } 223 | 224 | private func configureWebView() { 225 | if #available(iOS 14.0, macOS 11.0, *) { 226 | webView.configuration.defaultWebpagePreferences.allowsContentJavaScript = false 227 | } else { 228 | webView.configuration.preferences.javaScriptEnabled = false 229 | } 230 | 231 | for scriptMessage in ScriptMessageHandler.ScriptMessage.allCases { 232 | webView.configuration.userContentController.add(scriptMessageHandler, scriptMessage: scriptMessage) 233 | } 234 | 235 | #if canImport(UIKit) 236 | webView.backgroundColor = .clear 237 | webView.isOpaque = false 238 | #endif 239 | } 240 | 241 | private func loadScripts() { 242 | for script in UserScript.allCases { 243 | do { 244 | try script.load(to: webView) 245 | } catch { 246 | logger.error("Error while loading UserScript: \(error)") 247 | } 248 | } 249 | } 250 | 251 | private func loadWebViewPage() { 252 | guard let indexURL = Bundle.module.url(forResource: "index", withExtension: "html") else { 253 | return 254 | } 255 | 256 | let request = URLRequest(url: indexURL) 257 | webView.load(request) 258 | } 259 | 260 | private func enableWebViewDebug() { 261 | if #available(iOS 16.4, macOS 13.3, *) { 262 | #if DEBUG 263 | webView.isInspectable = true 264 | #endif 265 | } 266 | } 267 | 268 | private func setHTMLContent(_ newContent: String) { 269 | javaScriptManager.setHTMLContent(newContent) 270 | } 271 | 272 | #if canImport(UIKit) 273 | private func setScrollableBehavior(_ isScrollEnabled: Bool) { 274 | rawIsScrollEnabled = isScrollEnabled 275 | webView.scrollView.isScrollEnabled = isScrollEnabled 276 | } 277 | #endif 278 | } 279 | 280 | // MARK: - WKNavigationDelegate 281 | 282 | extension RichHTMLEditorView: WKNavigationDelegate { 283 | public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping @MainActor (WKNavigationActionPolicy) -> Void) { 284 | switch navigationAction.navigationType { 285 | case .linkActivated: 286 | if let url = navigationAction.request.url, delegate?.richHTMLEditorView(self, shouldHandleLink: url) == true { 287 | decisionHandler(.cancel) 288 | } else { 289 | decisionHandler(.allow) 290 | } 291 | case .backForward, .formSubmitted, .reload, .formResubmitted: 292 | decisionHandler(.cancel) 293 | case .other: 294 | decisionHandler(.allow) 295 | @unknown default: 296 | decisionHandler(.allow) 297 | } 298 | } 299 | } 300 | 301 | // MARK: - UIScrollViewDelegate 302 | 303 | #if canImport(UIKit) 304 | extension RichHTMLEditorView: UIScrollViewDelegate { 305 | /// When the editor is not scrollable, the WebView should not scroll. 306 | /// 307 | /// Disabling the scrollview is not enough to completely prevent 308 | /// scrolling. 309 | /// It is necessary to reset the scrollOffset each time it changes 310 | /// (when the caret is under the keyboard for example). 311 | public func scrollViewDidScroll(_ scrollView: UIScrollView) { 312 | guard !isScrollEnabled else { return } 313 | scrollView.contentOffset = .zero 314 | } 315 | } 316 | #endif 317 | 318 | // MARK: - ScriptMessageHandlerDelegate 319 | 320 | extension RichHTMLEditorView: ScriptMessageHandlerDelegate { 321 | func editorDidLoad() { 322 | javaScriptManager.isDOMContentLoaded = true 323 | delegate?.richHTMLEditorViewDidLoad(self) 324 | } 325 | 326 | func contentDidChange(_ text: String) { 327 | rawHTMLContent = text 328 | delegate?.richHTMLEditorViewDidChange(self) 329 | } 330 | 331 | func contentHeightDidChange(_ contentHeight: CGFloat) { 332 | rawContentHeight = contentHeight 333 | invalidateIntrinsicContentSize() 334 | } 335 | 336 | func selectedTextAttributesDidChange(_ selectedTextAttributes: UITextAttributes?) { 337 | guard let selectedTextAttributes else { 338 | return 339 | } 340 | 341 | self.selectedTextAttributes = selectedTextAttributes 342 | delegate?.richHTMLEditorView(self, selectedTextAttributesDidChange: selectedTextAttributes) 343 | } 344 | 345 | func caretRectDidChange(_ position: CGRect) { 346 | delegate?.richHTMLEditorView(self, caretPositionDidChange: position) 347 | } 348 | 349 | func caretPositionDidChange(_ caretRect: CGRect) { 350 | delegate?.richHTMLEditorView(self, caretPositionDidChange: caretRect) 351 | 352 | #if canImport(UIKit) 353 | if !isScrollEnabled, let scrollView = findClosestScrollView() { 354 | let scrollRect = convert(caretRect, to: scrollView) 355 | scrollView.scrollRectToVisible(scrollRect, animated: true) 356 | } 357 | #endif 358 | } 359 | } 360 | 361 | // MARK: - JavaScriptManagerDelegate 362 | 363 | extension RichHTMLEditorView: JavaScriptManagerDelegate { 364 | func javascriptFunctionDidFail(error: any Error, function: String) { 365 | delegate?.richHTMLEditorView(self, javascriptFunctionDidFail: error, whileExecuting: function) 366 | } 367 | } 368 | -------------------------------------------------------------------------------- /Examples/Example macOS/Example macOS.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | F90560E52C47B8020015E813 /* InfomaniakRichHTMLEditor in Frameworks */ = {isa = PBXBuildFile; productRef = F90560E42C47B8020015E813 /* InfomaniakRichHTMLEditor */; }; 11 | F9BACEA22C3435DF002FB720 /* WindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9BACEA12C3435DF002FB720 /* WindowController.swift */; }; 12 | F9BACEA42C352CB0002FB720 /* style.css in Resources */ = {isa = PBXBuildFile; fileRef = F9BACEA32C352CB0002FB720 /* style.css */; }; 13 | F9FBB6A42C1326B9007CC5A5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9FBB6A32C1326B9007CC5A5 /* AppDelegate.swift */; }; 14 | F9FBB6A62C1326B9007CC5A5 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9FBB6A52C1326B9007CC5A5 /* ViewController.swift */; }; 15 | F9FBB6A82C1326BB007CC5A5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F9FBB6A72C1326BB007CC5A5 /* Assets.xcassets */; }; 16 | F9FBB6AB2C1326BB007CC5A5 /* Base in Resources */ = {isa = PBXBuildFile; fileRef = F9FBB6AA2C1326BB007CC5A5 /* Base */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | F9BACEA12C3435DF002FB720 /* WindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowController.swift; sourceTree = ""; }; 21 | F9BACEA32C352CB0002FB720 /* style.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = style.css; sourceTree = ""; }; 22 | F9FBB6A02C1326B9007CC5A5 /* Example macOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Example macOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 23 | F9FBB6A32C1326B9007CC5A5 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 24 | F9FBB6A52C1326B9007CC5A5 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 25 | F9FBB6A72C1326BB007CC5A5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 26 | F9FBB6AA2C1326BB007CC5A5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 27 | F9FBB6AC2C1326BB007CC5A5 /* Example_macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Example_macOS.entitlements; sourceTree = ""; }; 28 | /* End PBXFileReference section */ 29 | 30 | /* Begin PBXFrameworksBuildPhase section */ 31 | F9FBB69D2C1326B9007CC5A5 /* Frameworks */ = { 32 | isa = PBXFrameworksBuildPhase; 33 | buildActionMask = 2147483647; 34 | files = ( 35 | F90560E52C47B8020015E813 /* InfomaniakRichHTMLEditor in Frameworks */, 36 | ); 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXFrameworksBuildPhase section */ 40 | 41 | /* Begin PBXGroup section */ 42 | F90560E32C47B8020015E813 /* Frameworks */ = { 43 | isa = PBXGroup; 44 | children = ( 45 | ); 46 | name = Frameworks; 47 | sourceTree = ""; 48 | }; 49 | F91D20122C342642004A8625 /* Resources */ = { 50 | isa = PBXGroup; 51 | children = ( 52 | F9FBB6A72C1326BB007CC5A5 /* Assets.xcassets */, 53 | F9FBB6AC2C1326BB007CC5A5 /* Example_macOS.entitlements */, 54 | F9BACEA32C352CB0002FB720 /* style.css */, 55 | ); 56 | path = Resources; 57 | sourceTree = ""; 58 | }; 59 | F91D20132C342687004A8625 /* UI */ = { 60 | isa = PBXGroup; 61 | children = ( 62 | F9FBB6A92C1326BB007CC5A5 /* Main.storyboard */, 63 | F9FBB6A52C1326B9007CC5A5 /* ViewController.swift */, 64 | F9BACEA12C3435DF002FB720 /* WindowController.swift */, 65 | ); 66 | path = UI; 67 | sourceTree = ""; 68 | }; 69 | F9FBB6972C1326B9007CC5A5 = { 70 | isa = PBXGroup; 71 | children = ( 72 | F9FBB6A22C1326B9007CC5A5 /* Example macOS */, 73 | F9FBB6A12C1326B9007CC5A5 /* Products */, 74 | F90560E32C47B8020015E813 /* Frameworks */, 75 | ); 76 | sourceTree = ""; 77 | }; 78 | F9FBB6A12C1326B9007CC5A5 /* Products */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | F9FBB6A02C1326B9007CC5A5 /* Example macOS.app */, 82 | ); 83 | name = Products; 84 | sourceTree = ""; 85 | }; 86 | F9FBB6A22C1326B9007CC5A5 /* Example macOS */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | F9FBB6A32C1326B9007CC5A5 /* AppDelegate.swift */, 90 | F91D20132C342687004A8625 /* UI */, 91 | F91D20122C342642004A8625 /* Resources */, 92 | ); 93 | path = "Example macOS"; 94 | sourceTree = ""; 95 | }; 96 | /* End PBXGroup section */ 97 | 98 | /* Begin PBXNativeTarget section */ 99 | F9FBB69F2C1326B9007CC5A5 /* Example macOS */ = { 100 | isa = PBXNativeTarget; 101 | buildConfigurationList = F9FBB6AF2C1326BB007CC5A5 /* Build configuration list for PBXNativeTarget "Example macOS" */; 102 | buildPhases = ( 103 | F9FBB69C2C1326B9007CC5A5 /* Sources */, 104 | F9FBB69D2C1326B9007CC5A5 /* Frameworks */, 105 | F9FBB69E2C1326B9007CC5A5 /* Resources */, 106 | ); 107 | buildRules = ( 108 | ); 109 | dependencies = ( 110 | ); 111 | name = "Example macOS"; 112 | packageProductDependencies = ( 113 | F90560E42C47B8020015E813 /* InfomaniakRichHTMLEditor */, 114 | ); 115 | productName = "Example macOS"; 116 | productReference = F9FBB6A02C1326B9007CC5A5 /* Example macOS.app */; 117 | productType = "com.apple.product-type.application"; 118 | }; 119 | /* End PBXNativeTarget section */ 120 | 121 | /* Begin PBXProject section */ 122 | F9FBB6982C1326B9007CC5A5 /* Project object */ = { 123 | isa = PBXProject; 124 | attributes = { 125 | BuildIndependentTargetsInParallel = 1; 126 | LastSwiftUpdateCheck = 1540; 127 | LastUpgradeCheck = 1540; 128 | TargetAttributes = { 129 | F9FBB69F2C1326B9007CC5A5 = { 130 | CreatedOnToolsVersion = 15.4; 131 | }; 132 | }; 133 | }; 134 | buildConfigurationList = F9FBB69B2C1326B9007CC5A5 /* Build configuration list for PBXProject "Example macOS" */; 135 | compatibilityVersion = "Xcode 14.0"; 136 | developmentRegion = en; 137 | hasScannedForEncodings = 0; 138 | knownRegions = ( 139 | en, 140 | Base, 141 | ); 142 | mainGroup = F9FBB6972C1326B9007CC5A5; 143 | packageReferences = ( 144 | F99CE4C72C132D7900949F40 /* XCRemoteSwiftPackageReference "swift-rich-html-editor" */, 145 | ); 146 | productRefGroup = F9FBB6A12C1326B9007CC5A5 /* Products */; 147 | projectDirPath = ""; 148 | projectRoot = ""; 149 | targets = ( 150 | F9FBB69F2C1326B9007CC5A5 /* Example macOS */, 151 | ); 152 | }; 153 | /* End PBXProject section */ 154 | 155 | /* Begin PBXResourcesBuildPhase section */ 156 | F9FBB69E2C1326B9007CC5A5 /* Resources */ = { 157 | isa = PBXResourcesBuildPhase; 158 | buildActionMask = 2147483647; 159 | files = ( 160 | F9FBB6A82C1326BB007CC5A5 /* Assets.xcassets in Resources */, 161 | F9BACEA42C352CB0002FB720 /* style.css in Resources */, 162 | F9FBB6AB2C1326BB007CC5A5 /* Base in Resources */, 163 | ); 164 | runOnlyForDeploymentPostprocessing = 0; 165 | }; 166 | /* End PBXResourcesBuildPhase section */ 167 | 168 | /* Begin PBXSourcesBuildPhase section */ 169 | F9FBB69C2C1326B9007CC5A5 /* Sources */ = { 170 | isa = PBXSourcesBuildPhase; 171 | buildActionMask = 2147483647; 172 | files = ( 173 | F9FBB6A62C1326B9007CC5A5 /* ViewController.swift in Sources */, 174 | F9BACEA22C3435DF002FB720 /* WindowController.swift in Sources */, 175 | F9FBB6A42C1326B9007CC5A5 /* AppDelegate.swift in Sources */, 176 | ); 177 | runOnlyForDeploymentPostprocessing = 0; 178 | }; 179 | /* End PBXSourcesBuildPhase section */ 180 | 181 | /* Begin PBXVariantGroup section */ 182 | F9FBB6A92C1326BB007CC5A5 /* Main.storyboard */ = { 183 | isa = PBXVariantGroup; 184 | children = ( 185 | F9FBB6AA2C1326BB007CC5A5 /* Base */, 186 | ); 187 | name = Main.storyboard; 188 | sourceTree = ""; 189 | }; 190 | /* End PBXVariantGroup section */ 191 | 192 | /* Begin XCBuildConfiguration section */ 193 | F9FBB6AD2C1326BB007CC5A5 /* Debug */ = { 194 | isa = XCBuildConfiguration; 195 | buildSettings = { 196 | ALWAYS_SEARCH_USER_PATHS = NO; 197 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 198 | CLANG_ANALYZER_NONNULL = YES; 199 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 200 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 201 | CLANG_ENABLE_MODULES = YES; 202 | CLANG_ENABLE_OBJC_ARC = YES; 203 | CLANG_ENABLE_OBJC_WEAK = YES; 204 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 205 | CLANG_WARN_BOOL_CONVERSION = YES; 206 | CLANG_WARN_COMMA = YES; 207 | CLANG_WARN_CONSTANT_CONVERSION = YES; 208 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 209 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 210 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 211 | CLANG_WARN_EMPTY_BODY = YES; 212 | CLANG_WARN_ENUM_CONVERSION = YES; 213 | CLANG_WARN_INFINITE_RECURSION = YES; 214 | CLANG_WARN_INT_CONVERSION = YES; 215 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 216 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 217 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 218 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 219 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 220 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 221 | CLANG_WARN_STRICT_PROTOTYPES = YES; 222 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 223 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 224 | CLANG_WARN_UNREACHABLE_CODE = YES; 225 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 226 | COPY_PHASE_STRIP = NO; 227 | DEBUG_INFORMATION_FORMAT = dwarf; 228 | ENABLE_STRICT_OBJC_MSGSEND = YES; 229 | ENABLE_TESTABILITY = YES; 230 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 231 | GCC_C_LANGUAGE_STANDARD = gnu17; 232 | GCC_DYNAMIC_NO_PIC = NO; 233 | GCC_NO_COMMON_BLOCKS = YES; 234 | GCC_OPTIMIZATION_LEVEL = 0; 235 | GCC_PREPROCESSOR_DEFINITIONS = ( 236 | "DEBUG=1", 237 | "$(inherited)", 238 | ); 239 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 240 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 241 | GCC_WARN_UNDECLARED_SELECTOR = YES; 242 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 243 | GCC_WARN_UNUSED_FUNCTION = YES; 244 | GCC_WARN_UNUSED_VARIABLE = YES; 245 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 246 | MACOSX_DEPLOYMENT_TARGET = 13.0; 247 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 248 | MTL_FAST_MATH = YES; 249 | ONLY_ACTIVE_ARCH = YES; 250 | SDKROOT = macosx; 251 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 252 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 253 | }; 254 | name = Debug; 255 | }; 256 | F9FBB6AE2C1326BB007CC5A5 /* Release */ = { 257 | isa = XCBuildConfiguration; 258 | buildSettings = { 259 | ALWAYS_SEARCH_USER_PATHS = NO; 260 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 261 | CLANG_ANALYZER_NONNULL = YES; 262 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 263 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 264 | CLANG_ENABLE_MODULES = YES; 265 | CLANG_ENABLE_OBJC_ARC = YES; 266 | CLANG_ENABLE_OBJC_WEAK = YES; 267 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 268 | CLANG_WARN_BOOL_CONVERSION = YES; 269 | CLANG_WARN_COMMA = YES; 270 | CLANG_WARN_CONSTANT_CONVERSION = YES; 271 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 272 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 273 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 274 | CLANG_WARN_EMPTY_BODY = YES; 275 | CLANG_WARN_ENUM_CONVERSION = YES; 276 | CLANG_WARN_INFINITE_RECURSION = YES; 277 | CLANG_WARN_INT_CONVERSION = YES; 278 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 279 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 280 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 281 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 282 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 283 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 284 | CLANG_WARN_STRICT_PROTOTYPES = YES; 285 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 286 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 287 | CLANG_WARN_UNREACHABLE_CODE = YES; 288 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 289 | COPY_PHASE_STRIP = NO; 290 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 291 | ENABLE_NS_ASSERTIONS = NO; 292 | ENABLE_STRICT_OBJC_MSGSEND = YES; 293 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 294 | GCC_C_LANGUAGE_STANDARD = gnu17; 295 | GCC_NO_COMMON_BLOCKS = YES; 296 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 297 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 298 | GCC_WARN_UNDECLARED_SELECTOR = YES; 299 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 300 | GCC_WARN_UNUSED_FUNCTION = YES; 301 | GCC_WARN_UNUSED_VARIABLE = YES; 302 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 303 | MACOSX_DEPLOYMENT_TARGET = 13.0; 304 | MTL_ENABLE_DEBUG_INFO = NO; 305 | MTL_FAST_MATH = YES; 306 | SDKROOT = macosx; 307 | SWIFT_COMPILATION_MODE = wholemodule; 308 | }; 309 | name = Release; 310 | }; 311 | F9FBB6B02C1326BB007CC5A5 /* Debug */ = { 312 | isa = XCBuildConfiguration; 313 | buildSettings = { 314 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 315 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 316 | CODE_SIGN_ENTITLEMENTS = "Example macOS/Resources/Example_macOS.entitlements"; 317 | CODE_SIGN_STYLE = Automatic; 318 | COMBINE_HIDPI_IMAGES = YES; 319 | CURRENT_PROJECT_VERSION = 1; 320 | DEVELOPMENT_TEAM = 864VDCS2QY; 321 | ENABLE_HARDENED_RUNTIME = YES; 322 | GENERATE_INFOPLIST_FILE = YES; 323 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 324 | INFOPLIST_KEY_NSMainStoryboardFile = Main; 325 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 326 | LD_RUNPATH_SEARCH_PATHS = ( 327 | "$(inherited)", 328 | "@executable_path/../Frameworks", 329 | ); 330 | MARKETING_VERSION = 1.0; 331 | PRODUCT_BUNDLE_IDENTIFIER = "com.infomaniak.Example-macOS"; 332 | PRODUCT_NAME = "$(TARGET_NAME)"; 333 | SWIFT_EMIT_LOC_STRINGS = YES; 334 | SWIFT_VERSION = 5.0; 335 | }; 336 | name = Debug; 337 | }; 338 | F9FBB6B12C1326BB007CC5A5 /* Release */ = { 339 | isa = XCBuildConfiguration; 340 | buildSettings = { 341 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 342 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 343 | CODE_SIGN_ENTITLEMENTS = "Example macOS/Resources/Example_macOS.entitlements"; 344 | CODE_SIGN_STYLE = Automatic; 345 | COMBINE_HIDPI_IMAGES = YES; 346 | CURRENT_PROJECT_VERSION = 1; 347 | DEVELOPMENT_TEAM = 864VDCS2QY; 348 | ENABLE_HARDENED_RUNTIME = YES; 349 | GENERATE_INFOPLIST_FILE = YES; 350 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 351 | INFOPLIST_KEY_NSMainStoryboardFile = Main; 352 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 353 | LD_RUNPATH_SEARCH_PATHS = ( 354 | "$(inherited)", 355 | "@executable_path/../Frameworks", 356 | ); 357 | MARKETING_VERSION = 1.0; 358 | PRODUCT_BUNDLE_IDENTIFIER = "com.infomaniak.Example-macOS"; 359 | PRODUCT_NAME = "$(TARGET_NAME)"; 360 | SWIFT_EMIT_LOC_STRINGS = YES; 361 | SWIFT_VERSION = 5.0; 362 | }; 363 | name = Release; 364 | }; 365 | /* End XCBuildConfiguration section */ 366 | 367 | /* Begin XCConfigurationList section */ 368 | F9FBB69B2C1326B9007CC5A5 /* Build configuration list for PBXProject "Example macOS" */ = { 369 | isa = XCConfigurationList; 370 | buildConfigurations = ( 371 | F9FBB6AD2C1326BB007CC5A5 /* Debug */, 372 | F9FBB6AE2C1326BB007CC5A5 /* Release */, 373 | ); 374 | defaultConfigurationIsVisible = 0; 375 | defaultConfigurationName = Release; 376 | }; 377 | F9FBB6AF2C1326BB007CC5A5 /* Build configuration list for PBXNativeTarget "Example macOS" */ = { 378 | isa = XCConfigurationList; 379 | buildConfigurations = ( 380 | F9FBB6B02C1326BB007CC5A5 /* Debug */, 381 | F9FBB6B12C1326BB007CC5A5 /* Release */, 382 | ); 383 | defaultConfigurationIsVisible = 0; 384 | defaultConfigurationName = Release; 385 | }; 386 | /* End XCConfigurationList section */ 387 | 388 | /* Begin XCRemoteSwiftPackageReference section */ 389 | F99CE4C72C132D7900949F40 /* XCRemoteSwiftPackageReference "swift-rich-html-editor" */ = { 390 | isa = XCRemoteSwiftPackageReference; 391 | repositoryURL = "https://github.com/Infomaniak/swift-rich-html-editor"; 392 | requirement = { 393 | branch = main; 394 | kind = branch; 395 | }; 396 | }; 397 | /* End XCRemoteSwiftPackageReference section */ 398 | 399 | /* Begin XCSwiftPackageProductDependency section */ 400 | F90560E42C47B8020015E813 /* InfomaniakRichHTMLEditor */ = { 401 | isa = XCSwiftPackageProductDependency; 402 | package = F99CE4C72C132D7900949F40 /* XCRemoteSwiftPackageReference "swift-rich-html-editor" */; 403 | productName = InfomaniakRichHTMLEditor; 404 | }; 405 | /* End XCSwiftPackageProductDependency section */ 406 | }; 407 | rootObject = F9FBB6982C1326B9007CC5A5 /* Project object */; 408 | } 409 | -------------------------------------------------------------------------------- /Examples/Example SwiftUI/Example SwiftUI.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | A768AFC72C5C35500055519D /* ScrollableEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A768AFC62C5C35500055519D /* ScrollableEditorView.swift */; }; 11 | A768AFC92C5C36090055519D /* NotScrollableEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A768AFC82C5C36090055519D /* NotScrollableEditorView.swift */; }; 12 | A768AFCC2C5C37210055519D /* FixedSizeEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A768AFCB2C5C37210055519D /* FixedSizeEditorView.swift */; }; 13 | F90560E82C47B8830015E813 /* InfomaniakRichHTMLEditor in Frameworks */ = {isa = PBXBuildFile; productRef = F90560E72C47B8830015E813 /* InfomaniakRichHTMLEditor */; }; 14 | F91EA2492C3D12B7007489BD /* Example_SwiftUIApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F91EA2482C3D12B7007489BD /* Example_SwiftUIApp.swift */; }; 15 | F91EA24B2C3D12B7007489BD /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F91EA24A2C3D12B7007489BD /* RootView.swift */; }; 16 | F91EA24D2C3D12B8007489BD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F91EA24C2C3D12B8007489BD /* Assets.xcassets */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXCopyFilesBuildPhase section */ 20 | F98909AF2C3FAFC300A594B8 /* Embed Frameworks */ = { 21 | isa = PBXCopyFilesBuildPhase; 22 | buildActionMask = 2147483647; 23 | dstPath = ""; 24 | dstSubfolderSpec = 10; 25 | files = ( 26 | ); 27 | name = "Embed Frameworks"; 28 | runOnlyForDeploymentPostprocessing = 0; 29 | }; 30 | /* End PBXCopyFilesBuildPhase section */ 31 | 32 | /* Begin PBXFileReference section */ 33 | A768AFC62C5C35500055519D /* ScrollableEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollableEditorView.swift; sourceTree = ""; }; 34 | A768AFC82C5C36090055519D /* NotScrollableEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotScrollableEditorView.swift; sourceTree = ""; }; 35 | A768AFCB2C5C37210055519D /* FixedSizeEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FixedSizeEditorView.swift; sourceTree = ""; }; 36 | F91EA2452C3D12B7007489BD /* Example SwiftUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Example SwiftUI.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 37 | F91EA2482C3D12B7007489BD /* Example_SwiftUIApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Example_SwiftUIApp.swift; sourceTree = ""; }; 38 | F91EA24A2C3D12B7007489BD /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; 39 | F91EA24C2C3D12B8007489BD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 40 | F91EA24E2C3D12B8007489BD /* Example_SwiftUI.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Example_SwiftUI.entitlements; sourceTree = ""; }; 41 | /* End PBXFileReference section */ 42 | 43 | /* Begin PBXFrameworksBuildPhase section */ 44 | F91EA2422C3D12B7007489BD /* Frameworks */ = { 45 | isa = PBXFrameworksBuildPhase; 46 | buildActionMask = 2147483647; 47 | files = ( 48 | F90560E82C47B8830015E813 /* InfomaniakRichHTMLEditor in Frameworks */, 49 | ); 50 | runOnlyForDeploymentPostprocessing = 0; 51 | }; 52 | /* End PBXFrameworksBuildPhase section */ 53 | 54 | /* Begin PBXGroup section */ 55 | A768AFC52C5C35370055519D /* Views */ = { 56 | isa = PBXGroup; 57 | children = ( 58 | F91EA24A2C3D12B7007489BD /* RootView.swift */, 59 | A768AFCA2C5C37100055519D /* Editors */, 60 | ); 61 | path = Views; 62 | sourceTree = ""; 63 | }; 64 | A768AFCA2C5C37100055519D /* Editors */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | A768AFC62C5C35500055519D /* ScrollableEditorView.swift */, 68 | A768AFC82C5C36090055519D /* NotScrollableEditorView.swift */, 69 | A768AFCB2C5C37210055519D /* FixedSizeEditorView.swift */, 70 | ); 71 | path = Editors; 72 | sourceTree = ""; 73 | }; 74 | F90560E62C47B8830015E813 /* Frameworks */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | ); 78 | name = Frameworks; 79 | sourceTree = ""; 80 | }; 81 | F91EA23C2C3D12B7007489BD = { 82 | isa = PBXGroup; 83 | children = ( 84 | F91EA2472C3D12B7007489BD /* Example SwiftUI */, 85 | F91EA2462C3D12B7007489BD /* Products */, 86 | F90560E62C47B8830015E813 /* Frameworks */, 87 | ); 88 | sourceTree = ""; 89 | }; 90 | F91EA2462C3D12B7007489BD /* Products */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | F91EA2452C3D12B7007489BD /* Example SwiftUI.app */, 94 | ); 95 | name = Products; 96 | sourceTree = ""; 97 | }; 98 | F91EA2472C3D12B7007489BD /* Example SwiftUI */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | F91EA2482C3D12B7007489BD /* Example_SwiftUIApp.swift */, 102 | A768AFC52C5C35370055519D /* Views */, 103 | F91EA24C2C3D12B8007489BD /* Assets.xcassets */, 104 | F91EA24E2C3D12B8007489BD /* Example_SwiftUI.entitlements */, 105 | ); 106 | path = "Example SwiftUI"; 107 | sourceTree = ""; 108 | }; 109 | /* End PBXGroup section */ 110 | 111 | /* Begin PBXNativeTarget section */ 112 | F91EA2442C3D12B7007489BD /* Example SwiftUI */ = { 113 | isa = PBXNativeTarget; 114 | buildConfigurationList = F91EA2542C3D12B8007489BD /* Build configuration list for PBXNativeTarget "Example SwiftUI" */; 115 | buildPhases = ( 116 | F91EA2412C3D12B7007489BD /* Sources */, 117 | F91EA2422C3D12B7007489BD /* Frameworks */, 118 | F91EA2432C3D12B7007489BD /* Resources */, 119 | F98909AF2C3FAFC300A594B8 /* Embed Frameworks */, 120 | ); 121 | buildRules = ( 122 | ); 123 | dependencies = ( 124 | ); 125 | name = "Example SwiftUI"; 126 | packageProductDependencies = ( 127 | F90560E72C47B8830015E813 /* InfomaniakRichHTMLEditor */, 128 | ); 129 | productName = "Example SwiftUI"; 130 | productReference = F91EA2452C3D12B7007489BD /* Example SwiftUI.app */; 131 | productType = "com.apple.product-type.application"; 132 | }; 133 | /* End PBXNativeTarget section */ 134 | 135 | /* Begin PBXProject section */ 136 | F91EA23D2C3D12B7007489BD /* Project object */ = { 137 | isa = PBXProject; 138 | attributes = { 139 | BuildIndependentTargetsInParallel = 1; 140 | LastSwiftUpdateCheck = 1540; 141 | LastUpgradeCheck = 1540; 142 | TargetAttributes = { 143 | F91EA2442C3D12B7007489BD = { 144 | CreatedOnToolsVersion = 15.4; 145 | }; 146 | }; 147 | }; 148 | buildConfigurationList = F91EA2402C3D12B7007489BD /* Build configuration list for PBXProject "Example SwiftUI" */; 149 | compatibilityVersion = "Xcode 14.0"; 150 | developmentRegion = en; 151 | hasScannedForEncodings = 0; 152 | knownRegions = ( 153 | en, 154 | Base, 155 | ); 156 | mainGroup = F91EA23C2C3D12B7007489BD; 157 | packageReferences = ( 158 | F98F1E3B2C3E8F77007845A8 /* XCRemoteSwiftPackageReference "swift-rich-html-editor" */, 159 | ); 160 | productRefGroup = F91EA2462C3D12B7007489BD /* Products */; 161 | projectDirPath = ""; 162 | projectRoot = ""; 163 | targets = ( 164 | F91EA2442C3D12B7007489BD /* Example SwiftUI */, 165 | ); 166 | }; 167 | /* End PBXProject section */ 168 | 169 | /* Begin PBXResourcesBuildPhase section */ 170 | F91EA2432C3D12B7007489BD /* Resources */ = { 171 | isa = PBXResourcesBuildPhase; 172 | buildActionMask = 2147483647; 173 | files = ( 174 | F91EA24D2C3D12B8007489BD /* Assets.xcassets in Resources */, 175 | ); 176 | runOnlyForDeploymentPostprocessing = 0; 177 | }; 178 | /* End PBXResourcesBuildPhase section */ 179 | 180 | /* Begin PBXSourcesBuildPhase section */ 181 | F91EA2412C3D12B7007489BD /* Sources */ = { 182 | isa = PBXSourcesBuildPhase; 183 | buildActionMask = 2147483647; 184 | files = ( 185 | F91EA24B2C3D12B7007489BD /* RootView.swift in Sources */, 186 | F91EA2492C3D12B7007489BD /* Example_SwiftUIApp.swift in Sources */, 187 | A768AFCC2C5C37210055519D /* FixedSizeEditorView.swift in Sources */, 188 | A768AFC92C5C36090055519D /* NotScrollableEditorView.swift in Sources */, 189 | A768AFC72C5C35500055519D /* ScrollableEditorView.swift in Sources */, 190 | ); 191 | runOnlyForDeploymentPostprocessing = 0; 192 | }; 193 | /* End PBXSourcesBuildPhase section */ 194 | 195 | /* Begin XCBuildConfiguration section */ 196 | F91EA2522C3D12B8007489BD /* Debug */ = { 197 | isa = XCBuildConfiguration; 198 | buildSettings = { 199 | ALWAYS_SEARCH_USER_PATHS = NO; 200 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 201 | CLANG_ANALYZER_NONNULL = YES; 202 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 203 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 204 | CLANG_ENABLE_MODULES = YES; 205 | CLANG_ENABLE_OBJC_ARC = YES; 206 | CLANG_ENABLE_OBJC_WEAK = YES; 207 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 208 | CLANG_WARN_BOOL_CONVERSION = YES; 209 | CLANG_WARN_COMMA = YES; 210 | CLANG_WARN_CONSTANT_CONVERSION = YES; 211 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 212 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 213 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 214 | CLANG_WARN_EMPTY_BODY = YES; 215 | CLANG_WARN_ENUM_CONVERSION = YES; 216 | CLANG_WARN_INFINITE_RECURSION = YES; 217 | CLANG_WARN_INT_CONVERSION = YES; 218 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 219 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 220 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 221 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 222 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 223 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 224 | CLANG_WARN_STRICT_PROTOTYPES = YES; 225 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 226 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 227 | CLANG_WARN_UNREACHABLE_CODE = YES; 228 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 229 | COPY_PHASE_STRIP = NO; 230 | DEBUG_INFORMATION_FORMAT = dwarf; 231 | ENABLE_STRICT_OBJC_MSGSEND = YES; 232 | ENABLE_TESTABILITY = YES; 233 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 234 | GCC_C_LANGUAGE_STANDARD = gnu17; 235 | GCC_DYNAMIC_NO_PIC = NO; 236 | GCC_NO_COMMON_BLOCKS = YES; 237 | GCC_OPTIMIZATION_LEVEL = 0; 238 | GCC_PREPROCESSOR_DEFINITIONS = ( 239 | "DEBUG=1", 240 | "$(inherited)", 241 | ); 242 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 243 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 244 | GCC_WARN_UNDECLARED_SELECTOR = YES; 245 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 246 | GCC_WARN_UNUSED_FUNCTION = YES; 247 | GCC_WARN_UNUSED_VARIABLE = YES; 248 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 249 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 250 | MTL_FAST_MATH = YES; 251 | ONLY_ACTIVE_ARCH = YES; 252 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 253 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 254 | }; 255 | name = Debug; 256 | }; 257 | F91EA2532C3D12B8007489BD /* Release */ = { 258 | isa = XCBuildConfiguration; 259 | buildSettings = { 260 | ALWAYS_SEARCH_USER_PATHS = NO; 261 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 262 | CLANG_ANALYZER_NONNULL = YES; 263 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 264 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 265 | CLANG_ENABLE_MODULES = YES; 266 | CLANG_ENABLE_OBJC_ARC = YES; 267 | CLANG_ENABLE_OBJC_WEAK = YES; 268 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 269 | CLANG_WARN_BOOL_CONVERSION = YES; 270 | CLANG_WARN_COMMA = YES; 271 | CLANG_WARN_CONSTANT_CONVERSION = YES; 272 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 273 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 274 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 275 | CLANG_WARN_EMPTY_BODY = YES; 276 | CLANG_WARN_ENUM_CONVERSION = YES; 277 | CLANG_WARN_INFINITE_RECURSION = YES; 278 | CLANG_WARN_INT_CONVERSION = YES; 279 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 280 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 281 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 282 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 283 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 284 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 285 | CLANG_WARN_STRICT_PROTOTYPES = YES; 286 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 287 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 288 | CLANG_WARN_UNREACHABLE_CODE = YES; 289 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 290 | COPY_PHASE_STRIP = NO; 291 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 292 | ENABLE_NS_ASSERTIONS = NO; 293 | ENABLE_STRICT_OBJC_MSGSEND = YES; 294 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 295 | GCC_C_LANGUAGE_STANDARD = gnu17; 296 | GCC_NO_COMMON_BLOCKS = YES; 297 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 298 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 299 | GCC_WARN_UNDECLARED_SELECTOR = YES; 300 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 301 | GCC_WARN_UNUSED_FUNCTION = YES; 302 | GCC_WARN_UNUSED_VARIABLE = YES; 303 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 304 | MTL_ENABLE_DEBUG_INFO = NO; 305 | MTL_FAST_MATH = YES; 306 | SWIFT_COMPILATION_MODE = wholemodule; 307 | }; 308 | name = Release; 309 | }; 310 | F91EA2552C3D12B8007489BD /* Debug */ = { 311 | isa = XCBuildConfiguration; 312 | buildSettings = { 313 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 314 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 315 | CODE_SIGN_ENTITLEMENTS = "Example SwiftUI/Example_SwiftUI.entitlements"; 316 | CODE_SIGN_STYLE = Automatic; 317 | CURRENT_PROJECT_VERSION = 1; 318 | DEVELOPMENT_TEAM = 864VDCS2QY; 319 | ENABLE_HARDENED_RUNTIME = YES; 320 | ENABLE_PREVIEWS = YES; 321 | GENERATE_INFOPLIST_FILE = YES; 322 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 323 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 324 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 325 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 326 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 327 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 328 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 329 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 330 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 331 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 332 | IPHONEOS_DEPLOYMENT_TARGET = 17.5; 333 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 334 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 335 | MACOSX_DEPLOYMENT_TARGET = 14.5; 336 | MARKETING_VERSION = 1.0; 337 | PRODUCT_BUNDLE_IDENTIFIER = "com.infomaniak.Example-SwiftUI"; 338 | PRODUCT_NAME = "$(TARGET_NAME)"; 339 | SDKROOT = auto; 340 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; 341 | SUPPORTS_MACCATALYST = NO; 342 | SWIFT_EMIT_LOC_STRINGS = YES; 343 | SWIFT_VERSION = 5.0; 344 | TARGETED_DEVICE_FAMILY = "1,2,7"; 345 | }; 346 | name = Debug; 347 | }; 348 | F91EA2562C3D12B8007489BD /* Release */ = { 349 | isa = XCBuildConfiguration; 350 | buildSettings = { 351 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 352 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 353 | CODE_SIGN_ENTITLEMENTS = "Example SwiftUI/Example_SwiftUI.entitlements"; 354 | CODE_SIGN_STYLE = Automatic; 355 | CURRENT_PROJECT_VERSION = 1; 356 | DEVELOPMENT_TEAM = 864VDCS2QY; 357 | ENABLE_HARDENED_RUNTIME = YES; 358 | ENABLE_PREVIEWS = YES; 359 | GENERATE_INFOPLIST_FILE = YES; 360 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 361 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 362 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 363 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 364 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 365 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 366 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 367 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 368 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 369 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 370 | IPHONEOS_DEPLOYMENT_TARGET = 17.5; 371 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 372 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 373 | MACOSX_DEPLOYMENT_TARGET = 14.5; 374 | MARKETING_VERSION = 1.0; 375 | PRODUCT_BUNDLE_IDENTIFIER = "com.infomaniak.Example-SwiftUI"; 376 | PRODUCT_NAME = "$(TARGET_NAME)"; 377 | SDKROOT = auto; 378 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; 379 | SUPPORTS_MACCATALYST = NO; 380 | SWIFT_EMIT_LOC_STRINGS = YES; 381 | SWIFT_VERSION = 5.0; 382 | TARGETED_DEVICE_FAMILY = "1,2,7"; 383 | }; 384 | name = Release; 385 | }; 386 | /* End XCBuildConfiguration section */ 387 | 388 | /* Begin XCConfigurationList section */ 389 | F91EA2402C3D12B7007489BD /* Build configuration list for PBXProject "Example SwiftUI" */ = { 390 | isa = XCConfigurationList; 391 | buildConfigurations = ( 392 | F91EA2522C3D12B8007489BD /* Debug */, 393 | F91EA2532C3D12B8007489BD /* Release */, 394 | ); 395 | defaultConfigurationIsVisible = 0; 396 | defaultConfigurationName = Release; 397 | }; 398 | F91EA2542C3D12B8007489BD /* Build configuration list for PBXNativeTarget "Example SwiftUI" */ = { 399 | isa = XCConfigurationList; 400 | buildConfigurations = ( 401 | F91EA2552C3D12B8007489BD /* Debug */, 402 | F91EA2562C3D12B8007489BD /* Release */, 403 | ); 404 | defaultConfigurationIsVisible = 0; 405 | defaultConfigurationName = Release; 406 | }; 407 | /* End XCConfigurationList section */ 408 | 409 | /* Begin XCRemoteSwiftPackageReference section */ 410 | F98F1E3B2C3E8F77007845A8 /* XCRemoteSwiftPackageReference "swift-rich-html-editor" */ = { 411 | isa = XCRemoteSwiftPackageReference; 412 | repositoryURL = "https://github.com/Infomaniak/swift-rich-html-editor"; 413 | requirement = { 414 | branch = main; 415 | kind = branch; 416 | }; 417 | }; 418 | /* End XCRemoteSwiftPackageReference section */ 419 | 420 | /* Begin XCSwiftPackageProductDependency section */ 421 | F90560E72C47B8830015E813 /* InfomaniakRichHTMLEditor */ = { 422 | isa = XCSwiftPackageProductDependency; 423 | package = F98F1E3B2C3E8F77007845A8 /* XCRemoteSwiftPackageReference "swift-rich-html-editor" */; 424 | productName = InfomaniakRichHTMLEditor; 425 | }; 426 | /* End XCSwiftPackageProductDependency section */ 427 | }; 428 | rootObject = F91EA23D2C3D12B7007489BD /* Project object */; 429 | } 430 | --------------------------------------------------------------------------------