├── .gitignore ├── .travis.yml ├── Demo1.gif ├── Demo2.gif ├── Example ├── Podfile ├── Podfile.lock ├── Pods │ ├── Local Podspecs │ │ └── SQRichTextEditor.podspec.json │ ├── Manifest.lock │ ├── Pods.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── Target Support Files │ │ ├── Pods-SQRichTextEditor_Example │ │ ├── Pods-SQRichTextEditor_Example-Info.plist │ │ ├── Pods-SQRichTextEditor_Example-acknowledgements.markdown │ │ ├── Pods-SQRichTextEditor_Example-acknowledgements.plist │ │ ├── Pods-SQRichTextEditor_Example-dummy.m │ │ ├── Pods-SQRichTextEditor_Example-frameworks.sh │ │ ├── Pods-SQRichTextEditor_Example-umbrella.h │ │ ├── Pods-SQRichTextEditor_Example.debug.xcconfig │ │ ├── Pods-SQRichTextEditor_Example.modulemap │ │ └── Pods-SQRichTextEditor_Example.release.xcconfig │ │ ├── Pods-SQRichTextEditor_Tests │ │ ├── Pods-SQRichTextEditor_Tests-Info.plist │ │ ├── Pods-SQRichTextEditor_Tests-acknowledgements.markdown │ │ ├── Pods-SQRichTextEditor_Tests-acknowledgements.plist │ │ ├── Pods-SQRichTextEditor_Tests-dummy.m │ │ ├── Pods-SQRichTextEditor_Tests-umbrella.h │ │ ├── Pods-SQRichTextEditor_Tests.debug.xcconfig │ │ ├── Pods-SQRichTextEditor_Tests.modulemap │ │ └── Pods-SQRichTextEditor_Tests.release.xcconfig │ │ └── SQRichTextEditor │ │ ├── SQRichTextEditor-Info.plist │ │ ├── SQRichTextEditor-dummy.m │ │ ├── SQRichTextEditor-prefix.pch │ │ ├── SQRichTextEditor-umbrella.h │ │ ├── SQRichTextEditor.modulemap │ │ └── SQRichTextEditor.xcconfig ├── SQRichTextEditor.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── SQRichTextEditor-Example.xcscheme ├── SQRichTextEditor.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── SQRichTextEditor │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── LaunchScreen.xib │ │ └── Main.storyboard │ ├── Constants.swift │ ├── EFColorPicker │ │ ├── EFColorComponentView.swift │ │ ├── EFColorSelectionView.swift │ │ ├── EFColorSelectionViewController.swift │ │ ├── EFColorUtils.swift │ │ ├── EFColorView.swift │ │ ├── EFColorWheelView.swift │ │ ├── EFControl.swift │ │ ├── EFHSBView.swift │ │ ├── EFRGBView.swift │ │ ├── EFSliderView.swift │ │ └── EFThumbView.swift │ ├── Images.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Info.plist │ ├── ToolItemCell.swift │ └── ViewController.swift └── Tests │ ├── Info.plist │ └── SQTextEditorViewTests.swift ├── LICENSE ├── README.md ├── SQRichTextEditor.podspec ├── SQRichTextEditor ├── Assets │ └── Editor │ │ ├── editor.css │ │ ├── editor.js │ │ ├── index.html │ │ └── squire-raw.js └── Classes │ ├── Helper.swift │ ├── KeyboardRequiresUserInteraction.swift │ ├── RepeatingTimer.swift │ ├── SQTextAttribute.swift │ └── SQTextEditorView.swift ├── _Pods.xcodeproj └── logo.png /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | # Bundler 23 | .bundle 24 | 25 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 26 | # Carthage/Checkouts 27 | 28 | Carthage/Build 29 | 30 | # We recommend against adding the Pods directory to your .gitignore. However 31 | # you should judge for yourself, the pros and cons are mentioned at: 32 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 33 | # 34 | # Note: if you ignore the Pods directory, make sure to uncomment 35 | # `pod install` in .travis.yml 36 | # 37 | # Pods/ 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: swift 2 | osx_image: xcode11.4 3 | script: 4 | xcodebuild clean build test -enableCodeCoverage YES -workspace Example/SQRichTextEditor.xcworkspace -scheme SQRichTextEditor-Example -sdk iphonesimulator13.4 -destination "platform=iOS Simulator,OS=13.4.1,name=iPhone 11" ONLY_ACTIVE_ARCH=NO CODE_SIGNING_REQUIRED=NO -------------------------------------------------------------------------------- /Demo1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OneupNetwork/SQRichTextEditor/9f859c6f07792a6966da7ff0f3b3523dbd2864ef/Demo1.gif -------------------------------------------------------------------------------- /Demo2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OneupNetwork/SQRichTextEditor/9f859c6f07792a6966da7ff0f3b3523dbd2864ef/Demo2.gif -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | use_frameworks! 2 | 3 | target 'SQRichTextEditor_Example' do 4 | pod 'SQRichTextEditor', :path => '../' 5 | 6 | target 'SQRichTextEditor_Tests' do 7 | inherit! :search_paths 8 | 9 | 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - SQRichTextEditor (0.1.0) 3 | 4 | DEPENDENCIES: 5 | - SQRichTextEditor (from `../`) 6 | 7 | EXTERNAL SOURCES: 8 | SQRichTextEditor: 9 | :path: "../" 10 | 11 | SPEC CHECKSUMS: 12 | SQRichTextEditor: e53d7d05238205a721a9fa7d2a77971e7f732a12 13 | 14 | PODFILE CHECKSUM: fd757c1dd76514f6263576df43948d63a15e3896 15 | 16 | COCOAPODS: 1.8.4 17 | -------------------------------------------------------------------------------- /Example/Pods/Local Podspecs/SQRichTextEditor.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SQRichTextEditor", 3 | "version": "0.1.0", 4 | "summary": "A short description of SQRichTextEditor.", 5 | "description": "TODO: Add long description of the pod here.", 6 | "homepage": "https://github.com/conscientiousness/SQRichTextEditor", 7 | "license": { 8 | "type": "MIT", 9 | "file": "LICENSE" 10 | }, 11 | "authors": { 12 | "conscientiousness": "jesse@gamer.com.tw" 13 | }, 14 | "source": { 15 | "git": "https://github.com/conscientiousness/SQRichTextEditor.git", 16 | "tag": "0.1.0" 17 | }, 18 | "platforms": { 19 | "ios": "8.0" 20 | }, 21 | "source_files": "SQRichTextEditor/Classes/**/*" 22 | } 23 | -------------------------------------------------------------------------------- /Example/Pods/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - SQRichTextEditor (0.1.0) 3 | 4 | DEPENDENCIES: 5 | - SQRichTextEditor (from `../`) 6 | 7 | EXTERNAL SOURCES: 8 | SQRichTextEditor: 9 | :path: "../" 10 | 11 | SPEC CHECKSUMS: 12 | SQRichTextEditor: e53d7d05238205a721a9fa7d2a77971e7f732a12 13 | 14 | PODFILE CHECKSUM: fd757c1dd76514f6263576df43948d63a15e3896 15 | 16 | COCOAPODS: 1.8.4 17 | -------------------------------------------------------------------------------- /Example/Pods/Pods.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /Example/Pods/Pods.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-SQRichTextEditor_Example/Pods-SQRichTextEditor_Example-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-SQRichTextEditor_Example/Pods-SQRichTextEditor_Example-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | 4 | ## SQRichTextEditor 5 | 6 | Copyright (c) 2019 conscientiousness 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | 26 | Generated by CocoaPods - https://cocoapods.org 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-SQRichTextEditor_Example/Pods-SQRichTextEditor_Example-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | Copyright (c) 2019 conscientiousness <jesse@gamer.com.tw> 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy 20 | of this software and associated documentation files (the "Software"), to deal 21 | in the Software without restriction, including without limitation the rights 22 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 23 | copies of the Software, and to permit persons to whom the Software is 24 | furnished to do so, subject to the following conditions: 25 | 26 | The above copyright notice and this permission notice shall be included in 27 | all copies or substantial portions of the Software. 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 35 | THE SOFTWARE. 36 | 37 | License 38 | MIT 39 | Title 40 | SQRichTextEditor 41 | Type 42 | PSGroupSpecifier 43 | 44 | 45 | FooterText 46 | Generated by CocoaPods - https://cocoapods.org 47 | Title 48 | 49 | Type 50 | PSGroupSpecifier 51 | 52 | 53 | StringsTable 54 | Acknowledgements 55 | Title 56 | Acknowledgements 57 | 58 | 59 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-SQRichTextEditor_Example/Pods-SQRichTextEditor_Example-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_SQRichTextEditor_Example : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_SQRichTextEditor_Example 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-SQRichTextEditor_Example/Pods-SQRichTextEditor_Example-frameworks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -u 4 | set -o pipefail 5 | 6 | function on_error { 7 | echo "$(realpath -mq "${0}"):$1: error: Unexpected failure" 8 | } 9 | trap 'on_error $LINENO' ERR 10 | 11 | if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then 12 | # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy 13 | # frameworks to, so exit 0 (signalling the script phase was successful). 14 | exit 0 15 | fi 16 | 17 | echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 18 | mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 19 | 20 | COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" 21 | SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" 22 | 23 | # Used as a return value for each invocation of `strip_invalid_archs` function. 24 | STRIP_BINARY_RETVAL=0 25 | 26 | # This protects against multiple targets copying the same framework dependency at the same time. The solution 27 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html 28 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") 29 | 30 | # Copies and strips a vendored framework 31 | install_framework() 32 | { 33 | if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then 34 | local source="${BUILT_PRODUCTS_DIR}/$1" 35 | elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then 36 | local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" 37 | elif [ -r "$1" ]; then 38 | local source="$1" 39 | fi 40 | 41 | local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 42 | 43 | if [ -L "${source}" ]; then 44 | echo "Symlinked..." 45 | source="$(readlink "${source}")" 46 | fi 47 | 48 | # Use filter instead of exclude so missing patterns don't throw errors. 49 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" 50 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" 51 | 52 | local basename 53 | basename="$(basename -s .framework "$1")" 54 | binary="${destination}/${basename}.framework/${basename}" 55 | 56 | if ! [ -r "$binary" ]; then 57 | binary="${destination}/${basename}" 58 | elif [ -L "${binary}" ]; then 59 | echo "Destination binary is symlinked..." 60 | dirname="$(dirname "${binary}")" 61 | binary="${dirname}/$(readlink "${binary}")" 62 | fi 63 | 64 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 65 | if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then 66 | strip_invalid_archs "$binary" 67 | fi 68 | 69 | # Resign the code if required by the build settings to avoid unstable apps 70 | code_sign_if_enabled "${destination}/$(basename "$1")" 71 | 72 | # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. 73 | if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then 74 | local swift_runtime_libs 75 | swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u) 76 | for lib in $swift_runtime_libs; do 77 | echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" 78 | rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" 79 | code_sign_if_enabled "${destination}/${lib}" 80 | done 81 | fi 82 | } 83 | 84 | # Copies and strips a vendored dSYM 85 | install_dsym() { 86 | local source="$1" 87 | if [ -r "$source" ]; then 88 | # Copy the dSYM into a the targets temp dir. 89 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" 90 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" 91 | 92 | local basename 93 | basename="$(basename -s .framework.dSYM "$source")" 94 | binary="${DERIVED_FILES_DIR}/${basename}.framework.dSYM/Contents/Resources/DWARF/${basename}" 95 | 96 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 97 | if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then 98 | strip_invalid_archs "$binary" 99 | fi 100 | 101 | if [[ $STRIP_BINARY_RETVAL == 1 ]]; then 102 | # Move the stripped file into its final destination. 103 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" 104 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.framework.dSYM" "${DWARF_DSYM_FOLDER_PATH}" 105 | else 106 | # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. 107 | touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.framework.dSYM" 108 | fi 109 | fi 110 | } 111 | 112 | # Copies the bcsymbolmap files of a vendored framework 113 | install_bcsymbolmap() { 114 | local bcsymbolmap_path="$1" 115 | local destination="${BUILT_PRODUCTS_DIR}" 116 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"" 117 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}" 118 | } 119 | 120 | # Signs a framework with the provided identity 121 | code_sign_if_enabled() { 122 | if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then 123 | # Use the current code_sign_identity 124 | echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" 125 | local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" 126 | 127 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 128 | code_sign_cmd="$code_sign_cmd &" 129 | fi 130 | echo "$code_sign_cmd" 131 | eval "$code_sign_cmd" 132 | fi 133 | } 134 | 135 | # Strip invalid architectures 136 | strip_invalid_archs() { 137 | binary="$1" 138 | # Get architectures for current target binary 139 | binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" 140 | # Intersect them with the architectures we are building for 141 | intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" 142 | # If there are no archs supported by this binary then warn the user 143 | if [[ -z "$intersected_archs" ]]; then 144 | echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." 145 | STRIP_BINARY_RETVAL=0 146 | return 147 | fi 148 | stripped="" 149 | for arch in $binary_archs; do 150 | if ! [[ "${ARCHS}" == *"$arch"* ]]; then 151 | # Strip non-valid architectures in-place 152 | lipo -remove "$arch" -output "$binary" "$binary" 153 | stripped="$stripped $arch" 154 | fi 155 | done 156 | if [[ "$stripped" ]]; then 157 | echo "Stripped $binary of architectures:$stripped" 158 | fi 159 | STRIP_BINARY_RETVAL=1 160 | } 161 | 162 | 163 | if [[ "$CONFIGURATION" == "Debug" ]]; then 164 | install_framework "${BUILT_PRODUCTS_DIR}/SQRichTextEditor/SQRichTextEditor.framework" 165 | fi 166 | if [[ "$CONFIGURATION" == "Release" ]]; then 167 | install_framework "${BUILT_PRODUCTS_DIR}/SQRichTextEditor/SQRichTextEditor.framework" 168 | fi 169 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 170 | wait 171 | fi 172 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-SQRichTextEditor_Example/Pods-SQRichTextEditor_Example-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_SQRichTextEditor_ExampleVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_SQRichTextEditor_ExampleVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-SQRichTextEditor_Example/Pods-SQRichTextEditor_Example.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SQRichTextEditor" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SQRichTextEditor/SQRichTextEditor.framework/Headers" 5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 6 | OTHER_LDFLAGS = $(inherited) -framework "SQRichTextEditor" 7 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 11 | PODS_ROOT = ${SRCROOT}/Pods 12 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 13 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-SQRichTextEditor_Example/Pods-SQRichTextEditor_Example.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_SQRichTextEditor_Example { 2 | umbrella header "Pods-SQRichTextEditor_Example-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-SQRichTextEditor_Example/Pods-SQRichTextEditor_Example.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SQRichTextEditor" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SQRichTextEditor/SQRichTextEditor.framework/Headers" 5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 6 | OTHER_LDFLAGS = $(inherited) -framework "SQRichTextEditor" 7 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 11 | PODS_ROOT = ${SRCROOT}/Pods 12 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 13 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-SQRichTextEditor_Tests/Pods-SQRichTextEditor_Tests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-SQRichTextEditor_Tests/Pods-SQRichTextEditor_Tests-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | Generated by CocoaPods - https://cocoapods.org 4 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-SQRichTextEditor_Tests/Pods-SQRichTextEditor_Tests-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | Generated by CocoaPods - https://cocoapods.org 18 | Title 19 | 20 | Type 21 | PSGroupSpecifier 22 | 23 | 24 | StringsTable 25 | Acknowledgements 26 | Title 27 | Acknowledgements 28 | 29 | 30 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-SQRichTextEditor_Tests/Pods-SQRichTextEditor_Tests-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_SQRichTextEditor_Tests : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_SQRichTextEditor_Tests 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-SQRichTextEditor_Tests/Pods-SQRichTextEditor_Tests-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_SQRichTextEditor_TestsVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_SQRichTextEditor_TestsVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-SQRichTextEditor_Tests/Pods-SQRichTextEditor_Tests.debug.xcconfig: -------------------------------------------------------------------------------- 1 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SQRichTextEditor" 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SQRichTextEditor/SQRichTextEditor.framework/Headers" 4 | OTHER_LDFLAGS = $(inherited) -framework "SQRichTextEditor" 5 | PODS_BUILD_DIR = ${BUILD_DIR} 6 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 7 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 8 | PODS_ROOT = ${SRCROOT}/Pods 9 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 10 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-SQRichTextEditor_Tests/Pods-SQRichTextEditor_Tests.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_SQRichTextEditor_Tests { 2 | umbrella header "Pods-SQRichTextEditor_Tests-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-SQRichTextEditor_Tests/Pods-SQRichTextEditor_Tests.release.xcconfig: -------------------------------------------------------------------------------- 1 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SQRichTextEditor" 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SQRichTextEditor/SQRichTextEditor.framework/Headers" 4 | OTHER_LDFLAGS = $(inherited) -framework "SQRichTextEditor" 5 | PODS_BUILD_DIR = ${BUILD_DIR} 6 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 7 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 8 | PODS_ROOT = ${SRCROOT}/Pods 9 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 10 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/SQRichTextEditor/SQRichTextEditor-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 0.1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/SQRichTextEditor/SQRichTextEditor-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_SQRichTextEditor : NSObject 3 | @end 4 | @implementation PodsDummy_SQRichTextEditor 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/SQRichTextEditor/SQRichTextEditor-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/SQRichTextEditor/SQRichTextEditor-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double SQRichTextEditorVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char SQRichTextEditorVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/SQRichTextEditor/SQRichTextEditor.modulemap: -------------------------------------------------------------------------------- 1 | framework module SQRichTextEditor { 2 | umbrella header "SQRichTextEditor-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/SQRichTextEditor/SQRichTextEditor.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SQRichTextEditor 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 4 | PODS_BUILD_DIR = ${BUILD_DIR} 5 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 6 | PODS_ROOT = ${SRCROOT} 7 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../.. 8 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 9 | SKIP_INSTALL = YES 10 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 11 | -------------------------------------------------------------------------------- /Example/SQRichTextEditor.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/SQRichTextEditor.xcodeproj/xcshareddata/xcschemes/SQRichTextEditor-Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 46 | 47 | 53 | 54 | 55 | 56 | 62 | 63 | 64 | 65 | 67 | 73 | 74 | 75 | 76 | 77 | 87 | 89 | 95 | 96 | 97 | 98 | 104 | 106 | 112 | 113 | 114 | 115 | 117 | 118 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /Example/SQRichTextEditor.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/SQRichTextEditor.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/SQRichTextEditor/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SQRichTextEditor 4 | // 5 | // Created by conscientiousness on 12/09/2019. 6 | // Copyright (c) 2019 conscientiousness. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | func applicationWillResignActive(_ application: UIApplication) { 22 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 23 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 24 | } 25 | 26 | func applicationDidEnterBackground(_ application: UIApplication) { 27 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 28 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 29 | } 30 | 31 | func applicationWillEnterForeground(_ application: UIApplication) { 32 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 33 | } 34 | 35 | func applicationDidBecomeActive(_ application: UIApplication) { 36 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 37 | } 38 | 39 | func applicationWillTerminate(_ application: UIApplication) { 40 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 41 | } 42 | 43 | 44 | } 45 | 46 | -------------------------------------------------------------------------------- /Example/SQRichTextEditor/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 22 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Example/SQRichTextEditor/Base.lproj/Main.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 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Example/SQRichTextEditor/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // SQRichTextEditor_Example 4 | // 5 | // Created by Jesse on 2019/12/13. 6 | // Copyright © 2019 CocoaPods. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SQRichTextEditor 11 | 12 | enum ToolItemCellSettings { 13 | static let id = "toolItemCell" 14 | 15 | static let normalfont = UIFont.systemFont(ofSize: 14, weight: .light) 16 | static let activeFont = UIFont.systemFont(ofSize: 14, weight: .bold) 17 | 18 | static let normalColor = UIColor.black 19 | static let activeColor = UIColor.blue 20 | 21 | static let height: CGFloat = 35 22 | } 23 | 24 | enum ToolOptionType: Int, CustomStringConvertible, CaseIterable { 25 | case bold 26 | case italic 27 | case strikethrough 28 | case underline 29 | case clear 30 | case makeLink 31 | case removeLink 32 | case insertImage 33 | case setTextColor 34 | case setTextBackgroundColor 35 | case setTextSize 36 | case insertHTML 37 | case getHTML 38 | case focusEditor 39 | case blurEditor 40 | case getHeight 41 | 42 | var description: String { 43 | switch self { 44 | case .bold: 45 | return "Bold" 46 | case .italic: 47 | return "Italic" 48 | case .strikethrough: 49 | return "Strikethrough" 50 | case .underline: 51 | return "Underline" 52 | case .clear: 53 | return "Clear" 54 | case .makeLink: 55 | return "Make Link" 56 | case .removeLink: 57 | return "Remove Link" 58 | case .insertImage: 59 | return "Insert Image" 60 | case .setTextColor: 61 | return "Text Color" 62 | case .setTextBackgroundColor: 63 | return "Text Background Color" 64 | case .setTextSize: 65 | return "Text Size(default)" 66 | case .insertHTML: 67 | return "Insert HTML" 68 | case .getHTML: 69 | return "Get HTML" 70 | case .focusEditor: 71 | return "focusEditor" 72 | case .blurEditor: 73 | return "blurEditor" 74 | case .getHeight: 75 | return "Get Editor Height" 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Example/SQRichTextEditor/EFColorPicker/EFColorComponentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EFColorComponentView.swift 3 | // EFColorPicker 4 | // 5 | // Created by EyreFree on 2017/9/28. 6 | // 7 | // Copyright (c) 2017 EyreFree 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import UIKit 28 | 29 | // The view to edit a color component. 30 | public class EFColorComponentView: UIControl, UITextFieldDelegate { 31 | 32 | // Indicates if the user touches the control at the moment 33 | var isTouched: Bool { 34 | return slider.isTouched 35 | } 36 | 37 | // Temporary disabled the color component editing via text field 38 | public var colorTextFieldEnabled: Bool = false { 39 | didSet { 40 | if textField.isHidden != !colorTextFieldEnabled { 41 | ef_remakeConstraints() 42 | textField.isHidden = !colorTextFieldEnabled 43 | } 44 | } 45 | } 46 | 47 | let EFColorComponentViewSpacing: CGFloat = 5.0 48 | let EFColorComponentLabelWidth: CGFloat = 60.0 49 | let EFColorComponentTextFieldWidth: CGFloat = 50.0 50 | 51 | // The title. 52 | var title: String { 53 | get { 54 | return label.text ?? "" 55 | } 56 | set { 57 | label.text = newValue 58 | } 59 | } 60 | 61 | // The current value. The default value is 0.0. 62 | var value: CGFloat { 63 | get { 64 | return slider.value 65 | } 66 | set { 67 | slider.setValue(value: newValue) 68 | textField.text = String(format: format, value) 69 | } 70 | } 71 | 72 | // The minimum value. The default value is 0.0. 73 | var minimumValue: CGFloat { 74 | get { 75 | return slider.minimumValue 76 | } 77 | set { 78 | slider.minimumValue = newValue 79 | } 80 | } 81 | 82 | // The maximum value. The default value is 255.0. 83 | var maximumValue: CGFloat { 84 | get { 85 | return slider.maximumValue 86 | } 87 | set { 88 | slider.maximumValue = newValue 89 | } 90 | } 91 | 92 | // The format string to use apply for textfield value. \c %.f by default. 93 | var format: String = "%.f" 94 | 95 | private let label: UILabel = UILabel() 96 | private let slider: EFSliderView = EFSliderView() // The color slider to edit color component. 97 | private let textField: UITextField = UITextField() 98 | 99 | override open class var requiresConstraintBasedLayout: Bool { 100 | get { 101 | return true 102 | } 103 | } 104 | 105 | override init(frame: CGRect) { 106 | super.init(frame: frame) 107 | ef_baseInit() 108 | } 109 | 110 | required public init?(coder aDecoder: NSCoder) { 111 | super.init(coder: aDecoder) 112 | ef_baseInit() 113 | } 114 | 115 | // MARK:- UITextFieldDelegate methods 116 | public func textFieldDidEndEditing(_ textField: UITextField) { 117 | self.value = CGFloat(Double(textField.text ?? "") ?? 0) 118 | self.sendActions(for: UIControl.Event.valueChanged) 119 | } 120 | 121 | public func textFieldShouldReturn(_ textField: UITextField) -> Bool { 122 | textField.resignFirstResponder() 123 | return true 124 | } 125 | 126 | public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { 127 | let newString = NSString(string: textField.text ?? "").replacingCharacters(in: range, with: string) 128 | //first, check if the new string is numeric only. If not, return NO; 129 | let characterSet = NSCharacterSet(charactersIn: "0123456789,.").inverted 130 | if !(newString.rangeOfCharacter(from: characterSet)?.isEmpty != false) { 131 | return false 132 | } 133 | return CGFloat(Double(newString) ?? 0) <= slider.maximumValue 134 | } 135 | 136 | // Sets the array of CGColorRef objects defining the color of each gradient stop on a slider's track. 137 | // The location of each gradient stop is evaluated with formula: i * width_of_the_track / number_of_colors. 138 | // @param colors An array of CGColorRef objects. 139 | func setColors(colors: [UIColor]) { 140 | 141 | if colors.count <= 1 { 142 | fatalError("‘colors: [CGColor]’ at least need to have 2 elements") 143 | } 144 | 145 | slider.setColors(colors: colors) 146 | } 147 | 148 | // MARK:- Private methods 149 | private func ef_baseInit() { 150 | self.accessibilityLabel = "color_component_view" 151 | 152 | label.translatesAutoresizingMaskIntoConstraints = false 153 | label.adjustsFontSizeToFitWidth = true 154 | self.addSubview(label) 155 | 156 | slider.maximumValue = EFRGBColorComponentMaxValue 157 | slider.translatesAutoresizingMaskIntoConstraints = false 158 | self.addSubview(slider) 159 | 160 | textField.borderStyle = UITextField.BorderStyle.roundedRect 161 | textField.translatesAutoresizingMaskIntoConstraints = false 162 | textField.keyboardType = UIKeyboardType.numbersAndPunctuation 163 | textField.isHidden = !colorTextFieldEnabled 164 | self.addSubview(textField) 165 | 166 | self.value = 0.0 167 | 168 | slider.addTarget(self, action: #selector(ef_didChangeSliderValue(sender:)), for: UIControl.Event.valueChanged) 169 | textField.delegate = self 170 | 171 | self.ef_installConstraints() 172 | } 173 | 174 | @objc private func ef_didChangeSliderValue(sender: EFSliderView) { 175 | self.value = sender.value 176 | self.sendActions(for: UIControl.Event.valueChanged) 177 | } 178 | 179 | private func ef_installConstraints() { 180 | if colorTextFieldEnabled { 181 | let views: [String : Any] = [ 182 | "label" : label, 183 | "slider" : slider, 184 | "textField" : textField 185 | ] 186 | let metrics: [String : Any] = [ 187 | "spacing" : EFColorComponentViewSpacing, 188 | "label_width" : EFColorComponentLabelWidth, 189 | "textfield_width" : EFColorComponentTextFieldWidth 190 | ] 191 | 192 | self.addConstraints( 193 | NSLayoutConstraint.constraints( 194 | withVisualFormat: "H:|[label(label_width)]-spacing-[slider]-spacing-[textField(textfield_width)]|", 195 | options: NSLayoutConstraint.FormatOptions.alignAllCenterY, 196 | metrics: metrics, 197 | views: views 198 | ) 199 | ) 200 | self.addConstraints( 201 | NSLayoutConstraint.constraints( 202 | withVisualFormat: "V:|[label]|", 203 | options: NSLayoutConstraint.FormatOptions(rawValue: 0), 204 | metrics: nil, 205 | views: views 206 | ) 207 | ) 208 | self.addConstraints( 209 | NSLayoutConstraint.constraints( 210 | withVisualFormat: "V:|[textField]|", 211 | options: NSLayoutConstraint.FormatOptions(rawValue: 0), 212 | metrics: nil, 213 | views: views 214 | ) 215 | ) 216 | } else { 217 | let views: [String : Any] = [ 218 | "label" : label, 219 | "slider" : slider 220 | ] 221 | let metrics: [String : Any] = [ 222 | "spacing" : EFColorComponentViewSpacing, 223 | "label_width" : EFColorComponentLabelWidth 224 | ] 225 | self.addConstraints( 226 | NSLayoutConstraint.constraints( 227 | withVisualFormat: "H:|[label(label_width)]-spacing-[slider]-spacing-|", 228 | options: NSLayoutConstraint.FormatOptions.alignAllCenterY, 229 | metrics: metrics, 230 | views: views 231 | ) 232 | ) 233 | self.addConstraints( 234 | NSLayoutConstraint.constraints( 235 | withVisualFormat: "V:|[label]|", 236 | options: NSLayoutConstraint.FormatOptions(rawValue: 0), 237 | metrics: nil, 238 | views: views 239 | ) 240 | ) 241 | } 242 | } 243 | 244 | private func ef_remakeConstraints() { 245 | // Remove all old constraints 246 | if !colorTextFieldEnabled { 247 | let views: [String : Any] = [ 248 | "label" : label, 249 | "slider" : slider, 250 | "textField" : textField 251 | ] 252 | let metrics: [String : Any] = [ 253 | "spacing" : EFColorComponentViewSpacing, 254 | "label_width" : EFColorComponentLabelWidth, 255 | "textfield_width" : EFColorComponentTextFieldWidth 256 | ] 257 | self.removeConstraints( 258 | NSLayoutConstraint.constraints( 259 | withVisualFormat: "H:|[label(label_width)]-spacing-[slider]-spacing-[textField(textfield_width)]|", 260 | options: NSLayoutConstraint.FormatOptions.alignAllCenterY, 261 | metrics: metrics, 262 | views: views 263 | ) 264 | ) 265 | self.removeConstraints( 266 | NSLayoutConstraint.constraints( 267 | withVisualFormat: "V:|[label]|", 268 | options: NSLayoutConstraint.FormatOptions(rawValue: 0), 269 | metrics: nil, 270 | views: views 271 | ) 272 | ) 273 | self.removeConstraints( 274 | NSLayoutConstraint.constraints( 275 | withVisualFormat: "V:|[textField]|", 276 | options: NSLayoutConstraint.FormatOptions(rawValue: 0), 277 | metrics: nil, 278 | views: views 279 | ) 280 | ) 281 | } else { 282 | let views: [String : Any] = [ 283 | "label" : label, 284 | "slider" : slider 285 | ] 286 | let metrics: [String : Any] = [ 287 | "spacing" : EFColorComponentViewSpacing, 288 | "label_width" : EFColorComponentLabelWidth 289 | ] 290 | self.removeConstraints( 291 | NSLayoutConstraint.constraints( 292 | withVisualFormat: "H:|[label(label_width)]-spacing-[slider]-spacing-|", 293 | options: NSLayoutConstraint.FormatOptions.alignAllCenterY, 294 | metrics: metrics, 295 | views: views 296 | ) 297 | ) 298 | self.removeConstraints( 299 | NSLayoutConstraint.constraints( 300 | withVisualFormat: "V:|[label]|", 301 | options: NSLayoutConstraint.FormatOptions(rawValue: 0), 302 | metrics: nil, 303 | views: views 304 | ) 305 | ) 306 | } 307 | 308 | // Readd control 309 | for control in [label, slider, textField] { 310 | control.removeFromSuperview() 311 | self.addSubview(control) 312 | } 313 | 314 | // Add new constraints 315 | ef_installConstraints() 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /Example/SQRichTextEditor/EFColorPicker/EFColorSelectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EFColorSelectionView.swift 3 | // EFColorPicker 4 | // 5 | // Created by EyreFree on 2017/9/29. 6 | // 7 | // Copyright (c) 2017 EyreFree 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Foundation 28 | import UIKit 29 | 30 | // The enum to define the EFColorView's types. 31 | public enum EFSelectedColorView: Int { 32 | // The RGB color view type. 33 | case RGB = 0 34 | 35 | // The HSB color view type. 36 | case HSB = 1 37 | } 38 | 39 | // The EFColorSelectionView aggregates views that should be used to edit color components. 40 | public class EFColorSelectionView: UIView, EFColorView, EFColorViewDelegate { 41 | 42 | // The selected color view 43 | private(set) var selectedIndex: EFSelectedColorView = EFSelectedColorView.RGB 44 | 45 | let rgbColorView: EFRGBView = EFRGBView() 46 | let hsbColorView: EFHSBView = EFHSBView() 47 | 48 | weak public var delegate: EFColorViewDelegate? 49 | 50 | public var color: UIColor = UIColor.white { 51 | didSet { 52 | self.selectedView()?.color = color 53 | } 54 | } 55 | 56 | override init(frame: CGRect) { 57 | super.init(frame: frame) 58 | self.ef_init() 59 | } 60 | 61 | required public init?(coder aDecoder: NSCoder) { 62 | super.init(coder: aDecoder) 63 | self.ef_init() 64 | } 65 | 66 | // Makes a color component view (rgb or hsb) visible according to the index. 67 | // @param index This index define a view to show. 68 | // @param animated If YES, the view is being appeared using an animation. 69 | func setSelectedIndex(index: EFSelectedColorView, animated: Bool) { 70 | self.selectedIndex = index 71 | self.selectedView()?.color = self.color 72 | UIView.animate(withDuration: animated ? 0.5 : 0.0) { 73 | [weak self] in 74 | if let strongSelf = self { 75 | strongSelf.rgbColorView.alpha = EFSelectedColorView.RGB == index ? 1.0 : 0.0 76 | strongSelf.hsbColorView.alpha = EFSelectedColorView.HSB == index ? 1.0 : 0.0 77 | } 78 | } 79 | } 80 | 81 | func selectedView() -> EFColorView? { 82 | return (EFSelectedColorView.RGB == self.selectedIndex ? self.rgbColorView : self.hsbColorView) as? EFColorView 83 | } 84 | 85 | func addColorView(view: EFColorView) { 86 | view.delegate = self 87 | if let view = view as? UIView { 88 | self.addSubview(view) 89 | view.translatesAutoresizingMaskIntoConstraints = false 90 | let views = [ 91 | "view" : view 92 | ] 93 | let visualFormats = [ 94 | "H:|[view]|", 95 | "V:|[view]|" 96 | ] 97 | for visualFormat in visualFormats { 98 | self.addConstraints( 99 | NSLayoutConstraint.constraints( 100 | withVisualFormat: visualFormat, 101 | options: NSLayoutConstraint.FormatOptions(rawValue: 0), 102 | metrics: nil, 103 | views: views 104 | ) 105 | ) 106 | } 107 | } 108 | } 109 | 110 | override public func updateConstraints() { 111 | self.rgbColorView.setNeedsUpdateConstraints() 112 | self.hsbColorView.setNeedsUpdateConstraints() 113 | super.updateConstraints() 114 | } 115 | 116 | // MARK:- FBColorViewDelegate methods 117 | public func colorView(_ colorView: EFColorView, didChangeColor color: UIColor) { 118 | self.color = color 119 | self.delegate?.colorView(self, didChangeColor: self.color) 120 | } 121 | 122 | // MARK:- Private 123 | private func ef_init() { 124 | if #available(iOS 11.0, *) { 125 | self.accessibilityIgnoresInvertColors = true 126 | } 127 | self.accessibilityLabel = "color_selection_view" 128 | 129 | self.backgroundColor = UIColor.white 130 | self.addColorView(view: rgbColorView) 131 | self.addColorView(view: hsbColorView) 132 | self.setSelectedIndex(index: EFSelectedColorView.RGB, animated: false) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Example/SQRichTextEditor/EFColorPicker/EFColorSelectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EFColorSelectionViewController.swift 3 | // EFColorPicker 4 | // 5 | // Created by EyreFree on 2017/9/29. 6 | // 7 | // Copyright (c) 2017 EyreFree 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import UIKit 28 | 29 | public enum EFColorSelectionMode: Int { 30 | case all = 0 31 | case rgb = 1 32 | case hsb = 2 33 | } 34 | 35 | // The delegate of a EFColorSelectionViewController object must adopt the EFColorSelectionViewController protocol. 36 | // Methods of the protocol allow the delegate to handle color value changes. 37 | @objc public protocol EFColorSelectionViewControllerDelegate: NSObjectProtocol { 38 | 39 | // Tells the data source to return the color components. 40 | // @param colorViewCntroller The color view. 41 | // @param color The new color value. 42 | func colorViewController(_ colorViewCntroller: EFColorSelectionViewController, didChangeColor color: UIColor) 43 | } 44 | 45 | public class EFColorSelectionViewController: UIViewController, EFColorViewDelegate { 46 | 47 | // The controller's delegate. Controller notifies a delegate on color change. 48 | public weak var delegate: EFColorSelectionViewControllerDelegate? 49 | 50 | // The current color value. 51 | public var color: UIColor { 52 | get { 53 | return self.colorSelectionView().color 54 | } 55 | set { 56 | self.colorSelectionView().color = newValue 57 | } 58 | } 59 | 60 | // Whether colorTextField will hide, default is `true` 61 | public var isColorTextFieldHidden: Bool { 62 | get { 63 | return !((self.view as? EFColorSelectionView)?.hsbColorView.brightnessView.colorTextFieldEnabled ?? false) 64 | } 65 | set { 66 | if let colorSelectionView = self.view as? EFColorSelectionView, 67 | colorSelectionView.hsbColorView.brightnessView.colorTextFieldEnabled != !newValue { 68 | colorSelectionView.hsbColorView.brightnessView.colorTextFieldEnabled = !newValue 69 | 70 | for colorComponentView in colorSelectionView.rgbColorView.colorComponentViews { 71 | colorComponentView.colorTextFieldEnabled = !newValue 72 | } 73 | } 74 | } 75 | } 76 | 77 | override public func loadView() { 78 | let colorSelectionView: EFColorSelectionView = EFColorSelectionView(frame: UIScreen.main.bounds) 79 | self.view = colorSelectionView 80 | } 81 | 82 | override public func viewDidLoad() { 83 | super.viewDidLoad() 84 | 85 | let segmentControl: UISegmentedControl = UISegmentedControl( 86 | items: [NSLocalizedString("RGB", comment: ""), NSLocalizedString("HSB", comment: "")] 87 | ) 88 | segmentControl.addTarget( 89 | self, 90 | action: #selector(segmentControlDidChangeValue(_:)), 91 | for: UIControl.Event.valueChanged 92 | ) 93 | segmentControl.selectedSegmentIndex = 0 94 | self.navigationItem.titleView = segmentControl 95 | 96 | self.colorSelectionView().setSelectedIndex(index: EFSelectedColorView.RGB, animated: false) 97 | self.colorSelectionView().delegate = self 98 | self.edgesForExtendedLayout = UIRectEdge(rawValue: 0) 99 | } 100 | 101 | public func setMode(mode: EFColorSelectionMode) { 102 | guard let segmentControl: UISegmentedControl = self.navigationItem.titleView as? UISegmentedControl else { return } 103 | switch mode { 104 | case .rgb: 105 | segmentControl.isHidden = true 106 | segmentControl.selectedSegmentIndex = 0 107 | self.colorSelectionView().setSelectedIndex(index: EFSelectedColorView.RGB, animated: false) 108 | case .hsb: 109 | segmentControl.isHidden = true 110 | segmentControl.selectedSegmentIndex = 1 111 | self.colorSelectionView().setSelectedIndex(index: EFSelectedColorView.HSB, animated: false) 112 | default: 113 | segmentControl.isHidden = false 114 | } 115 | } 116 | 117 | @IBAction func segmentControlDidChangeValue(_ segmentedControl: UISegmentedControl) { 118 | self.colorSelectionView().setSelectedIndex( 119 | index: EFSelectedColorView(rawValue: segmentedControl.selectedSegmentIndex) ?? EFSelectedColorView.RGB, 120 | animated: true 121 | ) 122 | } 123 | 124 | override public func viewWillLayoutSubviews() { 125 | self.colorSelectionView().setNeedsUpdateConstraints() 126 | self.colorSelectionView().updateConstraintsIfNeeded() 127 | } 128 | 129 | func colorSelectionView() -> EFColorSelectionView { 130 | return self.view as? EFColorSelectionView ?? EFColorSelectionView() 131 | } 132 | 133 | // MARK:- EFColorViewDelegate 134 | public func colorView(_ colorView: EFColorView, didChangeColor color: UIColor) { 135 | self.delegate?.colorViewController(self, didChangeColor: color) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Example/SQRichTextEditor/EFColorPicker/EFColorUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EFColorUtils.swift 3 | // EFColorPicker 4 | // 5 | // Created by EyreFree on 2017/9/28. 6 | // 7 | // Copyright (c) 2017 EyreFree 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import UIKit 28 | import CoreGraphics 29 | 30 | private let colorComponentValueRange = (CGFloat(0.0) ... CGFloat(1.0)) 31 | 32 | // The structure to represent a color in the Red-Green-Blue-Alpha color space. 33 | struct RGB { 34 | var red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat 35 | 36 | init(_ red: CGFloat, _ green: CGFloat, _ blue: CGFloat, _ alpha: CGFloat) { 37 | self.red = red 38 | self.green = green 39 | self.blue = blue 40 | self.alpha = alpha 41 | } 42 | } 43 | 44 | // The structure to represent a color in the hue-saturation-brightness color space. 45 | struct HSB { 46 | var hue: CGFloat, saturation: CGFloat, brightness: CGFloat, alpha: CGFloat 47 | 48 | init(_ hue: CGFloat, _ saturation: CGFloat, _ brightness: CGFloat, _ alpha: CGFloat) { 49 | self.hue = hue 50 | self.saturation = saturation 51 | self.brightness = brightness 52 | self.alpha = alpha 53 | } 54 | } 55 | 56 | // The maximum value of the RGB color components. 57 | let EFRGBColorComponentMaxValue: CGFloat = 255.0 58 | 59 | // The maximum value of the alpha component. 60 | let EFAlphaComponentMaxValue: CGFloat = 100.0 61 | 62 | // The maximum value of the HSB color components. 63 | let EFHSBColorComponentMaxValue: CGFloat = 1.0 64 | 65 | // Converts an RGB color value to HSV. 66 | // Assumes r, g, and b are contained in the set [0, 1] and 67 | // returns h, s, and b in the set [0, 1]. 68 | // @param rgb The rgb color values 69 | // @return The hsb color values 70 | func EFRGB2HSB(rgb: RGB) -> HSB { 71 | let rd = Double(rgb.red) 72 | let gd = Double(rgb.green) 73 | let bd = Double(rgb.blue) 74 | let max = fmax (rd, fmax(gd, bd)) 75 | let min = fmin(rd, fmin(gd, bd)) 76 | var h = 0.0, b = max 77 | 78 | let d = max - min 79 | 80 | let s = max == 0 ? 0 : d / max 81 | 82 | if max == min { 83 | h = 0 // achromatic 84 | } else { 85 | if max == rd { 86 | h = (gd - bd) / d + (gd < bd ? 6 : 0) 87 | } else if max == gd { 88 | h = (bd - rd) / d + 2 89 | } else if max == bd { 90 | h = (rd - gd) / d + 4 91 | } 92 | 93 | h /= 6 94 | } 95 | 96 | return HSB(CGFloat(h), CGFloat(s), CGFloat(b), CGFloat(rgb.alpha)) 97 | } 98 | 99 | // Converts an HSB color value to RGB. 100 | // Assumes h, s, and b are contained in the set [0, 1] and 101 | // returns r, g, and b in the set [0, 255]. 102 | // @param outRGB The rgb color values 103 | // @return The hsb color values 104 | func EFHSB2RGB(hsb: HSB) -> RGB { 105 | var r: CGFloat = 0.0, g: CGFloat = 0.0, b: CGFloat = 0.0 106 | 107 | let i: Int = Int(hsb.hue * 6) 108 | let f = hsb.hue * 6 - CGFloat(i) 109 | let p = hsb.brightness * (1 - hsb.saturation) 110 | let q = hsb.brightness * (1 - f * hsb.saturation) 111 | let t = hsb.brightness * (1 - (1 - f) * hsb.saturation) 112 | 113 | switch i % 6 { 114 | case 0: 115 | r = hsb.brightness 116 | g = t 117 | b = p 118 | break 119 | case 1: 120 | r = q 121 | g = hsb.brightness 122 | b = p 123 | break 124 | case 2: 125 | r = p 126 | g = hsb.brightness 127 | b = t 128 | break 129 | case 3: 130 | r = p 131 | g = q 132 | b = hsb.brightness 133 | break 134 | case 4: 135 | r = t 136 | g = p 137 | b = hsb.brightness 138 | break 139 | case 5: 140 | r = hsb.brightness 141 | g = p 142 | b = q 143 | break 144 | default: 145 | break 146 | } 147 | return RGB(r, g, b, hsb.alpha) 148 | } 149 | 150 | // Returns the rgb values of the color components. 151 | // @param color The color value. 152 | // @return The values of the color components (including alpha). 153 | func EFRGBColorComponents(color: UIColor) -> RGB { 154 | var result = RGB(1, 1, 1, 1) 155 | guard let colorSpaceModel: CGColorSpaceModel = color.cgColor.colorSpace?.model else { 156 | return result 157 | } 158 | 159 | if (CGColorSpaceModel.rgb != colorSpaceModel && CGColorSpaceModel.monochrome != colorSpaceModel) { 160 | return result 161 | } 162 | 163 | guard let components = color.cgColor.components else { 164 | return result 165 | } 166 | 167 | if CGColorSpaceModel.monochrome == colorSpaceModel { 168 | result.red = components[0].clamped(to: colorComponentValueRange) 169 | result.green = components[0].clamped(to: colorComponentValueRange) 170 | result.blue = components[0].clamped(to: colorComponentValueRange) 171 | result.alpha = components[1].clamped(to: colorComponentValueRange) 172 | } else { 173 | result.red = components[0].clamped(to: colorComponentValueRange) 174 | result.green = components[1].clamped(to: colorComponentValueRange) 175 | result.blue = components[2].clamped(to: colorComponentValueRange) 176 | result.alpha = components[3].clamped(to: colorComponentValueRange) 177 | } 178 | 179 | return result 180 | } 181 | 182 | // Converts hex string to the UIColor representation. 183 | // @param color The color value. 184 | // @return The hex string color value. 185 | func EFHexStringFromColor(color: UIColor) -> String? { 186 | guard let colorSpaceModel: CGColorSpaceModel = color.cgColor.colorSpace?.model else { 187 | return nil 188 | } 189 | 190 | if (CGColorSpaceModel.rgb != colorSpaceModel && CGColorSpaceModel.monochrome != colorSpaceModel) { 191 | return nil 192 | } 193 | 194 | guard let components = color.cgColor.components else { 195 | return nil 196 | } 197 | var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0 198 | 199 | if CGColorSpaceModel.monochrome == colorSpaceModel { 200 | red = components[0].clamped(to: colorComponentValueRange) 201 | green = components[0].clamped(to: colorComponentValueRange) 202 | blue = components[0].clamped(to: colorComponentValueRange) 203 | alpha = components[1].clamped(to: colorComponentValueRange) 204 | } else { 205 | red = components[0].clamped(to: colorComponentValueRange) 206 | green = components[1].clamped(to: colorComponentValueRange) 207 | blue = components[2].clamped(to: colorComponentValueRange) 208 | alpha = components[3].clamped(to: colorComponentValueRange) 209 | } 210 | 211 | return String( 212 | format: "#%02lX%02lX%02lX%02lX", 213 | UInt64(red * EFRGBColorComponentMaxValue), 214 | UInt64(green * EFRGBColorComponentMaxValue), 215 | UInt64(blue * EFRGBColorComponentMaxValue), 216 | UInt64(alpha * EFRGBColorComponentMaxValue) 217 | ) 218 | } 219 | 220 | // Converts UIColor value to the hex string. 221 | // @param hexString The hex string color value. 222 | // @return The color value. 223 | func EFColorFromHexString(hexColor: String) -> UIColor? { 224 | if !hexColor.hasPrefix("#") { 225 | return nil 226 | } 227 | 228 | let scanner = Scanner(string: hexColor) 229 | scanner.charactersToBeSkipped = CharacterSet(charactersIn: "#") 230 | 231 | var hexNum: UInt32 = 0 232 | if !scanner.scanHexInt32(&hexNum) { 233 | return nil 234 | } 235 | 236 | let r: CGFloat = CGFloat((hexNum >> 24) & 0xFF) 237 | let g: CGFloat = CGFloat((hexNum >> 16) & 0xFF) 238 | let b: CGFloat = CGFloat((hexNum >> 8) & 0xFF) 239 | let a: CGFloat = CGFloat((hexNum) & 0xFF) 240 | 241 | return UIColor( 242 | red: r / EFRGBColorComponentMaxValue, 243 | green: g / EFRGBColorComponentMaxValue, 244 | blue: b / EFRGBColorComponentMaxValue, 245 | alpha: a / EFRGBColorComponentMaxValue 246 | ) 247 | } 248 | 249 | extension Comparable { 250 | func clamped(to limits: ClosedRange) -> Self { 251 | return min(max(self, limits.lowerBound), limits.upperBound) 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /Example/SQRichTextEditor/EFColorPicker/EFColorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EFColorView.swift 3 | // EFColorPicker 4 | // 5 | // Created by EyreFree on 2017/9/28. 6 | // 7 | // Copyright (c) 2017 EyreFree 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import UIKit 28 | 29 | // The delegate of a EFColorView object must adopt the EFColorViewDelegate protocol. 30 | // Methods of the protocol allow the delegate to handle color value changes. 31 | public protocol EFColorViewDelegate: class { 32 | 33 | // Tells the data source to return the color components. 34 | // @param colorView The color view. 35 | // @param color The new color value. 36 | func colorView(_ colorView: EFColorView, didChangeColor color: UIColor) 37 | } 38 | 39 | /// The \c EFColorView protocol declares a view's interface for displaying and editing color value. 40 | public protocol EFColorView: class { 41 | 42 | // The object that acts as the delegate of the receiving color selection view. 43 | var delegate: EFColorViewDelegate? { get set } 44 | 45 | // The current color. 46 | var color: UIColor { get set } 47 | } 48 | -------------------------------------------------------------------------------- /Example/SQRichTextEditor/EFColorPicker/EFColorWheelView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EFColorWheelView.swift 3 | // EFColorPicker 4 | // 5 | // Created by EyreFree on 2017/9/29. 6 | // 7 | // Copyright (c) 2017 EyreFree 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import UIKit 28 | import CoreGraphics 29 | 30 | // The color wheel view. 31 | public class EFColorWheelView: UIControl { 32 | 33 | var isTouched = false 34 | var wheelImage: CGImage? 35 | 36 | // The hue value. 37 | var hue: CGFloat = 0.0 { 38 | didSet { 39 | self.setSelectedPoint(point: ef_selectedPoint()) 40 | self.setNeedsDisplay() 41 | } 42 | } 43 | 44 | // The saturation value. 45 | var saturation: CGFloat = 0.0 { 46 | didSet { 47 | self.setSelectedPoint(point: ef_selectedPoint()) 48 | self.setNeedsDisplay() 49 | } 50 | } 51 | 52 | // The saturation value. 53 | var brightness: CGFloat = 1.0 { 54 | didSet { 55 | self.setSelectedPoint(point: ef_selectedPoint()) 56 | if oldValue != brightness { 57 | drawWheelImage() 58 | } 59 | } 60 | } 61 | 62 | private lazy var indicatorLayer: CALayer = { 63 | let dimension: CGFloat = 33 64 | let edgeColor = UIColor(white: 0.9, alpha: 0.8) 65 | 66 | let indicatorLayer = CALayer() 67 | indicatorLayer.cornerRadius = dimension / 2 68 | indicatorLayer.borderColor = edgeColor.cgColor 69 | indicatorLayer.borderWidth = 2 70 | indicatorLayer.backgroundColor = UIColor.white.cgColor 71 | indicatorLayer.bounds = CGRect(x: 0, y: 0, width: dimension, height: dimension) 72 | indicatorLayer.position = CGPoint(x: self.bounds.width / 2, y: self.bounds.height / 2) 73 | indicatorLayer.shadowColor = UIColor.black.cgColor 74 | indicatorLayer.shadowOffset = CGSize.zero 75 | indicatorLayer.shadowRadius = 1 76 | indicatorLayer.shadowOpacity = 0.5 77 | return indicatorLayer 78 | }() 79 | 80 | override open class var requiresConstraintBasedLayout: Bool { 81 | get { 82 | return true 83 | } 84 | } 85 | 86 | override init(frame: CGRect) { 87 | super.init(frame: frame) 88 | 89 | self.accessibilityLabel = "color_wheel_view" 90 | 91 | self.layer.delegate = self 92 | self.layer.addSublayer(self.indicatorLayer) 93 | 94 | // [self setSelectedPoint:CGPointMake(dimension / 2, dimension / 2)]; 95 | } 96 | 97 | required public init?(coder aDecoder: NSCoder) { 98 | fatalError("init(coder:) has not been implemented") 99 | } 100 | 101 | override public func touchesBegan(_ touches: Set, with event: UIEvent?) { 102 | self.isTouched = true 103 | if let position: CGPoint = touches.first?.location(in: self) { 104 | self.onTouchEventWithPosition(point: position) 105 | } 106 | } 107 | 108 | override public func touchesMoved(_ touches: Set, with event: UIEvent?) { 109 | if let position: CGPoint = touches.first?.location(in: self) { 110 | self.onTouchEventWithPosition(point: position) 111 | } 112 | } 113 | 114 | override public func touchesEnded(_ touches: Set, with event: UIEvent?) { 115 | self.isTouched = false 116 | if let position: CGPoint = touches.first?.location(in: self) { 117 | self.onTouchEventWithPosition(point: position) 118 | } 119 | } 120 | 121 | func onTouchEventWithPosition(point: CGPoint) { 122 | let radius: CGFloat = self.bounds.width / 2 123 | 124 | let mx = Double(radius - point.x) 125 | let my = Double(radius - point.y) 126 | let dist: CGFloat = CGFloat(sqrt(mx * mx + my * my)) 127 | 128 | if dist <= radius { 129 | self.ef_colorWheelValueWithPosition(position: point, hue: &hue, saturation: &saturation) 130 | self.setSelectedPoint(point: point) 131 | self.sendActions(for: UIControl.Event.valueChanged) 132 | } 133 | } 134 | 135 | func setSelectedPoint(point: CGPoint) { 136 | let selectedColor: UIColor = UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1) 137 | 138 | CATransaction.begin() 139 | CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions) 140 | self.indicatorLayer.position = point 141 | self.indicatorLayer.backgroundColor = selectedColor.cgColor 142 | CATransaction.commit() 143 | } 144 | 145 | // MARK:- CALayerDelegate methods 146 | override public func display(_ layer: CALayer) { 147 | guard wheelImage == nil else { return } 148 | drawWheelImage() 149 | } 150 | 151 | override public func layoutSublayers(of layer: CALayer) { 152 | if layer == self.layer { 153 | self.setSelectedPoint(point: self.ef_selectedPoint()) 154 | self.layer.setNeedsDisplay() 155 | } 156 | } 157 | 158 | // MARK:- Private methods 159 | private func drawWheelImage() { 160 | let dimension: CGFloat = min(self.frame.width, self.frame.height) 161 | guard let bitmapData = CFDataCreateMutable(nil, 0) else { 162 | return 163 | } 164 | 165 | CFDataSetLength(bitmapData, CFIndex(dimension * dimension * 4)) 166 | self.ef_colorWheelBitmap( 167 | bitmap: CFDataGetMutableBytePtr(bitmapData), 168 | withSize: CGSize(width: dimension, height: dimension) 169 | ) 170 | if let image = self.ef_imageWithRGBAData(data: bitmapData, width: Int(dimension), height: Int(dimension)) { 171 | wheelImage = image 172 | self.layer.contents = wheelImage 173 | } 174 | } 175 | 176 | private func ef_selectedPoint() -> CGPoint { 177 | let dimension: CGFloat = min(self.frame.width, self.frame.height) 178 | 179 | let radius: CGFloat = saturation * dimension / 2 180 | let x: CGFloat = dimension / 2 + radius * CGFloat(cos(Double(hue) * Double.pi * 2.0)) 181 | let y: CGFloat = dimension / 2 + radius * CGFloat(sin(Double(hue) * Double.pi * 2.0)) 182 | 183 | return CGPoint(x: x, y: y) 184 | } 185 | 186 | private func ef_colorWheelBitmap(bitmap: UnsafeMutablePointer?, withSize size: CGSize) { 187 | if size.width <= 0 || size.height <= 0 { 188 | return 189 | } 190 | 191 | for y in 0 ..< Int(size.width) { 192 | for x in 0 ..< Int(size.height) { 193 | var hue: CGFloat = 0, saturation: CGFloat = 0, a: CGFloat = 0.0 194 | self.ef_colorWheelValueWithPosition(position: CGPoint(x: x, y: y), hue: &hue, saturation: &saturation) 195 | 196 | var rgb: RGB = RGB(1, 1, 1, 1) 197 | if saturation < 1.0 { 198 | // Antialias the edge of the circle. 199 | if saturation > 0.99 { 200 | a = (1.0 - saturation) * 100 201 | } else { 202 | a = 1.0 203 | } 204 | 205 | let hsb: HSB = HSB(hue, saturation, brightness, a) 206 | rgb = EFHSB2RGB(hsb: hsb) 207 | } 208 | 209 | let i: Int = 4 * (x + y * Int(size.width)) 210 | bitmap?[i] = UInt8(rgb.red * 0xff) 211 | bitmap?[i + 1] = UInt8(rgb.green * 0xff) 212 | bitmap?[i + 2] = UInt8(rgb.blue * 0xff) 213 | bitmap?[i + 3] = UInt8(rgb.alpha * 0xff) 214 | } 215 | } 216 | } 217 | 218 | private func ef_colorWheelValueWithPosition(position: CGPoint, hue: inout CGFloat, saturation: inout CGFloat) { 219 | let c: Int = Int(self.bounds.width / 2) 220 | let dx: CGFloat = (position.x - CGFloat(c)) / CGFloat(c) 221 | let dy: CGFloat = (position.y - CGFloat(c)) / CGFloat(c) 222 | let d: CGFloat = CGFloat(sqrt(Double(dx * dx + dy * dy))) 223 | 224 | saturation = d 225 | 226 | if d == 0 { 227 | hue = 0 228 | } else { 229 | hue = acos(dx / d) / CGFloat.pi / 2.0 230 | 231 | if dy < 0 { 232 | hue = 1.0 - hue 233 | } 234 | } 235 | } 236 | 237 | private func ef_imageWithRGBAData(data: CFData, width: Int, height: Int) -> CGImage? { 238 | guard let dataProvider = CGDataProvider(data: data) else { 239 | return nil 240 | } 241 | let colorSpace = CGColorSpaceCreateDeviceRGB() 242 | 243 | let imageRef = CGImage( 244 | width: width, 245 | height: height, 246 | bitsPerComponent: 8, 247 | bitsPerPixel: 32, 248 | bytesPerRow: width * 4, 249 | space: colorSpace, 250 | bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.last.rawValue), 251 | provider: dataProvider, 252 | decode: nil, 253 | shouldInterpolate: false, 254 | intent: CGColorRenderingIntent.defaultIntent 255 | ) 256 | return imageRef 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /Example/SQRichTextEditor/EFColorPicker/EFControl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EFControl.swift 3 | // EFColorPicker 4 | // 5 | // Created by EyreFree on 2017/9/28. 6 | // 7 | // Copyright (c) 2017 EyreFree 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import UIKit 28 | 29 | public class EFControl: UIControl { 30 | 31 | // Edge inset values are applied to a view bounds to shrink or expand the touchable area. 32 | var hitTestEdgeInsets: UIEdgeInsets = UIEdgeInsets.zero 33 | 34 | override public func point(inside point: CGPoint, with event: UIEvent?) -> Bool { 35 | if self.hitTestEdgeInsets == UIEdgeInsets.zero 36 | || !self.isEnabled 37 | || self.isHidden 38 | || !self.isUserInteractionEnabled 39 | || 0 == self.alpha { 40 | return super.point(inside: point, with: event) 41 | } 42 | 43 | let hitFrame: CGRect = self.bounds.inset(by: self.hitTestEdgeInsets) 44 | return hitFrame.contains(point) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Example/SQRichTextEditor/EFColorPicker/EFHSBView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EFHSBView.swift 3 | // EFColorPicker 4 | // 5 | // Created by EyreFree on 2017/9/29. 6 | // 7 | // Copyright (c) 2017 EyreFree 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import UIKit 28 | 29 | // The view to edit HSB color components. 30 | public class EFHSBView: UIView, EFColorView, UITextFieldDelegate { 31 | 32 | let EFColorSampleViewHeight: CGFloat = 30.0 33 | let EFViewMargin: CGFloat = 20.0 34 | let EFColorWheelDimension: CGFloat = 200.0 35 | 36 | private let colorWheel: EFColorWheelView = EFColorWheelView() 37 | let brightnessView: EFColorComponentView = EFColorComponentView() 38 | private let colorSample: UIView = UIView() 39 | 40 | private var colorComponents: HSB = HSB(1, 1, 1, 1) 41 | private var layoutConstraints: [NSLayoutConstraint] = [] 42 | 43 | weak public var delegate: EFColorViewDelegate? 44 | 45 | public var isTouched: Bool { 46 | if self.colorWheel.isTouched { 47 | return true 48 | } 49 | 50 | if self.brightnessView.isTouched { 51 | return true 52 | } 53 | 54 | return false 55 | } 56 | 57 | public var color: UIColor { 58 | get { 59 | return UIColor( 60 | hue: colorComponents.hue, 61 | saturation: colorComponents.saturation, 62 | brightness: colorComponents.brightness, 63 | alpha: colorComponents.alpha 64 | ) 65 | } 66 | set { 67 | colorComponents = EFRGB2HSB(rgb: EFRGBColorComponents(color: newValue)) 68 | self.reloadData() 69 | } 70 | } 71 | 72 | override init(frame: CGRect) { 73 | super.init(frame: frame) 74 | self.ef_baseInit() 75 | } 76 | 77 | required public init?(coder aDecoder: NSCoder) { 78 | super.init(coder: aDecoder) 79 | self.ef_baseInit() 80 | } 81 | 82 | func reloadData() { 83 | colorSample.backgroundColor = self.color 84 | colorSample.accessibilityValue = EFHexStringFromColor(color: self.color) 85 | self.ef_reloadViewsWithColorComponents(colorComponents: colorComponents) 86 | self.colorWheel.display(self.colorWheel.layer) 87 | } 88 | 89 | override public func updateConstraints() { 90 | self.ef_updateConstraints() 91 | super.updateConstraints() 92 | } 93 | 94 | // MARK:- Private methods 95 | private func ef_baseInit() { 96 | self.accessibilityLabel = "hsb_view" 97 | 98 | colorSample.accessibilityLabel = "color_sample" 99 | colorSample.layer.borderColor = UIColor.black.cgColor 100 | colorSample.layer.borderWidth = 0.5 101 | colorSample.translatesAutoresizingMaskIntoConstraints = false 102 | self.addSubview(colorSample) 103 | 104 | colorWheel.translatesAutoresizingMaskIntoConstraints = false 105 | self.addSubview(colorWheel) 106 | 107 | brightnessView.title = NSLocalizedString("Brightness", comment: "") 108 | brightnessView.maximumValue = EFHSBColorComponentMaxValue 109 | brightnessView.format = "%.2f" 110 | brightnessView.translatesAutoresizingMaskIntoConstraints = false 111 | brightnessView.setColors(colors: [UIColor.black, UIColor.white]) 112 | self.addSubview(brightnessView) 113 | 114 | colorWheel.addTarget( 115 | self, action: #selector(ef_colorDidChangeValue(sender:)), for: UIControl.Event.valueChanged 116 | ) 117 | brightnessView.addTarget( 118 | self, action: #selector(ef_brightnessDidChangeValue(sender:)), for: UIControl.Event.valueChanged 119 | ) 120 | 121 | self.setNeedsUpdateConstraints() 122 | } 123 | 124 | private func ef_updateConstraints() { 125 | // remove all constraints first 126 | if !layoutConstraints.isEmpty { 127 | self.removeConstraints(layoutConstraints) 128 | } 129 | 130 | layoutConstraints = UIUserInterfaceSizeClass.compact == self.traitCollection.verticalSizeClass 131 | ? self.ef_constraintsForCompactVerticalSizeClass() 132 | : self.ef_constraintsForRegularVerticalSizeClass() 133 | 134 | self.addConstraints(layoutConstraints) 135 | } 136 | 137 | private func ef_constraintsForRegularVerticalSizeClass() -> [NSLayoutConstraint] { 138 | let metrics = [ 139 | "margin" : EFViewMargin, 140 | "height" : EFColorSampleViewHeight, 141 | "color_wheel_dimension" : EFColorWheelDimension 142 | ] 143 | let views = [ 144 | "colorSample" : colorSample, 145 | "colorWheel" : colorWheel, 146 | "brightnessView" : brightnessView 147 | ] 148 | 149 | var layoutConstraints: [NSLayoutConstraint] = [] 150 | let visualFormats = [ 151 | "H:|-margin-[colorSample]-margin-|", 152 | "H:|-margin-[colorWheel(>=color_wheel_dimension)]-margin-|", 153 | "H:|-margin-[brightnessView]-margin-|", 154 | "V:|-margin-[colorSample(height)]-margin-[colorWheel]-margin-[brightnessView]-(>=margin@250)-|" 155 | ] 156 | for visualFormat in visualFormats { 157 | layoutConstraints.append( 158 | contentsOf: NSLayoutConstraint.constraints( 159 | withVisualFormat: visualFormat, 160 | options: NSLayoutConstraint.FormatOptions(rawValue: 0), 161 | metrics: metrics, 162 | views: views 163 | ) 164 | ) 165 | } 166 | layoutConstraints.append( 167 | NSLayoutConstraint( 168 | item: colorWheel, 169 | attribute: NSLayoutConstraint.Attribute.width, 170 | relatedBy: NSLayoutConstraint.Relation.equal, 171 | toItem: colorWheel, 172 | attribute: NSLayoutConstraint.Attribute.height, 173 | multiplier: 1, 174 | constant: 0) 175 | ) 176 | return layoutConstraints 177 | } 178 | 179 | private func ef_constraintsForCompactVerticalSizeClass() -> [NSLayoutConstraint] { 180 | let metrics = [ 181 | "margin" : EFViewMargin, 182 | "height" : EFColorSampleViewHeight, 183 | "color_wheel_dimension" : EFColorWheelDimension 184 | ] 185 | let views = [ 186 | "colorSample" : colorSample, 187 | "colorWheel" : colorWheel, 188 | "brightnessView" : brightnessView 189 | ] 190 | 191 | var layoutConstraints: [NSLayoutConstraint] = [] 192 | let visualFormats = [ 193 | "H:|-margin-[colorSample]-margin-|", 194 | "H:|-margin-[colorWheel(>=color_wheel_dimension)]-margin-[brightnessView]-(margin@500)-|", 195 | "V:|-margin-[colorSample(height)]-margin-[colorWheel]-(margin@500)-|" 196 | ] 197 | for visualFormat in visualFormats { 198 | layoutConstraints.append( 199 | contentsOf: NSLayoutConstraint.constraints( 200 | withVisualFormat: visualFormat, 201 | options: NSLayoutConstraint.FormatOptions(rawValue: 0), 202 | metrics: metrics, 203 | views: views 204 | ) 205 | ) 206 | } 207 | layoutConstraints.append( 208 | NSLayoutConstraint( 209 | item: colorWheel, 210 | attribute: NSLayoutConstraint.Attribute.width, 211 | relatedBy: NSLayoutConstraint.Relation.equal, 212 | toItem: colorWheel, 213 | attribute: NSLayoutConstraint.Attribute.height, 214 | multiplier: 1, 215 | constant: 0) 216 | ) 217 | layoutConstraints.append( 218 | NSLayoutConstraint( 219 | item: brightnessView, 220 | attribute: NSLayoutConstraint.Attribute.centerY, 221 | relatedBy: NSLayoutConstraint.Relation.equal, 222 | toItem: self, 223 | attribute: NSLayoutConstraint.Attribute.centerY, 224 | multiplier: 1, 225 | constant: 0) 226 | ) 227 | return layoutConstraints 228 | } 229 | 230 | private func ef_reloadViewsWithColorComponents(colorComponents: HSB) { 231 | colorWheel.hue = colorComponents.hue 232 | colorWheel.saturation = colorComponents.saturation 233 | colorWheel.brightness = colorComponents.brightness 234 | self.ef_updateSlidersWithColorComponents(colorComponents: colorComponents) 235 | } 236 | 237 | private func ef_updateSlidersWithColorComponents(colorComponents: HSB) { 238 | brightnessView.value = colorComponents.brightness 239 | } 240 | 241 | @objc private func ef_colorDidChangeValue(sender: EFColorWheelView) { 242 | colorComponents.hue = sender.hue 243 | colorComponents.saturation = sender.saturation 244 | self.delegate?.colorView(self, didChangeColor: self.color) 245 | self.reloadData() 246 | } 247 | 248 | @objc private func ef_brightnessDidChangeValue(sender: EFColorComponentView) { 249 | colorComponents.brightness = sender.value 250 | self.colorWheel.brightness = sender.value 251 | self.delegate?.colorView(self, didChangeColor: self.color) 252 | self.reloadData() 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /Example/SQRichTextEditor/EFColorPicker/EFRGBView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EFRGBView.swift 3 | // EFColorPicker 4 | // 5 | // Created by EyreFree on 2017/9/29. 6 | // 7 | // Copyright (c) 2017 EyreFree 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import UIKit 28 | 29 | public class EFRGBView: UIView, EFColorView { 30 | 31 | let EFColorSampleViewHeight: CGFloat = 30.0 32 | let EFViewMargin: CGFloat = 20.0 33 | let EFSliderViewMargin: CGFloat = 30.0 34 | let EFRGBColorComponentsSize: Int = 3 35 | 36 | private let colorSample: UIView = UIView() 37 | var colorComponentViews: [EFColorComponentView] = [] 38 | private var colorComponents: RGB = RGB(1, 1, 1, 1) 39 | 40 | weak public var delegate: EFColorViewDelegate? 41 | 42 | public var isTouched: Bool { 43 | return self.colorComponentViews.filter { $0.isTouched }.count > 0 44 | } 45 | 46 | public var color: UIColor { 47 | get { 48 | return UIColor( 49 | red: colorComponents.red, 50 | green: colorComponents.green, 51 | blue: colorComponents.blue, 52 | alpha: colorComponents.alpha 53 | ) 54 | } 55 | set { 56 | colorComponents = EFRGBColorComponents(color: newValue) 57 | self.reloadData() 58 | } 59 | } 60 | 61 | override init(frame: CGRect) { 62 | super.init(frame: frame) 63 | self.ef_baseInit() 64 | } 65 | 66 | required public init?(coder aDecoder: NSCoder) { 67 | super.init(coder: aDecoder) 68 | self.ef_baseInit() 69 | } 70 | 71 | func reloadData() { 72 | colorSample.backgroundColor = self.color 73 | colorSample.accessibilityValue = EFHexStringFromColor(color: self.color) 74 | self.ef_reloadColorComponentViews(colorComponents: colorComponents) 75 | } 76 | 77 | // MARK:- Private methods 78 | private func ef_baseInit() { 79 | self.accessibilityLabel = "rgb_view" 80 | 81 | colorSample.accessibilityLabel = "color_sample" 82 | colorSample.layer.borderColor = UIColor.black.cgColor 83 | colorSample.layer.borderWidth = 0.5 84 | colorSample.translatesAutoresizingMaskIntoConstraints = false 85 | self.addSubview(colorSample) 86 | 87 | var tmp: [EFColorComponentView] = [] 88 | let titles = [ 89 | NSLocalizedString("Red", comment: ""), 90 | NSLocalizedString("Green", comment: ""), 91 | NSLocalizedString("Blue", comment: "") 92 | ] 93 | let maxValues: [CGFloat] = [ 94 | EFRGBColorComponentMaxValue, EFRGBColorComponentMaxValue, EFRGBColorComponentMaxValue 95 | ] 96 | for i in 0 ..< EFRGBColorComponentsSize { 97 | let colorComponentView = self.ef_colorComponentViewWithTitle( 98 | title: titles[i], tag: i, maxValue: maxValues[i] 99 | ) 100 | self.addSubview(colorComponentView) 101 | colorComponentView.addTarget( 102 | self, action: #selector(ef_colorComponentDidChangeValue(_:)), for: UIControl.Event.valueChanged 103 | ) 104 | tmp.append(colorComponentView) 105 | } 106 | 107 | colorComponentViews = tmp 108 | self.ef_installConstraints() 109 | } 110 | 111 | @objc @IBAction private func ef_colorComponentDidChangeValue(_ sender: EFColorComponentView) { 112 | self.ef_setColorComponentValue(value: sender.value / sender.maximumValue, atIndex: UInt(sender.tag)) 113 | self.delegate?.colorView(self, didChangeColor: self.color) 114 | self.reloadData() 115 | } 116 | 117 | private func ef_setColorComponentValue(value: CGFloat, atIndex index: UInt) { 118 | switch index { 119 | case 0: 120 | colorComponents.red = value 121 | break 122 | case 1: 123 | colorComponents.green = value 124 | break 125 | case 2: 126 | colorComponents.blue = value 127 | break 128 | default: 129 | colorComponents.alpha = value 130 | break 131 | } 132 | } 133 | 134 | private func ef_colorComponentViewWithTitle(title: String, tag: Int, maxValue: CGFloat) -> EFColorComponentView { 135 | let colorComponentView: EFColorComponentView = EFColorComponentView() 136 | colorComponentView.title = title 137 | colorComponentView.translatesAutoresizingMaskIntoConstraints = false 138 | colorComponentView.tag = tag 139 | colorComponentView.maximumValue = maxValue 140 | return colorComponentView 141 | } 142 | 143 | private func ef_installConstraints() { 144 | let metrics = [ 145 | "margin" : EFViewMargin, 146 | "height" : EFColorSampleViewHeight, 147 | "slider_margin" : EFSliderViewMargin 148 | ] 149 | var views = [ 150 | "colorSample" : colorSample 151 | ] 152 | 153 | let visualFormats = [ 154 | "H:|-margin-[colorSample]-margin-|", 155 | "V:|-margin-[colorSample(height)]" 156 | ] 157 | for visualFormat in visualFormats { 158 | self.addConstraints( 159 | NSLayoutConstraint.constraints( 160 | withVisualFormat: visualFormat, 161 | options: NSLayoutConstraint.FormatOptions(rawValue: 0), 162 | metrics: metrics, 163 | views: views 164 | ) 165 | ) 166 | } 167 | 168 | var previousView: UIView = colorSample 169 | for colorComponentView in colorComponentViews { 170 | views = [ 171 | "previousView" : previousView, 172 | "colorComponentView" : colorComponentView 173 | ] 174 | 175 | let visualFormats = [ 176 | "H:|-margin-[colorComponentView]-margin-|", 177 | "V:[previousView]-slider_margin-[colorComponentView]" 178 | ] 179 | for visualFormat in visualFormats { 180 | self.addConstraints( 181 | NSLayoutConstraint.constraints( 182 | withVisualFormat: visualFormat, 183 | options: NSLayoutConstraint.FormatOptions(rawValue: 0), 184 | metrics: metrics, 185 | views: views 186 | ) 187 | ) 188 | } 189 | 190 | previousView = colorComponentView 191 | } 192 | 193 | views = [ 194 | "previousView" : previousView 195 | ] 196 | self.addConstraints( 197 | NSLayoutConstraint.constraints( 198 | withVisualFormat: "V:[previousView]-(>=margin)-|", 199 | options: NSLayoutConstraint.FormatOptions(rawValue: 0), 200 | metrics: metrics, 201 | views: views 202 | ) 203 | ) 204 | } 205 | 206 | private func ef_colorComponentsWithRGB(rgb: RGB) -> [CGFloat] { 207 | return [rgb.red, rgb.green, rgb.blue, rgb.alpha] 208 | } 209 | 210 | private func ef_reloadColorComponentViews(colorComponents: RGB) { 211 | let components = self.ef_colorComponentsWithRGB(rgb: colorComponents) 212 | 213 | for (idx, colorComponentView) in colorComponentViews.enumerated() { 214 | let cgColors: [CGColor] = self.ef_colorsWithColorComponents(colorComponents: components, 215 | currentColorIndex: colorComponentView.tag) 216 | let colors: [UIColor] = cgColors.map({ cgColor -> UIColor in 217 | return UIColor(cgColor: cgColor) 218 | }) 219 | 220 | colorComponentView.setColors(colors: colors) 221 | colorComponentView.value = components[idx] * colorComponentView.maximumValue 222 | } 223 | } 224 | 225 | private func ef_colorsWithColorComponents(colorComponents: [CGFloat], currentColorIndex colorIndex: Int) -> [CGColor] { 226 | let currentColorValue: CGFloat = colorComponents[colorIndex] 227 | var colors: [CGFloat] = [CGFloat](repeating: 0, count: 12) 228 | for i in 0 ..< EFRGBColorComponentsSize { 229 | colors[i] = colorComponents[i] 230 | colors[i + 4] = colorComponents[i] 231 | colors[i + 8] = colorComponents[i] 232 | } 233 | colors[colorIndex] = 0 234 | colors[colorIndex + 4] = currentColorValue 235 | colors[colorIndex + 8] = 1.0 236 | 237 | let start: UIColor = UIColor(red: colors[0], green: colors[1], blue: colors[2], alpha: 1) 238 | let middle: UIColor = UIColor(red: colors[4], green: colors[5], blue: colors[6], alpha: 1) 239 | let end: UIColor = UIColor(red: colors[8], green: colors[9], blue: colors[10], alpha: 1) 240 | 241 | return [start.cgColor, middle.cgColor, end.cgColor] 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /Example/SQRichTextEditor/EFColorPicker/EFSliderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EFSliderView.swift 3 | // EFColorPicker 4 | // 5 | // Created by EyreFree on 2017/9/28. 6 | // 7 | // Copyright (c) 2017 EyreFree 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Foundation 28 | import CoreGraphics 29 | import QuartzCore 30 | import UIKit 31 | 32 | public class EFSliderView: EFControl { 33 | 34 | let EFSliderViewHeight: CGFloat = 28.0 35 | let EFSliderViewMinWidth: CGFloat = 150.0 36 | let EFSliderViewTrackHeight: CGFloat = 6.0 37 | let EFThumbViewEdgeInset: CGFloat = -10.0 38 | 39 | private let thumbView: EFThumbView = EFThumbView() 40 | private let trackLayer: CAGradientLayer = CAGradientLayer() 41 | 42 | // The slider's current value. The default value is 0.0. 43 | private(set) var value: CGFloat = 0 44 | 45 | // The minimum value of the slider. The default value is 0.0. 46 | var minimumValue: CGFloat = 0 47 | 48 | // The maximum value of the slider. The default value is 1.0. 49 | var maximumValue: CGFloat = 1 50 | 51 | // Indicates if the user touches the control at the moment 52 | var isTouched = false 53 | 54 | override init(frame: CGRect) { 55 | super.init(frame: frame) 56 | 57 | self.accessibilityLabel = "color_slider" 58 | 59 | minimumValue = 0.0 60 | maximumValue = 1.0 61 | value = 0.0 62 | 63 | self.layer.delegate = self 64 | 65 | trackLayer.cornerRadius = EFSliderViewTrackHeight / 2.0 66 | trackLayer.startPoint = CGPoint(x: 0.0, y: 0.5) 67 | trackLayer.endPoint = CGPoint(x: 1.0, y: 0.5) 68 | self.layer.addSublayer(trackLayer) 69 | 70 | thumbView.hitTestEdgeInsets = UIEdgeInsets( 71 | top: EFThumbViewEdgeInset, left: EFThumbViewEdgeInset, 72 | bottom: EFThumbViewEdgeInset, right: EFThumbViewEdgeInset 73 | ) 74 | thumbView.gestureRecognizer.addTarget(self, action: #selector(ef_didPanThumbView(gestureRecognizer:))) 75 | self.addSubview(thumbView) 76 | 77 | let color = UIColor.blue 78 | self.setColors(colors: [color, color]) 79 | } 80 | 81 | required public init?(coder aDecoder: NSCoder) { 82 | fatalError("init(coder:) has not been implemented") 83 | } 84 | 85 | override open class var requiresConstraintBasedLayout: Bool { 86 | get { 87 | return true 88 | } 89 | } 90 | 91 | override public var intrinsicContentSize: CGSize { 92 | get { 93 | return CGSize(width: EFSliderViewMinWidth, height: EFSliderViewHeight) 94 | } 95 | } 96 | 97 | func setValue(value: CGFloat) { 98 | if (value < minimumValue) { 99 | self.value = minimumValue 100 | } else if (value > maximumValue) { 101 | self.value = maximumValue 102 | } else { 103 | self.value = value 104 | } 105 | 106 | self.ef_updateThumbPositionWithValue(value: self.value) 107 | } 108 | 109 | // Sets the array of CGColorRef objects defining the color of each gradient stop on the track. 110 | // The location of each gradient stop is evaluated with formula: i * width_of_the_track / number_of_colors. 111 | // @param colors An array of CGColorRef objects. 112 | func setColors(colors: [UIColor]) { 113 | let cgColors = colors.map { color -> CGColor in 114 | return color.cgColor 115 | } 116 | if cgColors.count <= 1 { 117 | fatalError("‘colors: [CGColor]’ at least need to have 2 elements") 118 | } 119 | trackLayer.colors = cgColors 120 | self.ef_updateLocations() 121 | } 122 | 123 | override public func layoutSubviews() { 124 | self.ef_updateThumbPositionWithValue(value: self.value) 125 | self.ef_updateTrackLayer() 126 | } 127 | 128 | // MARK:- UIControl touch tracking events 129 | @objc func ef_didPanThumbView(gestureRecognizer: UIPanGestureRecognizer) { 130 | if gestureRecognizer.state == UIGestureRecognizer.State.ended { 131 | self.isTouched = false 132 | } else if gestureRecognizer.state == UIGestureRecognizer.State.began { 133 | self.isTouched = true 134 | } 135 | 136 | if gestureRecognizer.state != UIGestureRecognizer.State.began 137 | && gestureRecognizer.state != UIGestureRecognizer.State.changed { 138 | return 139 | } 140 | 141 | let translation = gestureRecognizer.translation(in: self) 142 | gestureRecognizer.setTranslation(CGPoint.zero, in: self) 143 | 144 | self.ef_setValueWithTranslation(translation: translation.x) 145 | } 146 | 147 | func ef_updateTrackLayer() { 148 | let height: CGFloat = EFSliderViewHeight 149 | let width: CGFloat = self.bounds.width 150 | 151 | CATransaction.begin() 152 | CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions) 153 | trackLayer.bounds = CGRect(x: 0, y: 0, width: width, height: EFSliderViewTrackHeight) 154 | trackLayer.position = CGPoint(x: self.bounds.width / 2, y: height / 2) 155 | CATransaction.commit() 156 | } 157 | 158 | // MARK:- Private methods 159 | private func ef_setValueWithTranslation(translation: CGFloat) { 160 | let width: CGFloat = self.bounds.width - thumbView.bounds.width 161 | let valueRange: CGFloat = maximumValue - minimumValue 162 | let value: CGFloat = self.value + valueRange * translation / width 163 | 164 | self.setValue(value: value) 165 | self.sendActions(for: UIControl.Event.valueChanged) 166 | } 167 | 168 | private func ef_updateLocations() { 169 | let size: Int = trackLayer.colors?.count ?? 2 170 | if size == trackLayer.locations?.count { 171 | return 172 | } 173 | 174 | let step: CGFloat = 1.0 / (CGFloat(size) - 1) 175 | var locations: [NSNumber] = [0] 176 | 177 | var i: Int = 1 178 | while i < size - 1 { 179 | locations.append(NSNumber(value: Double(CGFloat(i) * step))) 180 | i += 1 181 | } 182 | 183 | locations.append(1.0) 184 | trackLayer.locations = locations 185 | } 186 | 187 | private func ef_updateThumbPositionWithValue(value: CGFloat) { 188 | let thumbWidth: CGFloat = thumbView.bounds.width 189 | let thumbHeight: CGFloat = thumbView.bounds.height 190 | let width: CGFloat = self.bounds.width - thumbWidth 191 | 192 | if width == 0 { 193 | return 194 | } 195 | 196 | let percentage: CGFloat = (value - minimumValue) / (maximumValue - minimumValue) 197 | let position: CGFloat = width * percentage 198 | thumbView.frame = CGRect(x: position, y: 0, width: thumbWidth, height: thumbHeight) 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /Example/SQRichTextEditor/EFColorPicker/EFThumbView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EFThumbView.swift 3 | // EFColorPicker 4 | // 5 | // Created by EyreFree on 2017/9/28. 6 | // 7 | // Copyright (c) 2017 EyreFree 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import UIKit 28 | 29 | public class EFThumbView: EFControl { 30 | 31 | private(set) var gestureRecognizer: UIGestureRecognizer = UIPanGestureRecognizer(target: nil, action: nil) 32 | 33 | private let EFSliderViewThumbDimension: CGFloat = 28.0 34 | private let thumbLayer: CALayer = CALayer() 35 | 36 | override init(frame: CGRect) { 37 | super.init( 38 | frame: CGRect( 39 | x: frame.origin.x, y: frame.origin.y, 40 | width: EFSliderViewThumbDimension, height: EFSliderViewThumbDimension 41 | ) 42 | ) 43 | 44 | self.thumbLayer.borderColor = UIColor.lightGray.withAlphaComponent(0.4).cgColor 45 | self.thumbLayer.borderWidth = 0.5 46 | self.thumbLayer.cornerRadius = EFSliderViewThumbDimension / 2 47 | self.thumbLayer.backgroundColor = UIColor.white.cgColor 48 | self.thumbLayer.shadowColor = UIColor.black.cgColor 49 | self.thumbLayer.shadowOffset = CGSize(width: 0, height: 3) 50 | self.thumbLayer.shadowRadius = 2 51 | self.thumbLayer.shadowOpacity = 0.3 52 | self.layer.addSublayer(self.thumbLayer) 53 | 54 | self.addGestureRecognizer(self.gestureRecognizer) 55 | } 56 | 57 | required public init?(coder aDecoder: NSCoder) { 58 | fatalError("init(coder:) has not been implemented") 59 | } 60 | 61 | override public func layoutSublayers(of layer: CALayer) { 62 | if layer != self.layer { 63 | return 64 | } 65 | 66 | CATransaction.begin() 67 | CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions) 68 | self.thumbLayer.bounds = CGRect( 69 | x: 0, y: 0, width: EFSliderViewThumbDimension, height: EFSliderViewThumbDimension 70 | ) 71 | self.thumbLayer.position = CGPoint(x: EFSliderViewThumbDimension / 2, y: EFSliderViewThumbDimension / 2) 72 | CATransaction.commit() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Example/SQRichTextEditor/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "size" : "1024x1024", 46 | "scale" : "1x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Example/SQRichTextEditor/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Example/SQRichTextEditor/ToolItemCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToolBarItemCell.swift 3 | // SQRichTextEditor_Example 4 | // 5 | // Created by Jesse on 2019/12/13. 6 | // Copyright © 2019 CocoaPods. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SQRichTextEditor 11 | 12 | class ToolItemCell: UICollectionViewCell { 13 | 14 | lazy var textLabel: UILabel = { 15 | let _textLabel = UILabel() 16 | _textLabel.translatesAutoresizingMaskIntoConstraints = false 17 | return _textLabel 18 | }() 19 | 20 | override init(frame: CGRect) { 21 | super.init(frame: frame) 22 | 23 | contentView.addSubview(textLabel) 24 | textLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true 25 | textLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true 26 | } 27 | 28 | required init?(coder: NSCoder) { 29 | fatalError("init(coder:) has not been implemented") 30 | } 31 | 32 | func configCell(option: ToolOptionType, attribute: SQTextAttribute) { 33 | //Format 34 | var isActive = false 35 | switch option { 36 | case .bold: 37 | isActive = attribute.format.hasBold 38 | case .italic: 39 | isActive = attribute.format.hasItalic 40 | case .strikethrough: 41 | isActive = attribute.format.hasStrikethrough 42 | case .underline: 43 | isActive = attribute.format.hasUnderline 44 | default: 45 | break 46 | } 47 | textLabel.text = option.description 48 | textLabel.font = isActive ? ToolItemCellSettings.activeFont : ToolItemCellSettings.normalfont 49 | textLabel.textColor = isActive ? ToolItemCellSettings.activeColor : ToolItemCellSettings.normalColor 50 | 51 | //TextInfo 52 | switch option { 53 | case .setTextSize: 54 | if let size = attribute.textInfo.size { 55 | textLabel.text = "Font Size(\(size)px)" 56 | } 57 | 58 | case .setTextColor: 59 | if let color = attribute.textInfo.color { 60 | textLabel.textColor = color 61 | } 62 | 63 | case .setTextBackgroundColor: 64 | if let color = attribute.textInfo.backgroundColor { 65 | textLabel.textColor = color 66 | } 67 | 68 | default: 69 | break 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Example/SQRichTextEditor/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // SQRichTextEditor 4 | // 5 | // Created by conscientiousness on 12/09/2019. 6 | // Copyright (c) 2019 conscientiousness. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SQRichTextEditor 11 | import WebKit 12 | 13 | class ViewController: UIViewController { 14 | 15 | private lazy var flowLayout: UICollectionViewFlowLayout = { 16 | let _flowLayout = UICollectionViewFlowLayout() 17 | _flowLayout.scrollDirection = .horizontal 18 | return _flowLayout 19 | }() 20 | 21 | private lazy var collectionView: UICollectionView = { 22 | let _collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) 23 | _collectionView.delegate = self 24 | _collectionView.dataSource = self 25 | _collectionView.translatesAutoresizingMaskIntoConstraints = false 26 | return _collectionView 27 | }() 28 | 29 | private lazy var editorView: SQTextEditorView = { 30 | let _editorView = SQTextEditorView() 31 | _editorView.delegate = self 32 | _editorView.translatesAutoresizingMaskIntoConstraints = false 33 | return _editorView 34 | }() 35 | 36 | private var selectedOption: ToolOptionType? 37 | 38 | override func didReceiveMemoryWarning() { 39 | super.didReceiveMemoryWarning() 40 | print("didReceiveMemoryWarning") 41 | } 42 | 43 | override func viewDidLoad() { 44 | super.viewDidLoad() 45 | 46 | setupUI() 47 | setupCollectioView() 48 | } 49 | 50 | override func viewDidLayoutSubviews() { 51 | super.viewDidLayoutSubviews() 52 | collectionView.layoutIfNeeded() 53 | } 54 | 55 | private func setupUI() { 56 | view.addSubview(collectionView) 57 | view.addSubview(editorView) 58 | 59 | collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true 60 | collectionView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true 61 | collectionView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true 62 | collectionView.heightAnchor.constraint(equalToConstant: ToolItemCellSettings.height).isActive = true 63 | 64 | editorView.topAnchor.constraint(equalTo: collectionView.bottomAnchor, constant: 10).isActive = true 65 | editorView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 10).isActive = true 66 | editorView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -10).isActive = true 67 | editorView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -10).isActive = true 68 | } 69 | 70 | private func setupCollectioView() { 71 | collectionView.backgroundColor = .clear 72 | collectionView.register(ToolItemCell.self, forCellWithReuseIdentifier: ToolItemCellSettings.id) 73 | } 74 | 75 | private lazy var colorPickerNavController: UINavigationController = { 76 | let colorSelectionController = EFColorSelectionViewController() 77 | colorSelectionController.delegate = self 78 | colorSelectionController.color = .black 79 | colorSelectionController.setMode(mode: .all) 80 | 81 | let nav = UINavigationController(rootViewController: colorSelectionController) 82 | if UIUserInterfaceSizeClass.compact == self.traitCollection.horizontalSizeClass { 83 | let doneBtn: UIBarButtonItem = UIBarButtonItem( 84 | title: NSLocalizedString("Done", comment: ""), 85 | style: .done, 86 | target: self, 87 | action: #selector(dismissColorPicker) 88 | ) 89 | colorSelectionController.navigationItem.rightBarButtonItem = doneBtn 90 | } 91 | 92 | return nav 93 | }() 94 | 95 | private func showColorPicker() { 96 | self.present(colorPickerNavController, animated: true, completion: nil) 97 | } 98 | 99 | @objc private func dismissColorPicker() { 100 | colorPickerNavController.dismiss(animated: true, completion: nil) 101 | } 102 | 103 | private func showInputAlert(type: ToolOptionType) { 104 | var textField: UITextField? 105 | 106 | let alertController = UIAlertController(title: type.description, message: nil, preferredStyle: .alert) 107 | alertController.addTextField { pTextField in 108 | pTextField.clearButtonMode = .whileEditing 109 | pTextField.borderStyle = .none 110 | textField = pTextField 111 | } 112 | 113 | alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) 114 | 115 | alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { (pAction) in 116 | if let inputValue = textField?.text { 117 | switch type { 118 | case .makeLink: 119 | self.editorView.makeLink(url: inputValue) 120 | case .insertImage: 121 | self.editorView.insertImage(url: inputValue) 122 | case .setTextSize: 123 | self.editorView.setText(size: Int(inputValue) ?? 20) 124 | case .insertHTML: 125 | self.editorView.insertHTML(inputValue) 126 | default: 127 | break 128 | } 129 | } 130 | })) 131 | 132 | self.present(alertController, animated: true, completion: nil) 133 | } 134 | 135 | private func showAlert(text: String?) { 136 | let alertController = UIAlertController(title: "", message: text, preferredStyle: .alert) 137 | 138 | alertController.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil)) 139 | 140 | self.present(alertController, animated: true, completion: nil) 141 | } 142 | } 143 | 144 | extension ViewController: UICollectionViewDataSource, UICollectionViewDelegate { 145 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 146 | return ToolOptionType.allCases.count 147 | } 148 | 149 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 150 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ToolItemCellSettings.id, for: indexPath) 151 | (cell as? ToolItemCell)?.configCell(option: ToolOptionType(rawValue: indexPath.row)!, 152 | attribute: editorView.selectedTextAttribute) 153 | return cell 154 | } 155 | 156 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 157 | selectedOption = ToolOptionType(rawValue: indexPath.row) 158 | 159 | if let option = selectedOption { 160 | switch option { 161 | case .bold: 162 | editorView.bold() 163 | case .italic: 164 | editorView.italic() 165 | case .strikethrough: 166 | editorView.strikethrough() 167 | case .underline: 168 | editorView.underline() 169 | case .clear: 170 | editorView.clear() 171 | case .removeLink: 172 | editorView.removeLink() 173 | case .setTextColor, .setTextBackgroundColor: 174 | showColorPicker() 175 | case .insertHTML, .makeLink, .insertImage, .setTextSize: 176 | showInputAlert(type: option) 177 | case .getHTML: 178 | editorView.getHTML { html in 179 | self.showAlert(text: html) 180 | } 181 | case .focusEditor: 182 | editorView.focus(true) 183 | case .blurEditor: 184 | editorView.focus(false) 185 | case .getHeight: 186 | break 187 | } 188 | } 189 | } 190 | } 191 | 192 | extension ViewController: UICollectionViewDelegateFlowLayout { 193 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 194 | guard let option = ToolOptionType(rawValue: indexPath.row) else { return .zero } 195 | 196 | let width = option.description 197 | .size(withAttributes: [.font: ToolItemCellSettings.normalfont]).width + 15 198 | 199 | return CGSize(width: width, height: ToolItemCellSettings.height) 200 | } 201 | } 202 | 203 | extension ViewController: SQTextEditorDelegate { 204 | 205 | func editorDidLoad(_ editor: SQTextEditorView) { 206 | print("editorDidLoad") 207 | } 208 | 209 | func editor(_ editor: SQTextEditorView, selectedTextAttributeDidChange attribute: SQTextAttribute) { 210 | collectionView.reloadData() 211 | } 212 | 213 | func editor(_ editor: SQTextEditorView, contentHeightDidChange height: Int) { 214 | print("contentHeightDidChange = \(height)") 215 | } 216 | 217 | func editorDidFocus(_ editor: SQTextEditorView) { 218 | print("editorDidFocus") 219 | } 220 | 221 | func editor(_ editor: SQTextEditorView, cursorPositionDidChange position: SQEditorCursorPosition) { 222 | print(position) 223 | } 224 | 225 | func editorDidTapDoneButton(_ editor: SQTextEditorView) { 226 | print("editorDidTapDoneButton") 227 | } 228 | } 229 | 230 | extension ViewController: EFColorSelectionViewControllerDelegate { 231 | 232 | func colorViewController(_ colorViewCntroller: EFColorSelectionViewController, didChangeColor color: UIColor) { 233 | if let option = selectedOption { 234 | switch option { 235 | case .setTextColor: 236 | editorView.setText(color: color) 237 | case .setTextBackgroundColor: 238 | editorView.setText(backgroundColor: color) 239 | default: 240 | break 241 | } 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /Example/Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Example/Tests/SQTextEditorViewTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SQRichTextEditor 3 | 4 | class SQTextEditorViewTests: XCTestCase, SQTextEditorDelegate { 5 | 6 | private let timeout: TimeInterval = 600 7 | private var editor: SQTextEditorView! 8 | private var editorDidLoadHandler: (() -> ())? 9 | private let logoImgUrl = "https://i.imgur.com/tSwpCeL.png" 10 | 11 | private func makeTestHTML(id: String? = nil, value: String? = nil) -> String { 12 | let tag = id == nil ? "
" : "
" 13 | return "\(tag)\(value ?? "")
" 14 | } 15 | 16 | override func setUp() { 17 | super.setUp() 18 | 19 | editor = SQTextEditorView() 20 | editor.delegate = self 21 | 22 | let exp = expectation(description: "\(#function)\(#line)") 23 | 24 | editorDidLoadHandler = { 25 | exp.fulfill() 26 | } 27 | 28 | waitForExpectations(timeout: timeout, handler: nil) 29 | } 30 | 31 | override func tearDown() { 32 | editor = nil 33 | 34 | super.tearDown() 35 | } 36 | 37 | func testGetHTML() { 38 | let exp = expectation(description: "\(#function)\(#line)") 39 | 40 | editor.getHTML(completion: { html in 41 | 42 | XCTAssert(html == self.makeTestHTML()) 43 | 44 | exp.fulfill() 45 | }) 46 | 47 | waitForExpectations(timeout: timeout, handler: nil) 48 | } 49 | 50 | func testInsertHTML() { 51 | let exp = expectation(description: "\(#function)\(#line)") 52 | 53 | let testHtml = makeTestHTML(value: "test") 54 | 55 | editor.insertHTML(testHtml) { error in 56 | XCTAssertNil(error) 57 | 58 | self.editor.getHTML(completion: { html in 59 | XCTAssert(html == testHtml) 60 | exp.fulfill() 61 | }) 62 | } 63 | 64 | waitForExpectations(timeout: timeout, handler: nil) 65 | } 66 | 67 | func testSetSelectionInSameElementId() { 68 | let exp = expectation(description: "\(#function)\(#line)") 69 | 70 | let value = "test text" 71 | let tagId = "test" 72 | let testHtml = makeTestHTML(id: tagId, value: value) 73 | let startIndex = 0 74 | let endIndex = 2 75 | 76 | editor.insertHTML(testHtml) { error in 77 | XCTAssertNil(error) 78 | 79 | self.editor.setTextSelection(startElementId: tagId, 80 | startIndex: startIndex, 81 | endElementId: tagId, 82 | endIndex: endIndex, 83 | completion: { error in 84 | XCTAssertNil(error) 85 | 86 | self.editor.getSelectedText { text in 87 | XCTAssert((text ?? "") == value.prefix(endIndex - startIndex)) 88 | exp.fulfill() 89 | } 90 | }) 91 | } 92 | 93 | waitForExpectations(timeout: timeout, handler: nil) 94 | } 95 | 96 | func testSetSelectionCrossElementId() { 97 | let exp = expectation(description: "\(#function)\(#line)") 98 | 99 | let tagId1 = "a" 100 | let tagId2 = "b" 101 | 102 | let testHtml = "
123
456
" 103 | 104 | editor.insertHTML(testHtml) { error in 105 | XCTAssertNil(error) 106 | 107 | self.editor.setTextSelection(startElementId: tagId1, startIndex: 2, endElementId: tagId2, endIndex: 1, completion: { error in 108 | XCTAssertNil(error) 109 | 110 | self.editor.getSelectedText { text in 111 | XCTAssert((text ?? "") 112 | .replacingOccurrences(of: "\n", with: "") 113 | .replacingOccurrences(of: " ", with: "") 114 | == "34") 115 | exp.fulfill() 116 | } 117 | }) 118 | } 119 | 120 | waitForExpectations(timeout: timeout, handler: nil) 121 | } 122 | 123 | func testBold() { 124 | let exp = expectation(description: "\(#function)\(#line)") 125 | 126 | let tagId = "testBold" 127 | let testValue = "123" 128 | let testHtml = makeTestHTML(id: tagId, value: testValue) 129 | 130 | editor.insertHTML(testHtml) { error in 131 | XCTAssertNil(error) 132 | 133 | self.editor.setTextSelection(startElementId: tagId, startIndex: 0, endElementId: tagId, endIndex: 2, completion: { error in 134 | XCTAssertNil(error) 135 | 136 | self.editor.bold { error in 137 | sleep(1) 138 | XCTAssertNil(error) 139 | XCTAssert(self.editor.selectedTextAttribute.format.hasBold) 140 | exp.fulfill() 141 | } 142 | }) 143 | } 144 | 145 | waitForExpectations(timeout: timeout, handler: nil) 146 | } 147 | 148 | func testItalic() { 149 | let exp = expectation(description: "\(#function)\(#line)") 150 | 151 | let tagId = "test" 152 | 153 | let testHtml = "
123
" 154 | 155 | editor.insertHTML(testHtml) { error in 156 | XCTAssertNil(error) 157 | 158 | self.editor.setTextSelection(startElementId: tagId, startIndex: 0, endElementId: tagId, endIndex: 2, completion: { error in 159 | XCTAssertNil(error) 160 | 161 | self.editor.italic { error in 162 | sleep(1) 163 | XCTAssertNil(error) 164 | XCTAssert(self.editor.selectedTextAttribute.format.hasItalic) 165 | exp.fulfill() 166 | } 167 | }) 168 | } 169 | 170 | waitForExpectations(timeout: timeout, handler: nil) 171 | } 172 | 173 | func testUnderline() { 174 | let exp = expectation(description: "\(#function)\(#line)") 175 | 176 | let tagId = "testUnderline" 177 | 178 | let testHtml = "
123
" 179 | 180 | editor.insertHTML(testHtml) { error in 181 | XCTAssertNil(error) 182 | 183 | self.editor.setTextSelection(startElementId: tagId, startIndex: 0, endElementId: tagId, endIndex: 2, completion: { error in 184 | XCTAssertNil(error) 185 | 186 | self.editor.underline { error in 187 | sleep(1) 188 | XCTAssertNil(error) 189 | XCTAssert(self.editor.selectedTextAttribute.format.hasUnderline) 190 | exp.fulfill() 191 | } 192 | }) 193 | } 194 | 195 | waitForExpectations(timeout: timeout, handler: nil) 196 | } 197 | 198 | func testStrikethrough() { 199 | let exp = expectation(description: "\(#function)\(#line)") 200 | 201 | let tagId = "testStrikethrough" 202 | 203 | let testHtml = "
123
" 204 | 205 | editor.insertHTML(testHtml) { error in 206 | XCTAssertNil(error) 207 | 208 | self.editor.setTextSelection(startElementId: tagId, startIndex: 0, endElementId: tagId, endIndex: 2, completion: { error in 209 | XCTAssertNil(error) 210 | 211 | self.editor.strikethrough { error in 212 | sleep(1) 213 | XCTAssertNil(error) 214 | XCTAssert(self.editor.selectedTextAttribute.format.hasStrikethrough) 215 | exp.fulfill() 216 | } 217 | }) 218 | } 219 | 220 | waitForExpectations(timeout: timeout, handler: nil) 221 | } 222 | 223 | func testSetTextSize() { 224 | let exp = expectation(description: "\(#function)\(#line)") 225 | 226 | let tagId = "testSetTextSize" 227 | 228 | let testHtml = "
123
" 229 | 230 | editor.insertHTML(testHtml) { error in 231 | XCTAssertNil(error) 232 | 233 | self.editor.setTextSelection(startElementId: tagId, startIndex: 0, endElementId: tagId, endIndex: 2, completion: { error in 234 | XCTAssertNil(error) 235 | 236 | let size = 25 237 | 238 | self.editor.setText(size: size, completion: { error in 239 | sleep(1) 240 | XCTAssert(self.editor.selectedTextAttribute.textInfo.size == size) 241 | exp.fulfill() 242 | }) 243 | }) 244 | } 245 | 246 | waitForExpectations(timeout: timeout, handler: nil) 247 | } 248 | 249 | func testSetTextColor() { 250 | let exp = expectation(description: "\(#function)\(#line)") 251 | 252 | let tagId = "testSetTextColor" 253 | let testHtml = makeTestHTML(id: tagId, value: "test") 254 | 255 | editor.insertHTML(testHtml) { error in 256 | XCTAssertNil(error) 257 | 258 | self.editor.setTextSelection(startElementId: tagId, 259 | startIndex: 0, 260 | endElementId: tagId, 261 | endIndex: 2, 262 | completion: { error in 263 | XCTAssertNil(error) 264 | 265 | let color = UIColor.brown 266 | 267 | self.editor.setText(color: color, completion: { error in 268 | sleep(1) 269 | XCTAssert(self.editor.selectedTextAttribute.textInfo.color == color) 270 | exp.fulfill() 271 | }) 272 | }) 273 | } 274 | 275 | waitForExpectations(timeout: timeout, handler: nil) 276 | } 277 | 278 | func testSetTextBackgroundColor() { 279 | let exp = expectation(description: "\(#function)\(#line)") 280 | 281 | let tagId = "testSetTextBackgroundColor" 282 | let testHtml = makeTestHTML(id: tagId, value: "test") 283 | 284 | editor.insertHTML(testHtml) { error in 285 | XCTAssertNil(error) 286 | 287 | self.editor.setTextSelection(startElementId: tagId, 288 | startIndex: 0, 289 | endElementId: tagId, 290 | endIndex: 2, 291 | completion: { error in 292 | XCTAssertNil(error) 293 | 294 | let color = UIColor.brown 295 | 296 | self.editor.setText(backgroundColor: color, completion: { error in 297 | sleep(1) 298 | XCTAssert(self.editor.selectedTextAttribute.textInfo.backgroundColor == color) 299 | exp.fulfill() 300 | }) 301 | }) 302 | } 303 | 304 | waitForExpectations(timeout: timeout, handler: nil) 305 | } 306 | 307 | func testInsertImage() { 308 | let exp = expectation(description: "\(#function)\(#line)") 309 | 310 | editor.insertImage(url: logoImgUrl, completion: { error in 311 | XCTAssertNil(error) 312 | 313 | self.editor.getHTML { html in 314 | XCTAssertNotNil(html) 315 | 316 | if let html = html { 317 | XCTAssert(html == "

") 318 | } 319 | 320 | exp.fulfill() 321 | } 322 | }) 323 | 324 | waitForExpectations(timeout: timeout, handler: nil) 325 | } 326 | 327 | func testMakeLink() { 328 | let exp = expectation(description: "\(#function)\(#line)") 329 | 330 | let tagId = "logo" 331 | let value = "123" 332 | let testHtml = makeTestHTML(id: tagId, value: value) 333 | 334 | editor.insertHTML(testHtml) { error in 335 | XCTAssertNil(error) 336 | 337 | self.editor.setTextSelection(startElementId: tagId, 338 | startIndex: 0, 339 | endElementId: tagId, 340 | endIndex: value.count, 341 | completion: { error in 342 | XCTAssertNil(error) 343 | 344 | self.editor.makeLink(url: self.logoImgUrl, completion: { error in 345 | XCTAssertNil(error) 346 | 347 | self.editor.getHTML { html in 348 | XCTAssertNotNil(html) 349 | 350 | if let html = html { 351 | XCTAssert(html == "") 352 | } 353 | 354 | exp.fulfill() 355 | } 356 | }) 357 | 358 | }) 359 | } 360 | 361 | waitForExpectations(timeout: timeout, handler: nil) 362 | } 363 | 364 | func testRemoveLink() { 365 | let exp = expectation(description: "\(#function)\(#line)") 366 | 367 | let tagId = "testRemoveLink" 368 | let value = "123" 369 | let testHtml = makeTestHTML(id: tagId, value: value) 370 | 371 | editor.insertHTML(testHtml) { error in 372 | XCTAssertNil(error) 373 | 374 | self.editor.setTextSelection(startElementId: tagId, 375 | startIndex: 0, 376 | endElementId: tagId, 377 | endIndex: value.count, 378 | completion: { error in 379 | XCTAssertNil(error) 380 | 381 | self.editor.makeLink(url: self.logoImgUrl, completion: { error in 382 | XCTAssertNil(error) 383 | 384 | self.editor.removeLink { error in 385 | XCTAssertNil(error) 386 | 387 | self.editor.getHTML { html in 388 | XCTAssertNotNil(html) 389 | 390 | if let html = html { 391 | XCTAssert(html == "
\(value)
") 392 | } 393 | 394 | exp.fulfill() 395 | } 396 | } 397 | }) 398 | 399 | }) 400 | } 401 | 402 | waitForExpectations(timeout: timeout, handler: nil) 403 | } 404 | 405 | func testClearEditor() { 406 | let exp = expectation(description: "\(#function)\(#line)") 407 | 408 | let tagId = "testClearEditor" 409 | let value = "123" 410 | let testHtml = makeTestHTML(id: tagId, value: value) 411 | 412 | editor.insertHTML(testHtml) { error in 413 | XCTAssertNil(error) 414 | 415 | self.editor.clear { error in 416 | XCTAssertNil(error) 417 | 418 | self.editor.getHTML { html in 419 | XCTAssertNotNil(html) 420 | 421 | if let html = html { 422 | XCTAssert(html == self.makeTestHTML()) 423 | } 424 | 425 | exp.fulfill() 426 | } 427 | } 428 | } 429 | 430 | waitForExpectations(timeout: timeout, handler: nil) 431 | } 432 | 433 | //MARK: - SQTextEditorDelegate 434 | 435 | func editorDidLoad(_ editor: SQTextEditorView) { 436 | editorDidLoadHandler?() 437 | } 438 | } 439 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 conscientiousness 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![SQRichTextEditor](logo.png) 3 | 4 | [![Build Status](https://travis-ci.org/OneupNetwork/SQRichTextEditor.svg?branch=master)](https://travis-ci.org/OneupNetwork/SQRichTextEditor) 5 | [![Version](https://img.shields.io/cocoapods/v/SQRichTextEditor.svg?style=flat)](https://cocoapods.org/pods/SQRichTextEditor) 6 | [![License](https://img.shields.io/cocoapods/l/SQRichTextEditor.svg?style=flat)](https://cocoapods.org/pods/SQRichTextEditor) 7 | [![Platform](https://img.shields.io/cocoapods/p/SQRichTextEditor.svg?style=flat)](https://cocoapods.org/pods/SQRichTextEditor) 8 | [![Swift Version](https://img.shields.io/badge/swift-5.2-orange.svg)](https://git.zsinfo.nl/Zandor300/GeneralToolsFramework) 9 | 10 | ## Introduction 11 | I was looking for a WYSIWYG text editor for iOS and found some solutions but all of them didn't use `WKWebView`. Apple will stop accepting submissions of apps that use UIWebView [APIs](https://developer.apple.com/documentation/uikit/uiwebview). I found a [HTML5 rich text editor](https://github.com/neilj/Squire), which provides powerful cross-browser normalization in a flexible lightweight package. So I built this project and an iOS [bridge](https://github.com/OneupNetwork/Squire-native-bridge) for sending messages between Swift and JavaScript in WKWebView. 12 | 13 | ## Example 14 | 15 | To run the example project, clone the repo, and open `SQRichTextEditor.xcworkspace` from the Example directory. 16 | 17 | ![SQRichTextEditor](Demo1.gif) 18 | ![SQRichTextEditor](Demo2.gif) 19 | 20 | ## Requirements 21 | 22 | - Deployment target iOS 10.0+ 23 | - Swift 5+ 24 | - Xcode 11+ 25 | 26 | ## Installation 27 | 28 | SQRichTextEditor is available through [CocoaPods](https://cocoapods.org). To install 29 | it, simply add the following line to your Podfile: 30 | 31 | ```ruby 32 | pod 'SQRichTextEditor' 33 | ``` 34 | 35 | ## Features 36 | 37 | - [x] Bold 38 | - [x] Italic 39 | - [x] Underline 40 | - [x] Strikethrough 41 | - [x] Insert Image 42 | - [x] Insert HTML 43 | - [x] Make Link 44 | - [x] Text Color 45 | - [x] Text Size 46 | - [x] Text Background Color 47 | 48 | ## Getting Started 49 | The `SQTextEditorView` is a plain UIView subclass, so you are free to use it wherever you want. 50 | 51 | ```swift 52 | import SQRichTextEditor 53 | 54 | private lazy var editorView: SQTextEditorView = { 55 | /// You can pass the custom css string, if you want to change the default editor style 56 | /// var customCss: String? 57 | /// if let cssURL = Bundle.main.url(forResource: isDarkMode ? "editor_dark" : "editor_light", withExtension: "css"), 58 | /// let css = try? String(contentsOf: cssURL, encoding: .utf8) { 59 | /// customCss = css 60 | /// } 61 | /// let _editorView = SQTextEditorView(customCss: customCss) 62 | 63 | let _editorView = SQTextEditorView() 64 | _editorView.translatesAutoresizingMaskIntoConstraints = false 65 | return _editorView 66 | }() 67 | 68 | view.addSubview(editorView) 69 | editorView.topAnchor.constraint(equalTo: collectionView.bottomAnchor, constant: 10).isActive = true 70 | editorView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 10).isActive = true 71 | editorView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -10).isActive = true 72 | editorView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -10).isActive = true 73 | 74 | ``` 75 | 76 | ### Delegate 77 | 78 | You can check events by implement SQTextEditorDelegate: 79 | 80 | ```swift 81 | editorView.delegate = self 82 | ``` 83 | 84 | Delegate has these functions: 85 | 86 | ```swift 87 | //Called when the editor components is ready. 88 | optional func editorDidLoad(_ editor: SQTextEditorView) 89 | 90 | //Called when the user selected some text or moved the cursor to a different position. 91 | optional func editor(_ editor: SQTextEditorView, 92 | selectedTextAttributeDidChange attribute: SQTextAttribute) 93 | 94 | //Called when the user inserted, deleted or changed the style of some text. 95 | optional func editor(_ editor: SQTextEditorView, 96 | contentHeightDidChange height: Int) 97 | 98 | //Called when the user tapped the editor 99 | optional func editorDidFocus(_ editor: SQTextEditorView) 100 | 101 | //Called when the user tapped the done button of keyboard tool bar 102 | optional func editorDidTapDoneButton(_ editor: SQTextEditorView) 103 | 104 | //Called when the editor cursor moved 105 | optional func editor(_ editor: SQTextEditorView, cursorPositionDidChange position: SQEditorCursorPosition) 106 | ``` 107 | 108 | ## Editor Functions 109 | 110 | ### getHTML 111 | Returns the HTML value of the editor in its current state. 112 | 113 | ```swift 114 | func getHTML(completion: @escaping (_ html: String?) -> ()) 115 | ``` 116 | 117 | ### insertHTML 118 | Inserts an HTML fragment at the current cursor location, or replaces the selection if selected. The value supplied should not contain tags or anything outside of that. A block to invoke when script evaluation completes or fails. 119 | 120 | 121 | ```swift 122 | func insertHTML(_ html: String, completion: ((_ error: Error?) -> ())? = nil) 123 | ``` 124 | 125 | ### getSelectedText 126 | The text currently selected in the editor. 127 | 128 | 129 | ```swift 130 | func getSelectedText(completion: @escaping (_ text: String?) -> ()) 131 | ``` 132 | 133 | ### bold 134 | Makes any non-bold currently selected text bold (by wrapping it in a 'b' tag), otherwise removes any bold formatting from the selected text. A block to invoke when script evaluation completes or fails. 135 | 136 | 137 | ```swift 138 | func bold(completion: ((_ error: Error?) -> ())? = nil) 139 | ``` 140 | 141 | ### italic 142 | By wrapping it in an 'i' tag. 143 | 144 | ```swift 145 | func italic(completion: ((_ error: Error?) -> ())? = nil) 146 | ``` 147 | 148 | ### underline 149 | By wrapping it in an 'u' tag. 150 | 151 | ```swift 152 | func underline(completion: ((_ error: Error?) -> ())? = nil) 153 | ``` 154 | 155 | ### strikethrough 156 | By wrapping it in a 'del' tag. 157 | 158 | ```swift 159 | func strikethrough(completion: ((_ error: Error?) -> ())? = nil) 160 | ``` 161 | 162 | ### setTextColor 163 | Sets the color of the selected text. 164 | 165 | ```swift 166 | func setText(color: UIColor, completion: ((_ error: Error?) -> ())? = nil) 167 | ``` 168 | 169 | ### setTextBackgroundColor 170 | Sets the color of the background of the selected text. 171 | 172 | ```swift 173 | func setText(backgroundColor: UIColor, completion: ((_ error: Error?) -> ())? = nil) 174 | ``` 175 | 176 | ### setTextSize 177 | Sets the font size for the selected text. 178 | 179 | ```swift 180 | func setText(size: Int, completion: ((_ error: Error?) -> ())? = nil) 181 | ``` 182 | 183 | ### insertImage 184 | Inserts an image at the current cursor location. A block to invoke when script evaluation completes or fails. 185 | 186 | ```swift 187 | func insertImage(url: String, completion: ((_ error: Error?) -> ())? = nil) 188 | ``` 189 | 190 | ### makeLink 191 | Makes the currently selected text a link. If no text is selected, the URL or email will be inserted as text at the current cursor point and made into a link. A block to invoke when script evaluation completes or fails. 192 | 193 | ```swift 194 | func makeLink(url: String, completion: ((_ error: Error?) -> ())? = nil) 195 | ``` 196 | 197 | ### removeLink 198 | Removes any link that is currently at least partially selected. A block to invoke when script evaluation completes or fails. 199 | 200 | ```swift 201 | func removeLink(completion: ((_ error: Error?) -> ())? = nil) 202 | ``` 203 | 204 | ### clear 205 | Clear Editor's content. The method removes all Blocks and inserts new initial empty Block 206 | `

`. A block to invoke when script evaluation completes or fails. 207 | 208 | ```swift 209 | func clear(completion: ((_ error: Error?) -> ())? = nil) 210 | ``` 211 | 212 | ### focus 213 | The editor gained focus or lost focus. 214 | 215 | ```swift 216 | func focus(_ isFocused: Bool, completion: ((_ error: Error?) -> ())? = nil) 217 | ``` 218 | 219 | ## Contributions 220 | 221 | `SQRichTextEditor` welcomes both fixes, improvements, and feature additions. If you'd like to contribute, open a pull request with a detailed description of your changes. 222 | 223 | If you'd like to fix or add some functions for `editor.js` or `editor.css`, you can open a pull request in this [repo](https://github.com/OneupNetwork/Squire-native-bridge). 224 | 225 | ## Author 226 | 227 | Yuwei Lin, jesse@gamer.com.tw @ [OneupNetwork](https://www.gamer.com.tw/) 228 | 229 | ## License 230 | 231 | SQRichTextEditor is available under the MIT license. See the LICENSE file for more info. 232 | 233 | 234 | ## Attribution 235 | 236 | `SQRichTextEditor` uses portions of code from the following powerful source: 237 | 238 | | Component | Description | License | 239 | | :------------ | :------------ | :------------ | 240 | | [Squire](https://github.com/neilj/Squire) | Squire is an HTML5 rich text editor, which provides powerful cross-browser normalisation in a flexible lightweight package. | [MIT](https://github.com/neilj/Squire/blob/master/LICENSE) | 241 | | [EFColorPicker](https://github.com/EFPrefix/EFColorPicker) | A lightweight color picker in Swift. | [MIT](https://github.com/EFPrefix/EFColorPicker/blob/master/LICENSE) | 242 | -------------------------------------------------------------------------------- /SQRichTextEditor.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint SQRichTextEditor.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'SQRichTextEditor' 11 | s.version = '1.0.2' 12 | s.summary = 'A rich text WYSIWYG editor for iOS base on Squire.' 13 | s.description = "A rich text WYSIWYG editor for iOS, which is based on Squire using HTML5 and javascript." 14 | s.homepage = 'https://github.com/OneupNetwork/SQRichTextEditor' 15 | 16 | s.license = { :type => 'MIT', :file => 'LICENSE' } 17 | s.author = { 'Yuwei Lin' => 'jesse@gamer.com.tw' } 18 | s.source = { :git => 'https://github.com/OneupNetwork/SQRichTextEditor.git', :tag => s.version.to_s } 19 | 20 | s.ios.deployment_target = '10.0' 21 | 22 | s.ios.source_files = 'SQRichTextEditor/Classes/*' 23 | 24 | s.ios.resources = "SQRichTextEditor/Assets/Editor/*" 25 | 26 | #s.ios.resource_bundles = { 'imageResource' => ['SQRichTextEditor/Assets/*.xcassets'] } 27 | 28 | s.ios.frameworks = 'UIKit', 'WebKit' 29 | 30 | s.swift_version = '5.1' 31 | end 32 | -------------------------------------------------------------------------------- /SQRichTextEditor/Assets/Editor/editor.css: -------------------------------------------------------------------------------- 1 | html{height:100%}body{margin:0;padding:0;border:0;height:100%}#outer-container{min-height:100%}#editor{font-family:-apple-system,BlinkMacSystemFont,sans-serif;min-height:0;padding:0;background:0 0;cursor:text}#editor:focus{outline:0}img{max-height:100%;max-width:100%;width:auto;height:auto}h1{font-size:1.95em}h2{font-size:123.1%}h3{font-size:108%}h1,h2,h3,p{margin:1em 0}h4,h5,h6{margin:0}ol,ul{margin:0 1em;padding:0 1em}blockquote{margin:0;padding:0 10px}pre{white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word;border-radius:3px;border:1px solid #ccc;padding:7px 10px;background:#f6f6f6;font-family:menlo,consolas,monospace;font-size:90%}code{border-radius:3px;border:1px solid #ccc;padding:1px 3px;background:#f6f6f6;font-family:menlo,consolas,monospace;font-size:90%} -------------------------------------------------------------------------------- /SQRichTextEditor/Assets/Editor/editor.js: -------------------------------------------------------------------------------- 1 | "use strict";const container=document.getElementById("editor"),editor=new Squire(container,{blockTag:"div",tagAttributes:{a:{target:"_blank"}}}),tagName={bold:"b",italic:"i",strikethrough:"del",underline:"u",link:"a"};var lastFontInfo={},newFontInfo={},lastFormat={bold:!1,italic:!1,strikethrough:!1,underline:!1,link:!1},newFormat=clone(lastFormat),lastHeight=0,newHeight=0,isFocused=!1;function detectCursorPosition(){let t=editor.getCursorPosition();postCursorPosition({top:t.top,right:t.right,bottom:t.bottom,left:t.left,width:t.width,height:t.height,x:t.x,y:t.y})}function detectFontInfoChnaged(){let t=editor.getFontInfo().size;null!=t&&(t=t.replace("px","")),newFontInfo={color:rgbToHex(editor.getFontInfo().color),backgroundColor:rgbToHex(editor.getFontInfo().backgroundColor),family:editor.getFontInfo().family,size:t},isEquivalent(lastFontInfo,newFontInfo)||postFontInfo(lastFontInfo=clone(newFontInfo))}function detectFormatChnaged(){let t=Object.getOwnPropertyNames(lastFormat);for(let e=0;e
"),editor.moveCursorToStart()}function makeLink(t){editor.makeLink(t)}function removeLink(){editor.removeLink()}function setTextSelection(t,e,o,n){let r=Array.prototype.find.call(document.getElementById(t).childNodes,(function(t){return t.nodeType==Node.TEXT_NODE})),i=Array.prototype.find.call(document.getElementById(o).childNodes,(function(t){return t.nodeType==Node.TEXT_NODE})),s=editor.createRange(r,e,i,n);editor.setSelection(s)}function getSelectedText(){return editor.getSelectedText()}function setTextBackgroundColor(t){editor.setHighlightColour(t)}function getEditorHeight(){return container.clientHeight}function postFontInfo(t){window.webkit.messageHandlers.fontInfo.postMessage(t)}function postFormat(t){window.webkit.messageHandlers.format.postMessage(t)}function postFocusStatus(t){window.webkit.messageHandlers.isFocused.postMessage(t)}function postCursorPosition(t){window.webkit.messageHandlers.cursorPosition.postMessage(t)}document.getElementById("outer-container").onclick=function(t){t.target===this&&(isFocused||editor.focus())},editor.addEventListener("focus",(function(){postFocusStatus(isFocused=!0)}),!1),editor.addEventListener("blur",(function(){postFocusStatus(isFocused=!1)}),!1),editor.addEventListener("input",(function(){detectFontInfoChnaged(),detectFormatChnaged()}),!1),editor.addEventListener("select",(function(){detectFontInfoChnaged(),detectFormatChnaged()}),!1),editor.addEventListener("cursor",(function(){detectFontInfoChnaged(),detectFormatChnaged(),detectCursorPosition()}),!1); -------------------------------------------------------------------------------- /SQRichTextEditor/Assets/Editor/index.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /SQRichTextEditor/Classes/Helper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Helper.swift 3 | // Pods-SQRichTextEditor_Example 4 | // 5 | // Created by Jesse on 2019/12/16. 6 | // 7 | 8 | import UIKit 9 | 10 | struct Helper { 11 | static func hexToRGBColor(hex string: String) -> UIColor { 12 | let hex = string.hasPrefix("#") 13 | ? String(string.dropFirst()) 14 | : string 15 | guard hex.count == 6 else { 16 | return UIColor(white: 1.0, alpha: 0.0) 17 | } 18 | 19 | return UIColor( 20 | red: CGFloat((Int(hex, radix: 16)! >> 16) & 0xFF) / 255.0, 21 | green: CGFloat((Int(hex, radix: 16)! >> 8) & 0xFF) / 255.0, 22 | blue: CGFloat((Int(hex, radix: 16)!) & 0xFF) / 255.0, alpha: 1.0) 23 | } 24 | 25 | static func rgbColorToHex(color: UIColor) -> String { 26 | var (r, g, b, a): (CGFloat, CGFloat, CGFloat, CGFloat) = (0.0, 0.0, 0.0, 0.0) 27 | 28 | color.getRed(&r, green: &g, blue: &b, alpha: &a) 29 | 30 | return String(format: "#%02X%02X%02X", Int(r * 255), Int(g * 255), Int(b * 255)) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /SQRichTextEditor/Classes/KeyboardRequiresUserInteraction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SQWebView.swift 3 | // Pods-SQRichTextEditor_Example 4 | // 5 | // Created by jesse on 2020/5/5. 6 | // 7 | 8 | import Foundation 9 | import WebKit 10 | 11 | /// Programmatically focus on a form in a webview (WKWebView) 12 | /// https://stackoverflow.com/a/46029192 13 | 14 | typealias OldClosureType = @convention(c) (Any, Selector, UnsafeRawPointer, Bool, Bool, Any?) -> Void 15 | typealias NewClosureType = @convention(c) (Any, Selector, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void 16 | 17 | extension WKWebView { 18 | 19 | func setKeyboardRequiresUserInteraction( _ value: Bool) { 20 | guard let WKContentView: AnyClass = NSClassFromString("WKContentView") else { 21 | print("keyboardDisplayRequiresUserAction extension: Cannot find the WKContentView class") 22 | return 23 | } 24 | // For iOS 10, * 25 | let sel_10: Selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:userObject:") 26 | // For iOS 11.3, * 27 | let sel_11_3: Selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:") 28 | // For iOS 12.2, * 29 | let sel_12_2: Selector = sel_getUid("_elementDidFocus:userIsInteracting:blurPreviousNode:changingActivityState:userObject:") 30 | // For iOS 13.0, * 31 | let sel_13_0: Selector = sel_getUid("_elementDidFocus:userIsInteracting:blurPreviousNode:activityStateChanges:userObject:") 32 | 33 | if let method = class_getInstanceMethod(WKContentView, sel_10) { 34 | let originalImp: IMP = method_getImplementation(method) 35 | let original: OldClosureType = unsafeBitCast(originalImp, to: OldClosureType.self) 36 | let block : @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3) in 37 | original(me, sel_10, arg0, !value, arg2, arg3) 38 | } 39 | let imp: IMP = imp_implementationWithBlock(block) 40 | method_setImplementation(method, imp) 41 | } 42 | 43 | if let method = class_getInstanceMethod(WKContentView, sel_11_3) { 44 | let originalImp: IMP = method_getImplementation(method) 45 | let original: NewClosureType = unsafeBitCast(originalImp, to: NewClosureType.self) 46 | let block : @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3, arg4) in 47 | original(me, sel_11_3, arg0, !value, arg2, arg3, arg4) 48 | } 49 | let imp: IMP = imp_implementationWithBlock(block) 50 | method_setImplementation(method, imp) 51 | } 52 | 53 | if let method = class_getInstanceMethod(WKContentView, sel_12_2) { 54 | let originalImp: IMP = method_getImplementation(method) 55 | let original: NewClosureType = unsafeBitCast(originalImp, to: NewClosureType.self) 56 | let block : @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3, arg4) in 57 | original(me, sel_12_2, arg0, !value, arg2, arg3, arg4) 58 | } 59 | let imp: IMP = imp_implementationWithBlock(block) 60 | method_setImplementation(method, imp) 61 | } 62 | 63 | if let method = class_getInstanceMethod(WKContentView, sel_13_0) { 64 | let originalImp: IMP = method_getImplementation(method) 65 | let original: NewClosureType = unsafeBitCast(originalImp, to: NewClosureType.self) 66 | let block : @convention(block) (Any, UnsafeRawPointer, Bool, Bool, Bool, Any?) -> Void = { (me, arg0, arg1, arg2, arg3, arg4) in 67 | original(me, sel_13_0, arg0, !value, arg2, arg3, arg4) 68 | } 69 | let imp: IMP = imp_implementationWithBlock(block) 70 | method_setImplementation(method, imp) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /SQRichTextEditor/Classes/RepeatingTimer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepeatingTimer.swift 3 | // Pods-SQRichTextEditor_Example 4 | // 5 | // Created by jesse on 2020/5/29. 6 | // 7 | 8 | import Foundation 9 | 10 | class RepeatingTimer { 11 | 12 | let timeInterval: TimeInterval 13 | 14 | init(timeInterval: TimeInterval) { 15 | self.timeInterval = timeInterval 16 | } 17 | 18 | private lazy var timer: DispatchSourceTimer = { 19 | let t = DispatchSource.makeTimerSource() 20 | t.schedule(deadline: .now() + self.timeInterval, repeating: self.timeInterval) 21 | t.setEventHandler(handler: { [weak self] in 22 | self?.eventHandler?() 23 | }) 24 | return t 25 | }() 26 | 27 | var eventHandler: (() -> Void)? 28 | 29 | private enum State { 30 | case suspended 31 | case resumed 32 | } 33 | 34 | private var state: State = .suspended 35 | 36 | deinit { 37 | timer.setEventHandler {} 38 | timer.cancel() 39 | resume() 40 | eventHandler = nil 41 | } 42 | 43 | func resume() { 44 | if state == .resumed { 45 | return 46 | } 47 | state = .resumed 48 | timer.resume() 49 | } 50 | 51 | func suspend() { 52 | if state == .suspended { 53 | return 54 | } 55 | state = .suspended 56 | timer.suspend() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /SQRichTextEditor/Classes/SQTextAttribute.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SQAttributedText.swift 3 | // Pods-SQRichTextEditor_Example 4 | // 5 | // Created by Jesse on 2019/12/12. 6 | // 7 | 8 | import UIKit 9 | 10 | public class SQTextAttribute: NSObject { 11 | public var format = SQTextAttributeFormat() 12 | public var textInfo = SQTextAttributeTextInfo() 13 | } 14 | 15 | public struct SQTextAttributeFormat: Codable { 16 | public var hasBold = false 17 | public var hasItalic = false 18 | public var hasStrikethrough = false 19 | public var hasUnderline = false 20 | public var hasLink = false 21 | 22 | enum CodingKeys: String, CodingKey { 23 | case hasBold = "bold" 24 | case hasItalic = "italic" 25 | case hasStrikethrough = "strikethrough" 26 | case hasUnderline = "underline" 27 | case hasLink = "link" 28 | } 29 | } 30 | 31 | public struct SQTextAttributeTextInfo: Codable { 32 | private var textColor: String? 33 | private var textBackgroundColor: String? 34 | 35 | public var size: Int? 36 | 37 | public var color: UIColor? { 38 | guard let textColor = textColor else { return nil } 39 | return Helper.hexToRGBColor(hex: textColor) 40 | } 41 | 42 | public var backgroundColor: UIColor? { 43 | guard let textBackgroundColor = textBackgroundColor else { return nil } 44 | return Helper.hexToRGBColor(hex: textBackgroundColor) 45 | } 46 | 47 | enum CodingKeys: String, CodingKey { 48 | case textColor = "color" 49 | case textBackgroundColor = "backgroundColor" 50 | case size = "size" 51 | } 52 | 53 | init(textHexColor: String? = nil, 54 | textBackgroundHexColor: String? = nil, 55 | size: Int? = nil) { 56 | self.textColor = textHexColor 57 | self.textBackgroundColor = textBackgroundHexColor 58 | self.size = size 59 | } 60 | 61 | public init(from decoder: Decoder) throws { 62 | let container = try decoder.container(keyedBy: CodingKeys.self) 63 | 64 | textColor = try? container.decode(String.self, forKey: .textColor) 65 | 66 | textBackgroundColor = try? container.decode(String.self, forKey: .textBackgroundColor) 67 | 68 | if let value = try? container.decode(String.self, forKey: .size) { 69 | size = Int(value) 70 | } 71 | } 72 | } 73 | 74 | public struct SQEditorCursorPosition: Codable { 75 | public var bottom: Double = 0 76 | public var height: Double = 0 77 | public var left: Double = 0 78 | public var right: Double = 0 79 | public var top: Double = 0 80 | public var width: Double = 0 81 | public var x: Double = 0 82 | public var y: Double = 0 83 | } 84 | -------------------------------------------------------------------------------- /SQRichTextEditor/Classes/SQTextEditorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SQTextEditorView.swift 3 | // SQRichTextEditor 4 | // 5 | // Created by Jesse on 2019/12/10. 6 | // 7 | 8 | import UIKit 9 | import WebKit 10 | 11 | public protocol SQTextEditorDelegate: class { 12 | 13 | /// Called when the editor components is ready. 14 | func editorDidLoad(_ editor: SQTextEditorView) 15 | 16 | /// Called when the user selected some text or moved the cursor to a different position. 17 | func editor(_ editor: SQTextEditorView, selectedTextAttributeDidChange attribute: SQTextAttribute) 18 | 19 | /// Called when the user inserted, deleted or changed the style of some text. 20 | func editor(_ editor: SQTextEditorView, contentHeightDidChange height: Int) 21 | 22 | func editorDidFocus(_ editor: SQTextEditorView) 23 | 24 | func editorDidTapDoneButton(_ editor: SQTextEditorView) 25 | 26 | func editor(_ editor: SQTextEditorView, cursorPositionDidChange position: SQEditorCursorPosition) 27 | } 28 | 29 | /// Make optional protocol methods 30 | public extension SQTextEditorDelegate { 31 | func editorDidLoad(_ editor: SQTextEditorView) {} 32 | func editor(_ editor: SQTextEditorView, selectedTextAttributeDidChange attribute: SQTextAttribute) {} 33 | func editor(_ editor: SQTextEditorView, contentHeightDidChange height: Int) {} 34 | func editorDidFocus(_ editor: SQTextEditorView) {} 35 | func editorDidTapDoneButton(_ editor: SQTextEditorView) {} 36 | func editor(_ editor: SQTextEditorView, cursorPositionDidChange position: SQEditorCursorPosition) {} 37 | } 38 | 39 | public class SQTextEditorView: UIView { 40 | 41 | public weak var delegate: SQTextEditorDelegate? 42 | 43 | public lazy var selectedTextAttribute = SQTextAttribute() 44 | 45 | public lazy var contentHeight: Int = 0 46 | 47 | private enum JSFunctionType { 48 | case getHTML 49 | case insertHTML(html: String) 50 | case setSelection( 51 | startElementId: String, 52 | startIndex: Int, 53 | endElementId: String, 54 | endIndex: Int) 55 | case getSelectedText 56 | case setFormat(type: RichTextFormatType) 57 | case removeFormat(type: RichTextFormatType) 58 | case setTextColor(hex: String) 59 | case setTextBackgroundColor(hex: String) 60 | case setTextSize(size: Int) 61 | case insertImage(url: String) 62 | case makeLink(url: String) 63 | case removeLink 64 | case clear 65 | case focusEditor(isFocused: Bool) 66 | case getEditorHeight 67 | 68 | var name: String { 69 | switch self { 70 | case .getHTML: 71 | return "getHTML()" 72 | 73 | case .insertHTML(let html): 74 | let safetyHTML = html 75 | .replacingOccurrences(of: "\n", with: "\\n") 76 | .replacingOccurrences(of: "\r", with: "\\r") 77 | return "insertHTML('\(safetyHTML)')" 78 | 79 | case .setSelection(let sId, let s, let eId, let e): 80 | return "setTextSelection('\(sId)','\(s)','\(eId)','\(e)')" 81 | 82 | case .getSelectedText: 83 | return "getSelectedText()" 84 | 85 | case .setFormat(let type): 86 | return "setFormat('\(type.keyName)')" 87 | 88 | case .removeFormat(let type): 89 | return "removeFormat('\(type.keyName)')" 90 | 91 | case .setTextColor(let hex): 92 | return "setTextColor('\(hex)')" 93 | 94 | case .setTextBackgroundColor(let hex): 95 | return "setTextBackgroundColor('\(hex)')" 96 | 97 | case .setTextSize(let size): 98 | return "setFontSize('\(size)')" 99 | 100 | case .insertImage(let url): 101 | return "insertImage('\(url)')" 102 | 103 | case .makeLink(let url): 104 | return "makeLink('\(url)')" 105 | 106 | case .removeLink: 107 | return "removeLink()" 108 | 109 | case .clear: 110 | return "clear()" 111 | 112 | case .focusEditor(let isFocused): 113 | return "focusEditor('\(isFocused)')" 114 | 115 | case .getEditorHeight: 116 | return "getEditorHeight()" 117 | } 118 | } 119 | } 120 | 121 | private enum JSMessageName: String, CaseIterable { 122 | case fontInfo = "fontInfo" 123 | case format = "format" 124 | case isFocused = "isFocused" 125 | case cursorPosition = "cursorPosition" 126 | } 127 | 128 | private enum RichTextFormatType { 129 | case bold 130 | case italic 131 | case strikethrough 132 | case underline 133 | 134 | var keyName: String { 135 | switch self { 136 | case .bold: 137 | return "bold" 138 | case .italic: 139 | return "italic" 140 | case .strikethrough: 141 | return "strikethrough" 142 | case .underline: 143 | return "underline" 144 | } 145 | } 146 | } 147 | 148 | public lazy var webView: WKWebView = { 149 | let config = WKWebViewConfiguration() 150 | config.preferences = WKPreferences() 151 | config.preferences.minimumFontSize = 10 152 | config.preferences.javaScriptEnabled = true 153 | config.preferences.javaScriptCanOpenWindowsAutomatically = false 154 | config.processPool = WKProcessPool() 155 | config.userContentController = WKUserContentController() 156 | 157 | JSMessageName.allCases.forEach { 158 | config.userContentController.add(self, name: $0.rawValue) 159 | } 160 | 161 | //inject css to html 162 | if customCss == nil, 163 | let cssURL = Bundle(for: SQTextEditorView.self).url(forResource: "editor", withExtension: "css"), 164 | let css = try? String(contentsOf: cssURL, encoding: .utf8) { 165 | customCss = css 166 | } 167 | 168 | if let css = customCss { 169 | let cssStyle = """ 170 | javascript:(function() { 171 | var parent = document.getElementsByTagName('head').item(0); 172 | var style = document.createElement('style'); 173 | style.type = 'text/css'; 174 | style.innerHTML = window.atob('\(encodeStringTo64(fromString: css))'); 175 | parent.appendChild(style)})() 176 | """ 177 | let cssScript = WKUserScript(source: cssStyle, injectionTime: .atDocumentEnd, forMainFrameOnly: false) 178 | config.userContentController.addUserScript(cssScript) 179 | } 180 | 181 | let _webView = WKWebView(frame: .zero, configuration: config) 182 | _webView.translatesAutoresizingMaskIntoConstraints = false 183 | _webView.navigationDelegate = self 184 | _webView.allowsLinkPreview = false 185 | _webView.setKeyboardRequiresUserInteraction(false) 186 | return _webView 187 | }() 188 | 189 | private lazy var editorEventQueue: OperationQueue = { 190 | let _editorEventQueue = OperationQueue() 191 | _editorEventQueue.maxConcurrentOperationCount = 1 192 | return _editorEventQueue 193 | }() 194 | 195 | private var lastContentHeight: Int = 0 { 196 | didSet { 197 | if contentHeight != lastContentHeight { 198 | contentHeight = lastContentHeight 199 | delegate?.editor(self, contentHeightDidChange: contentHeight) 200 | } 201 | } 202 | } 203 | 204 | private var timer: RepeatingTimer? 205 | 206 | private var customCss: String? 207 | 208 | public init(customCss: String? = nil) { 209 | self.customCss = customCss 210 | super.init(frame: .zero) 211 | setupUI() 212 | setupEditor() 213 | } 214 | 215 | required init?(coder: NSCoder) { 216 | fatalError("init(coder:) has not been implemented") 217 | } 218 | 219 | deinit { 220 | timer = nil 221 | 222 | JSMessageName.allCases.forEach { 223 | webView.configuration 224 | .userContentController 225 | .removeScriptMessageHandler(forName: $0.rawValue) 226 | } 227 | } 228 | 229 | //MARK: - Private Methods 230 | 231 | private func encodeStringTo64(fromString: String) -> String { 232 | let plainData = fromString.data(using: .utf8) 233 | return plainData?.base64EncodedString(options: []) ?? "" 234 | } 235 | 236 | private func setupUI() { 237 | self.addSubview(webView) 238 | 239 | webView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true 240 | webView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true 241 | webView.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true 242 | webView.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true 243 | } 244 | 245 | private func setupEditor() { 246 | if let path = Bundle(for: SQTextEditorView.self) 247 | .path(forResource: "index", ofType: "html") { 248 | let url = URL(fileURLWithPath: path) 249 | 250 | let request = URLRequest.init(url: url, 251 | cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, 252 | timeoutInterval: 5.0) 253 | webView.load(request) 254 | } 255 | } 256 | 257 | private func setFormat(_ type: RichTextFormatType, 258 | completion: ((_ error: Error?) -> ())?) { 259 | webView.evaluateJavaScript(JSFunctionType.setFormat(type: type).name, 260 | completionHandler: { (_, error) in 261 | completion?(error) 262 | }) 263 | } 264 | 265 | private func removeFormat(_ type: RichTextFormatType, 266 | completion: ((_ error: Error?) -> ())?) { 267 | webView.evaluateJavaScript(JSFunctionType.removeFormat(type: type).name, 268 | completionHandler: { (_, error) in 269 | completion?(error) 270 | }) 271 | } 272 | 273 | private func observerContentHeight() { 274 | timer = nil 275 | 276 | timer = RepeatingTimer(timeInterval: 0.2) 277 | 278 | timer?.eventHandler = { [weak self] in 279 | guard let self = `self` else { return } 280 | 281 | DispatchQueue.main.async { 282 | self.getEditorHeight() 283 | } 284 | } 285 | 286 | timer?.resume() 287 | } 288 | 289 | private func getEditorHeight() { 290 | webView.evaluateJavaScript(JSFunctionType.getEditorHeight.name, 291 | completionHandler: { [weak self] (height, error) in 292 | guard let self = `self` else { return } 293 | if let height = height as? Int, error == nil { 294 | self.lastContentHeight = height 295 | } 296 | }) 297 | } 298 | 299 | //MARK: - Public Methods 300 | 301 | /** 302 | Returns the HTML value of the editor in its current state. 303 | 304 | - Parameter html: HTML String. 305 | */ 306 | public func getHTML(completion: @escaping (_ html: String?) -> ()) { 307 | webView.evaluateJavaScript(JSFunctionType.getHTML.name, completionHandler: { (value, error) in 308 | completion(value as? String) 309 | }) 310 | } 311 | 312 | /** 313 | Inserts an HTML fragment at the current cursor location, or replaces the selection if selected. The value supplied should not contain tags or anything outside of that. 314 | 315 | - Parameter html: The html String to insert. 316 | - Parameter completion: The block to execute after the operation finishes. This takes an error of script evaluation as a parameter. You may specify nil for this parameter. 317 | */ 318 | public func insertHTML(_ html: String, 319 | completion: ((_ error: Error?) -> ())? = nil) { 320 | webView.evaluateJavaScript(JSFunctionType.insertHTML(html: html).name, completionHandler: { (_, error) in 321 | completion?(error) 322 | }) 323 | } 324 | 325 | /** 326 | Changes the current selection position. The selected range will be selected from the first node that type equal 'TEXT_NODE' under the input element id 327 | 328 | - Parameter startElementId: The element ID for range of start selection 329 | - Parameter startIndex: Sets the starting position of the element that the id you specified. 330 | - Parameter endElementId: The element ID for range of end selection 331 | - Parameter endIndex: Sets the ending position of the element that the id you specified. 332 | - Parameter completion: The block to execute after the operation finishes. This takes an error of script evaluation as a parameter. You may specify nil for this parameter. 333 | 334 | HTML: 335 | ``` 336 |
123
337 |
456
338 | ``` 339 | 340 | The selected text is `12` 341 | ``` 342 | setSelection(startElementId: a, startIndex: 0, endElementId: a, endIndex: 2) 343 | 344 | ``` 345 | 346 | The selected text is `34` 347 | ``` 348 | setSelection(startElementId: a, startIndex: 2, endElementId: b, endIndex: 1) 349 | 350 | ``` 351 | */ 352 | public func setTextSelection(startElementId: String, 353 | startIndex: Int, 354 | endElementId: String, 355 | endIndex: Int, 356 | completion: ((_ error: Error?) -> ())? = nil) { 357 | webView.evaluateJavaScript(JSFunctionType.setSelection(startElementId: startElementId, 358 | startIndex: startIndex, 359 | endElementId: endElementId, 360 | endIndex: endIndex).name, 361 | completionHandler: { (_, error) in 362 | completion?(error) 363 | }) 364 | 365 | } 366 | 367 | /** 368 | Returns the text currently selected in the editor. 369 | 370 | - Parameter text: Selected text. 371 | */ 372 | public func getSelectedText(completion: @escaping (_ text: String?) -> ()) { 373 | webView.evaluateJavaScript(JSFunctionType.getSelectedText.name, 374 | completionHandler: { (value, error) in 375 | completion(value as? String) 376 | }) 377 | } 378 | 379 | /** 380 | Makes any non-bold currently selected text bold (by wrapping it in a 'b' tag), 381 | otherwise removes any bold formatting from the selected text. 382 | */ 383 | public func bold(completion: ((_ error: Error?) -> ())? = nil) { 384 | selectedTextAttribute.format.hasBold ? 385 | removeFormat(.bold, completion: completion) : 386 | setFormat(.bold, completion: completion) 387 | } 388 | 389 | /** 390 | Makes any non-italic currently selected text italic (by wrapping it in an 'i' tag), 391 | otherwise removes any italic formatting from the selected text. 392 | */ 393 | public func italic(completion: ((_ error: Error?) -> ())? = nil) { 394 | selectedTextAttribute.format.hasItalic ? 395 | removeFormat(.italic, completion: completion) : 396 | setFormat(.italic, completion: completion) 397 | } 398 | 399 | /** 400 | Makes any non-underlined currently selected text underlined (by wrapping it in a 'u' tag), 401 | otherwise removes any underline formatting from the selected text. 402 | */ 403 | public func underline(completion: ((_ error: Error?) -> ())? = nil) { 404 | selectedTextAttribute.format.hasUnderline ? 405 | removeFormat(.underline, completion: completion) : 406 | setFormat(.underline, completion: completion) 407 | } 408 | 409 | /** 410 | Makes any non-strikethrough currently selected text underlined (by wrapping it in a 'del' tag), 411 | otherwise removes any strikethrough formatting from the selected text. 412 | */ 413 | public func strikethrough(completion: ((_ error: Error?) -> ())? = nil) { 414 | selectedTextAttribute.format.hasStrikethrough ? 415 | removeFormat(.strikethrough, completion: completion) : 416 | setFormat(.strikethrough, completion: completion) 417 | } 418 | 419 | /** 420 | Sets the colour of the selected text. 421 | 422 | - Parameter color: The colour to set. 423 | */ 424 | public func setText(color: UIColor, completion: ((_ error: Error?) -> ())? = nil) { 425 | let hex = Helper.rgbColorToHex(color: color) 426 | 427 | webView.evaluateJavaScript(JSFunctionType.setTextColor(hex: hex).name, 428 | completionHandler: { (_, error) in 429 | completion?(error) 430 | }) 431 | } 432 | 433 | /** 434 | Sets the colour of the background of the selected text. 435 | 436 | - Parameter color: The colour to set. 437 | */ 438 | public func setText(backgroundColor: UIColor, completion: ((_ error: Error?) -> ())? = nil) { 439 | let hex = Helper.rgbColorToHex(color: backgroundColor) 440 | 441 | webView.evaluateJavaScript(JSFunctionType.setTextBackgroundColor(hex: hex).name, 442 | completionHandler: { (_, error) in 443 | completion?(error) 444 | }) 445 | } 446 | 447 | /** 448 | Sets the font size for the selected text. 449 | 450 | - Parameter size: A size to set. The absolute length units will be 'px' 451 | */ 452 | public func setText(size: Int, completion: ((_ error: Error?) -> ())? = nil) { 453 | webView.evaluateJavaScript(JSFunctionType.setTextSize(size: size).name, 454 | completionHandler: { (_, error) in 455 | completion?(error) 456 | }) 457 | } 458 | 459 | /** 460 | Inserts an image at the current cursor location. 461 | 462 | - Parameter url: The source path for the image. 463 | */ 464 | public func insertImage(url: String, completion: ((_ error: Error?) -> ())? = nil) { 465 | webView.evaluateJavaScript(JSFunctionType.insertImage(url: url).name, 466 | completionHandler: { (_, error) in 467 | completion?(error) 468 | }) 469 | } 470 | 471 | /** 472 | Makes the currently selected text a link. If no text is selected, the URL or email will be inserted as text at the current cursor point and made into a link. 473 | 474 | - Parameter url: The url or email to link to. 475 | */ 476 | public func makeLink(url: String, completion: ((_ error: Error?) -> ())? = nil) { 477 | webView.evaluateJavaScript(JSFunctionType.makeLink(url: url).name, 478 | completionHandler: { (_, error) in 479 | completion?(error) 480 | }) 481 | } 482 | 483 | /** 484 | Removes any link that is currently at least partially selected. 485 | */ 486 | public func removeLink(completion: ((_ error: Error?) -> ())? = nil) { 487 | webView.evaluateJavaScript(JSFunctionType.removeLink.name, 488 | completionHandler: { (_, error) in 489 | completion?(error) 490 | }) 491 | } 492 | 493 | /** 494 | Clear Editor's content. Method removes all Blocks and inserts new initial empty Block 495 | `

` 496 | */ 497 | public func clear(completion: ((_ error: Error?) -> ())? = nil) { 498 | webView.evaluateJavaScript(JSFunctionType.clear.name, 499 | completionHandler: { (_, error) in 500 | completion?(error) 501 | }) 502 | } 503 | 504 | /** 505 | The editor gained focus or lost focus 506 | */ 507 | public func focus(_ isFocused: Bool, completion: ((_ error: Error?) -> ())? = nil) { 508 | webView.evaluateJavaScript(JSFunctionType.focusEditor(isFocused: isFocused).name, 509 | completionHandler: { [weak self] (_, error) in 510 | if !isFocused { 511 | self?.webView.endEditing(true) 512 | } 513 | completion?(error) 514 | }) 515 | } 516 | } 517 | 518 | extension SQTextEditorView: WKNavigationDelegate { 519 | 520 | public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { 521 | observerContentHeight() 522 | delegate?.editorDidLoad(self) 523 | } 524 | } 525 | 526 | extension SQTextEditorView: WKScriptMessageHandler { 527 | 528 | public func userContentController(_ userContentController: WKUserContentController, 529 | didReceive message: WKScriptMessage) { 530 | if let name = JSMessageName(rawValue: message.name) { 531 | 532 | let body: Any = message.body 533 | 534 | editorEventQueue.addOperation { [weak self] in 535 | guard let self = `self` else { return } 536 | 537 | switch name { 538 | case .format: 539 | if let dict = body as? [String: Bool], 540 | let data = try? JSONSerialization.data(withJSONObject: dict, options: []), 541 | let format = try? JSONDecoder().decode(SQTextAttributeFormat.self, from: data) { 542 | DispatchQueue.main.async { 543 | self.selectedTextAttribute.format = format 544 | self.delegate?.editor(self, selectedTextAttributeDidChange: self.selectedTextAttribute) 545 | } 546 | } 547 | 548 | case .fontInfo: 549 | if let dict = body as? [String: Any], 550 | let data = try? JSONSerialization.data(withJSONObject: dict, options: []), 551 | let fontInfo = try? JSONDecoder().decode(SQTextAttributeTextInfo.self, from: data) { 552 | DispatchQueue.main.async { 553 | self.selectedTextAttribute.textInfo = fontInfo 554 | self.delegate?.editor(self, selectedTextAttributeDidChange: self.selectedTextAttribute) 555 | } 556 | } 557 | 558 | case .isFocused: 559 | DispatchQueue.main.async { 560 | if let value = body as? Bool { 561 | value ? self.delegate?.editorDidFocus(self) : self.delegate?.editorDidTapDoneButton(self) 562 | } 563 | } 564 | 565 | case .cursorPosition: 566 | if let dict = body as? [String: Any], 567 | let data = try? JSONSerialization.data(withJSONObject: dict, options: []), 568 | let position = try? JSONDecoder().decode(SQEditorCursorPosition.self, from: data) { 569 | DispatchQueue.main.async { 570 | self.delegate?.editor(self, cursorPositionDidChange: position) 571 | } 572 | } 573 | } 574 | } 575 | } 576 | } 577 | } 578 | 579 | -------------------------------------------------------------------------------- /_Pods.xcodeproj: -------------------------------------------------------------------------------- 1 | Example/Pods/Pods.xcodeproj -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OneupNetwork/SQRichTextEditor/9f859c6f07792a6966da7ff0f3b3523dbd2864ef/logo.png --------------------------------------------------------------------------------