├── .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://swiftpackageindex.com/Infomaniak/swift-rich-html-editor)
4 | [](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 |
13 |
14 | |
15 |
16 |
17 | |
18 |
19 |
20 | |
21 |
22 |
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 |
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 |
--------------------------------------------------------------------------------