├── .gitignore ├── LICENSE.md ├── README.md ├── RichEditorView.podspec ├── RichEditorView.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── RichEditorView.xcscheme ├── RichEditorView ├── Assets │ ├── editor │ │ ├── assert.js │ │ ├── normalize.css │ │ ├── rich_editor.html │ │ ├── rich_editor.js │ │ ├── rich_editor_tests.html │ │ ├── rich_editor_tests.js │ │ └── style.css │ └── icons │ │ ├── bg_color@2x.png │ │ ├── bg_color@3x.png │ │ ├── bold@2x.png │ │ ├── bold@3x.png │ │ ├── clear@2x.png │ │ ├── h1@2x.png │ │ ├── h2@2x.png │ │ ├── h3@2x.png │ │ ├── h4@2x.png │ │ ├── h5@2x.png │ │ ├── h6@2x.png │ │ ├── indent@2x.png │ │ ├── indent@3x.png │ │ ├── insert_image@2x.png │ │ ├── insert_link@2x.png │ │ ├── italic@2x.png │ │ ├── italic@3x.png │ │ ├── justify_center@2x.png │ │ ├── justify_left@2x.png │ │ ├── justify_right@2x.png │ │ ├── ordered_list@2x.png │ │ ├── ordered_list@3x.png │ │ ├── outdent@2x.png │ │ ├── outdent@3x.png │ │ ├── redo@2x.png │ │ ├── redo@3x.png │ │ ├── strikethrough@2x.png │ │ ├── subscript@2x.png │ │ ├── superscript@2x.png │ │ ├── text_color@2x.png │ │ ├── text_color@3x.png │ │ ├── underline@2x.png │ │ ├── underline@3x.png │ │ ├── undo@2x.png │ │ ├── undo@3x.png │ │ ├── unordered_list@2x.png │ │ └── unordered_list@3x.png ├── Classes │ ├── RichEditorOptionItem.swift │ ├── RichEditorToolbar.swift │ ├── RichEditorView.swift │ ├── RichEditorWebView.swift │ ├── String+Extensions.swift │ └── UIColor+Extensions.swift ├── Info.plist ├── RichEditorView-Bridging-Header.h └── RichEditorView.h ├── RichEditorViewSample ├── Podfile ├── Podfile.lock ├── Pods │ ├── Local Podspecs │ │ └── RichEditorView.podspec.json │ ├── Manifest.lock │ ├── Pods.xcodeproj │ │ └── project.pbxproj │ └── Target Support Files │ │ └── RichEditorView │ │ ├── Info.plist │ │ ├── RichEditorView-dummy.m │ │ ├── RichEditorView-prefix.pch │ │ ├── RichEditorView-umbrella.h │ │ ├── RichEditorView.modulemap │ │ └── RichEditorView.xcconfig ├── RichEditorViewSample.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── RichEditorViewSample.xcworkspace │ └── contents.xcworkspacedata ├── RichEditorViewSample │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── LaunchScreen.xib │ │ └── Main.storyboard │ ├── Images.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Info.plist │ ├── KeyboardManager.swift │ ├── RichEditorViewSample-Bridging-Header.h │ └── ViewController.swift └── RichEditorViewSampleTests │ ├── Info.plist │ └── RichEditorViewSampleTests.swift ├── RichEditorViewTests ├── Info.plist └── RichEditorViewTests.swift └── art ├── Demo.gif └── Toolbar.gif /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | *.xccheckout 14 | *.moved-aside 15 | DerivedData 16 | *.hmap 17 | *.ipa 18 | *.xcuserstate 19 | 20 | .DS_Store 21 | .AppleDouble 22 | .LSOverride 23 | 24 | # Icon must end with two \r 25 | Icon 26 | 27 | 28 | # Thumbnails 29 | ._* 30 | 31 | # Files that might appear in the root of a volume 32 | .DocumentRevisions-V100 33 | .fseventsd 34 | .Spotlight-V100 35 | .TemporaryItems 36 | .Trashes 37 | .VolumeIcon.icns 38 | 39 | # Directories potentially created on remote AFP share 40 | .AppleDB 41 | .AppleDesktop 42 | Network Trash Folder 43 | Temporary Items 44 | .apdisk 45 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, C. Bess - Soli Deo gloria - perfectGod.com 2 | Copyright (c) 2015, Caesar Wirth 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | 11 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | RichEditorView 2 | -------------- 3 | [](./LICENSE.md) 4 | [](http://cocoapods.org/pods/RichEditorView) 5 | [](https://github.com/Carthage/Carthage) 6 | 7 | RichEditorView is a simple, modular, drop-in UIView subclass for Rich Text Editing (`WKWebView` wrapper). 8 | 9 | Written in Swift 5.0 (Xcode 11.x) 10 | 11 | Supports iOS 10 through CocoaPods or Carthage. 12 | 13 | [Soli Deo gloria](https://perfectGod.com) 14 | 15 | Seen in Action 16 | -------------- 17 |  18 | 19 | Just clone the project and open `RichEditorViewSample/RichEditorViewSample.xcworkspace` in Xcode. 20 | 21 | Features 22 | -------- 23 | 24 |  25 | 26 | - [x] Bold 27 | - [x] Italic 28 | - [x] Subscript 29 | - [x] Superscript 30 | - [x] Strikethrough 31 | - [x] Underline 32 | - [x] Justify Left 33 | - [x] Justify Center 34 | - [x] Justify Right 35 | - [x] Heading 1 36 | - [x] Heading 2 37 | - [x] Heading 3 38 | - [x] Heading 4 39 | - [x] Heading 5 40 | - [x] Heading 6 41 | - [x] Undo 42 | - [x] Redo 43 | - [x] Ordered List 44 | - [x] Unordered List 45 | - [x] Indent 46 | - [x] Outdent 47 | - [x] Insert Image 48 | - [x] Insert Link 49 | - [x] Text Color 50 | - [x] Text Background Color 51 | 52 | Installation 53 | ------------ 54 | 55 | #### Cocoapods 56 | 57 | If you have Cocoapods 1.0+ installed, you can use Cocoapods to include `RichEditorView` into your project. 58 | Add the following to your `Podfile`: 59 | 60 | ``` 61 | pod 'RichEditorView', :git => 'https://github.com/cbess/RichEditorView.git', :tag => '4.0' 62 | ``` 63 | 64 | Note: the `use_frameworks!` is required for pods made in Swift. 65 | 66 | #### Carthage 67 | 68 | Add the following to your `Cartfile`: 69 | 70 | ``` 71 | github 'cbess/RichEditorView' 72 | ``` 73 | 74 | Using RichEditorView 75 | -------------------- 76 | 77 | `RichEditorView` makes no assumptions about how you want to use it in your app. It is a plain `UIView` subclass, so you are free to use it wherever, however you want. 78 | 79 | Most basic use: 80 | 81 | ``` 82 | editor = RichEditorView(frame: view.bounds) 83 | editor.html = "
'); 265 | }; 266 | 267 | RE.insertHTML = function(html) { 268 | RE.restorerange(); 269 | document.execCommand('insertHTML', false, html); 270 | }; 271 | 272 | RE.insertLink = function(url, title) { 273 | RE.restorerange(); 274 | const sel = document.getSelection(); 275 | if (sel.toString().length !== 0) { 276 | if (sel.rangeCount) { 277 | let el = document.createElement('a'); 278 | el.setAttribute('href', url); 279 | el.setAttribute('title', title); 280 | 281 | let range = sel.getRangeAt(0).cloneRange(); 282 | range.surroundContents(el); 283 | sel.removeAllRanges(); 284 | sel.addRange(range); 285 | } 286 | } 287 | 288 | RE.sendInputCallback(); 289 | }; 290 | 291 | RE.prepareInsert = function() { 292 | RE.backuprange(); 293 | }; 294 | 295 | RE.backuprange = function() { 296 | const selection = window.getSelection(); 297 | if (selection.rangeCount === 0) { 298 | return; 299 | } 300 | 301 | let node = selection.anchorNode; 302 | if (node.nodeType === 3) { 303 | // use the parent, if text node 304 | node = node.parentNode; 305 | } 306 | 307 | const range = selection.getRangeAt(0); 308 | RE.currentSelection = { 309 | startContainer: range.startContainer, 310 | startOffset: range.startOffset, 311 | endContainer: range.endContainer, 312 | endOffset: range.endOffset, 313 | node, 314 | }; 315 | }; 316 | 317 | RE.addRangeToSelection = function(selection, range) { 318 | if (selection) { 319 | selection.removeAllRanges(); 320 | selection.addRange(range); 321 | } 322 | }; 323 | 324 | // Programatically select a DOM element 325 | RE.selectElementContents = function(el) { 326 | let range = document.createRange(); 327 | range.selectNodeContents(el); 328 | let sel = window.getSelection(); 329 | // this.createSelectionFromRange sel, range 330 | RE.addRangeToSelection(sel, range); 331 | }; 332 | 333 | RE.restorerange = function() { 334 | let selection = window.getSelection(); 335 | selection.removeAllRanges(); 336 | let range = document.createRange(); 337 | range.setStart(RE.currentSelection.startContainer, RE.currentSelection.startOffset); 338 | range.setEnd(RE.currentSelection.endContainer, RE.currentSelection.endOffset); 339 | selection.addRange(range); 340 | }; 341 | 342 | RE.focus = function() { 343 | let range = document.createRange(); 344 | range.selectNodeContents(RE.editor); 345 | range.collapse(false); 346 | let selection = window.getSelection(); 347 | selection.removeAllRanges(); 348 | selection.addRange(range); 349 | RE.editor.focus(); 350 | }; 351 | 352 | RE.focusAtPoint = function(x, y) { 353 | const range = document.caretRangeFromPoint(x, y) || document.createRange(); 354 | const selection = window.getSelection(); 355 | selection.removeAllRanges(); 356 | selection.addRange(range); 357 | RE.editor.focus(); 358 | }; 359 | 360 | RE.blurFocus = function() { 361 | RE.editor.blur(); 362 | }; 363 | 364 | /** 365 | Recursively search element ancestors to find a element nodeName e.g. A 366 | **/ 367 | const _findNodeByNameInContainer = function(element, nodeName, rootElementId) { 368 | if (element.nodeName == nodeName) { 369 | return element; 370 | } else { 371 | if (element.id === rootElementId) { 372 | return null; 373 | } 374 | _findNodeByNameInContainer(element.parentElement, nodeName, rootElementId); 375 | } 376 | }; 377 | 378 | const isAnchorNode = function(node) { 379 | return ('A' == node.nodeName); 380 | }; 381 | 382 | RE.getAnchorTagsInNode = function(node) { 383 | let links = []; 384 | 385 | while (node.nextSibling !== null && node.nextSibling !== undefined) { 386 | node = node.nextSibling; 387 | if (isAnchorNode(node)) { 388 | links.push(node.getAttribute('href')); 389 | } 390 | } 391 | return links; 392 | }; 393 | 394 | RE.countAnchorTagsInNode = function(node) { 395 | return RE.getAnchorTagsInNode(node).length; 396 | }; 397 | 398 | /** 399 | * If the current selection's parent is an anchor tag, get the href. 400 | * @returns {string} 401 | */ 402 | RE.getSelectedHref = function() { 403 | let href = ''; 404 | let sel = window.getSelection(); 405 | if (!RE.rangeOrCaretSelectionExists()) { 406 | return null; 407 | } 408 | 409 | let tags = RE.getAnchorTagsInNode(sel.anchorNode); 410 | //if more than one link is there, return null 411 | if (tags.length > 1) { 412 | return null; 413 | } else if (tags.length == 1) { 414 | href = tags[0]; 415 | } else { 416 | let node = _findNodeByNameInContainer(sel.anchorNode.parentElement, 'A', 'editor'); 417 | href = node.href; 418 | } 419 | 420 | return (href ? href : null); 421 | }; 422 | 423 | // Returns the cursor position relative to its current position onscreen. 424 | // Can be negative if it is above what is visible 425 | RE.getRelativeCaretYPosition = function() { 426 | let y = 0; 427 | let sel = window.getSelection(); 428 | if (sel.rangeCount) { 429 | const range = sel.getRangeAt(0); 430 | const needsWorkAround = (range.startOffset == 0); 431 | /* Removing fixes bug when node name other than 'div' */ 432 | // && range.startContainer.nodeName.toLowerCase() == 'div'); 433 | if (needsWorkAround) { 434 | y = range.startContainer.offsetTop - window.pageYOffset; 435 | } else { 436 | if (range.getClientRects) { 437 | let rects = range.getClientRects(); 438 | if (rects.length > 0) { 439 | y = rects[0].top; 440 | } 441 | } 442 | } 443 | } 444 | 445 | return y; 446 | }; 447 | 448 | window.onload = function() { 449 | RE.callback('ready'); 450 | }; 451 | -------------------------------------------------------------------------------- /RichEditorView/Assets/editor/rich_editor_tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |10 |11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /RichEditorView/Assets/editor/rich_editor_tests.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var RichEditorTests = function() { 4 | var self = {}; 5 | var tests = []; 6 | var link = "http://foo.bar/"; 7 | var anchor = "Foo"; 8 | var htmlWithLink = "What are these so withered and wild in their attire? " + anchor + "
that look not like the inhabitants of the Earth and yet are on't?"; 9 | var htmlWith2Links = "Blah? " + anchor + " " + anchor + " Blah
"; 10 | 11 | var tearDown = function() { 12 | RE.setHtml(''); 13 | }; 14 | 15 | /** 16 | This is the main and only public "method" 17 | **/ 18 | self.runTests = function() { 19 | var content = ""; 20 | for (var testName in tests) { 21 | tests[testName](); 22 | var log = 'Passed : ' + testName; 23 | console.log(log); 24 | content += log + "
"; 25 | tearDown(); 26 | } 27 | RE.setHtml(content); 28 | }; 29 | 30 | tests['testGetSet'] = function() { 31 | var testContent = "Test"; 32 | RE.setHtml(testContent); 33 | Assert.equals(RE.getHtml(), testContent, 'testGetSet'); 34 | }; 35 | 36 | tests['testGetSelectedHrefReturnsLinkOnFullSelection'] = function() { 37 | let htmlWithLink = "Foo"; 38 | RE.setHtml(htmlWithLink); 39 | //select the anchor tag directly and fully 40 | RE.selectElementContents(document.querySelector('#link_id')); 41 | Assert.equals(RE.getSelectedHref(), link); 42 | }; 43 | 44 | tests['testGetSelectedHrefWithSelectionContainingOneLink'] = function() { 45 | RE.setHtml(htmlWithLink); 46 | //select the anchor tag directly and fully 47 | RE.selectElementContents(document.querySelector('#prose')); 48 | Assert.equals(RE.getSelectedHref(), link); 49 | }; 50 | 51 | tests['testCountAnchorTagsInSelection'] = function() { 52 | RE.setHtml(htmlWithLink); 53 | //select the anchor tag directly and fully 54 | RE.selectElementContents(document.querySelector('#prose')); 55 | let count = RE.countAnchorTagsInNode(getSelection().anchorNode); 56 | Assert.equals(count, 1); 57 | }; 58 | 59 | tests['testgetSelectedHrefWith2LinksReturnsNull'] = function() { 60 | RE.setHtml(htmlWith2Links); 61 | 62 | //select the anchor tag directly and fully 63 | RE.selectElementContents(document.querySelector('#two_links')); 64 | let count = RE.countAnchorTagsInNode(getSelection().anchorNode); 65 | Assert.equals(count, 2); 66 | // Assert.equals(RE.getSelectedHref(), null) 67 | }; 68 | 69 | return self; 70 | }(); 71 | 72 | RichEditorTests.runTests(); -------------------------------------------------------------------------------- /RichEditorView/Assets/editor/style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015 Wasabeef 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @charset "UTF-8"; 18 | 19 | :root { 20 | color-scheme: light dark; 21 | } 22 | 23 | body, html { 24 | height: 100%; 25 | } 26 | 27 | body { 28 | overflow: auto; 29 | margin: 0; 30 | font: -apple-system-body; 31 | } 32 | 33 | @media (prefers-color-scheme: dark) { 34 | body, #editor { 35 | } 36 | } 37 | 38 | #container { 39 | display: table; 40 | width: 100%; 41 | height: 100%; 42 | } 43 | 44 | #editor { 45 | -webkit-user-select: auto !important; 46 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 47 | overflow: auto; 48 | display: table-cell; 49 | height: 100%; 50 | } 51 | 52 | #editor:focus { 53 | outline: 0px solid transparent; 54 | } 55 | 56 | .placeholder[placeholder]:after { 57 | content: attr(placeholder); 58 | position: absolute; 59 | top: 0px; 60 | color: #ccc; 61 | } 62 | 63 | h1, p, ul, ol { 64 | margin: 0; 65 | } 66 | 67 | a { 68 | word-break: break-all; 69 | color: -apple-system-blue; 70 | } 71 | -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/bg_color@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/bg_color@2x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/bg_color@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/bg_color@3x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/bold@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/bold@2x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/bold@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/bold@3x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/clear@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/clear@2x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/h1@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/h1@2x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/h2@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/h2@2x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/h3@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/h3@2x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/h4@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/h4@2x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/h5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/h5@2x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/h6@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/h6@2x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/indent@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/indent@2x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/indent@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/indent@3x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/insert_image@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/insert_image@2x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/insert_link@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/insert_link@2x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/italic@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/italic@2x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/italic@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/italic@3x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/justify_center@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/justify_center@2x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/justify_left@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/justify_left@2x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/justify_right@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/justify_right@2x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/ordered_list@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/ordered_list@2x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/ordered_list@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/ordered_list@3x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/outdent@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/outdent@2x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/outdent@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/outdent@3x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/redo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/redo@2x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/redo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/redo@3x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/strikethrough@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/strikethrough@2x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/subscript@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/subscript@2x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/superscript@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/superscript@2x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/text_color@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/text_color@2x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/text_color@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/text_color@3x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/underline@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/underline@2x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/underline@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/underline@3x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/undo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/undo@2x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/undo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/undo@3x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/unordered_list@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/unordered_list@2x.png -------------------------------------------------------------------------------- /RichEditorView/Assets/icons/unordered_list@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/RichEditorView/Assets/icons/unordered_list@3x.png -------------------------------------------------------------------------------- /RichEditorView/Classes/RichEditorOptionItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RichEditorOptionItem.swift 3 | // 4 | // Created by Caesar Wirth on 4/2/15. 5 | // Copyright (c) 2015 Caesar Wirth. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | /// A RichEditorOption object is an object that can be displayed in a RichEditorToolbar. 11 | /// This protocol is proviced to allow for custom actions not provided in the RichEditorOptions enum. 12 | public protocol RichEditorOption { 13 | 14 | /// The image to be displayed in the RichEditorToolbar. 15 | var image: UIImage? { get } 16 | 17 | /// The title of the item. 18 | /// If `image` is nil, this will be used for display in the RichEditorToolbar. 19 | var title: String { get } 20 | 21 | /// The action to be evoked when the action is tapped 22 | /// - parameter editor: The RichEditorToolbar that the RichEditorOption was being displayed in when tapped. 23 | /// Contains a reference to the `editor` RichEditorView to perform actions on. 24 | /// - parameter sender: The object that sent the action. Usually a `UIView` from the toolbar item that represents the option. 25 | func action(_ editor: RichEditorToolbar, sender: AnyObject) 26 | } 27 | 28 | /// RichEditorOptionItem is a concrete implementation of RichEditorOption. 29 | /// It can be used as a configuration object for custom objects to be shown on a RichEditorToolbar. 30 | public struct RichEditorOptionItem: RichEditorOption { 31 | 32 | /// The image that should be shown when displayed in the RichEditorToolbar. 33 | public var image: UIImage? 34 | 35 | /// If an `itemImage` is not specified, this is used in display 36 | public var title: String 37 | 38 | /// The action to be performed when tapped 39 | public var handler: ((RichEditorToolbar, AnyObject) -> Void) 40 | 41 | public init(image: UIImage? = nil, title: String, action: @escaping ((RichEditorToolbar, AnyObject) -> Void)) { 42 | self.image = image 43 | self.title = title 44 | self.handler = action 45 | } 46 | 47 | public init(title: String, action: @escaping ((RichEditorToolbar, AnyObject) -> Void)) { 48 | self.init(image: nil, title: title, action: action) 49 | } 50 | 51 | public func action(_ toolbar: RichEditorToolbar, sender: AnyObject) { 52 | handler(toolbar, sender) 53 | } 54 | } 55 | 56 | /// RichEditorOptions is an enum of standard editor actions 57 | public enum RichEditorDefaultOption: RichEditorOption { 58 | 59 | case clear 60 | case undo 61 | case redo 62 | case bold 63 | case italic 64 | case `subscript` 65 | case superscript 66 | case strike 67 | case underline 68 | case textColor 69 | case textBackgroundColor 70 | case header(Int) 71 | case indent 72 | case outdent 73 | case orderedList 74 | case unorderedList 75 | case alignLeft 76 | case alignCenter 77 | case alignRight 78 | case image 79 | case link 80 | 81 | public static let all: [RichEditorDefaultOption] = [ 82 | //.clear, 83 | .undo, .redo, .bold, .italic, 84 | .subscript, .superscript, .strike, .underline, 85 | .textColor, .textBackgroundColor, 86 | .header(1), .header(2), .header(3), .header(4), .header(5), .header(6), 87 | .indent, outdent, orderedList, unorderedList, 88 | .alignLeft, .alignCenter, .alignRight, .image, .link 89 | ] 90 | 91 | // MARK: RichEditorOption 92 | 93 | public var image: UIImage? { 94 | var name = "" 95 | switch self { 96 | case .clear: name = "clear" 97 | case .undo: name = "undo" 98 | case .redo: name = "redo" 99 | case .bold: name = "bold" 100 | case .italic: name = "italic" 101 | case .subscript: name = "subscript" 102 | case .superscript: name = "superscript" 103 | case .strike: name = "strikethrough" 104 | case .underline: name = "underline" 105 | case .textColor: name = "text_color" 106 | case .textBackgroundColor: name = "bg_color" 107 | case .header(let h): name = "h\(h)" 108 | case .indent: name = "indent" 109 | case .outdent: name = "outdent" 110 | case .orderedList: name = "ordered_list" 111 | case .unorderedList: name = "unordered_list" 112 | case .alignLeft: name = "justify_left" 113 | case .alignCenter: name = "justify_center" 114 | case .alignRight: name = "justify_right" 115 | case .image: name = "insert_image" 116 | case .link: name = "insert_link" 117 | } 118 | 119 | let bundle = Bundle(for: RichEditorToolbar.self) 120 | return UIImage(named: name, in: bundle, compatibleWith: nil)?.withRenderingMode(.alwaysTemplate) 121 | } 122 | 123 | public var title: String { 124 | switch self { 125 | case .clear: return NSLocalizedString("Clear", comment: "") 126 | case .undo: return NSLocalizedString("Undo", comment: "") 127 | case .redo: return NSLocalizedString("Redo", comment: "") 128 | case .bold: return NSLocalizedString("Bold", comment: "") 129 | case .italic: return NSLocalizedString("Italic", comment: "") 130 | case .subscript: return NSLocalizedString("Sub", comment: "") 131 | case .superscript: return NSLocalizedString("Super", comment: "") 132 | case .strike: return NSLocalizedString("Strike", comment: "") 133 | case .underline: return NSLocalizedString("Underline", comment: "") 134 | case .textColor: return NSLocalizedString("Color", comment: "") 135 | case .textBackgroundColor: return NSLocalizedString("BG Color", comment: "") 136 | case .header(let h): return NSLocalizedString("H\(h)", comment: "") 137 | case .indent: return NSLocalizedString("Indent", comment: "") 138 | case .outdent: return NSLocalizedString("Outdent", comment: "") 139 | case .orderedList: return NSLocalizedString("Ordered List", comment: "") 140 | case .unorderedList: return NSLocalizedString("Unordered List", comment: "") 141 | case .alignLeft: return NSLocalizedString("Left", comment: "") 142 | case .alignCenter: return NSLocalizedString("Center", comment: "") 143 | case .alignRight: return NSLocalizedString("Right", comment: "") 144 | case .image: return NSLocalizedString("Image", comment: "") 145 | case .link: return NSLocalizedString("Link", comment: "") 146 | } 147 | } 148 | 149 | public func action(_ toolbar: RichEditorToolbar, sender: AnyObject) { 150 | switch self { 151 | case .clear: toolbar.editor?.removeFormat() 152 | case .undo: toolbar.editor?.undo() 153 | case .redo: toolbar.editor?.redo() 154 | case .bold: toolbar.editor?.bold() 155 | case .italic: toolbar.editor?.italic() 156 | case .subscript: toolbar.editor?.subscriptText() 157 | case .superscript: toolbar.editor?.superscript() 158 | case .strike: toolbar.editor?.strikethrough() 159 | case .underline: toolbar.editor?.underline() 160 | case .textColor: toolbar.delegate?.richEditorToolbarChangeTextColor?(toolbar, sender: sender) 161 | case .textBackgroundColor: toolbar.delegate?.richEditorToolbarChangeBackgroundColor?(toolbar, sender: sender) 162 | case .header(let h): toolbar.editor?.header(h) 163 | case .indent: toolbar.editor?.indent() 164 | case .outdent: toolbar.editor?.outdent() 165 | case .orderedList: toolbar.editor?.orderedList() 166 | case .unorderedList: toolbar.editor?.unorderedList() 167 | case .alignLeft: toolbar.editor?.alignLeft() 168 | case .alignCenter: toolbar.editor?.alignCenter() 169 | case .alignRight: toolbar.editor?.alignRight() 170 | case .image: toolbar.delegate?.richEditorToolbarInsertImage?(toolbar) 171 | case .link: toolbar.delegate?.richEditorToolbarInsertLink?(toolbar) 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /RichEditorView/Classes/RichEditorToolbar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RichEditorToolbar.swift 3 | // 4 | // Created by Caesar Wirth on 4/2/15. 5 | // Updated/Modernized by C. Bess on 9/18/19. 6 | // 7 | // Copyright (c) 2015 Caesar Wirth. All rights reserved. 8 | // 9 | 10 | import UIKit 11 | 12 | /// RichEditorToolbarDelegate is a protocol for the RichEditorToolbar. 13 | /// Used to receive actions that need extra work to perform (eg. display some UI) 14 | @objc public protocol RichEditorToolbarDelegate: AnyObject { 15 | 16 | /// Called when the Text Color toolbar item is pressed. 17 | @objc optional func richEditorToolbarChangeTextColor(_ toolbar: RichEditorToolbar, sender: AnyObject) 18 | 19 | /// Called when the Background Color toolbar item is pressed. 20 | @objc optional func richEditorToolbarChangeBackgroundColor(_ toolbar: RichEditorToolbar, sender: AnyObject) 21 | 22 | /// Called when the Insert Image toolbar item is pressed. 23 | @objc optional func richEditorToolbarInsertImage(_ toolbar: RichEditorToolbar) 24 | 25 | /// Called when the Insert Link toolbar item is pressed. 26 | @objc optional func richEditorToolbarInsertLink(_ toolbar: RichEditorToolbar) 27 | } 28 | 29 | fileprivate func pinViewEdges(of childView: UIView, to parentView: UIView) { 30 | NSLayoutConstraint.activate([ 31 | childView.leadingAnchor.constraint(equalTo: parentView.leadingAnchor), 32 | childView.trailingAnchor.constraint(equalTo: parentView.trailingAnchor), 33 | childView.topAnchor.constraint(equalTo: parentView.topAnchor), 34 | childView.bottomAnchor.constraint(equalTo: parentView.bottomAnchor) 35 | ]) 36 | } 37 | 38 | private let DefaultFont = UIFont.preferredFont(forTextStyle: .body) 39 | 40 | /// RichEditorToolbar is UIView that contains the toolbar for actions that can be performed on a RichEditorView 41 | @objcMembers open class RichEditorToolbar: UIView, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { 42 | 43 | /// The delegate to receive events that cannot be automatically completed 44 | open weak var delegate: RichEditorToolbarDelegate? 45 | 46 | /// A reference to the RichEditorView that it should be performing actions on 47 | open weak var editor: RichEditorView? 48 | 49 | /// The list of options to be displayed on the toolbar 50 | open var options: [RichEditorOption] = [] { 51 | didSet { 52 | updateToolbar() 53 | } 54 | } 55 | 56 | /// The tint color to apply to the toolbar background. 57 | open var barTintColor: UIColor? { 58 | get { return backgroundColor } 59 | set { backgroundColor = newValue } 60 | } 61 | 62 | /// The spacing between the option items 63 | open var itemMargin: CGFloat = 12 { 64 | didSet { 65 | collectionView.collectionViewLayout.invalidateLayout() 66 | } 67 | } 68 | 69 | private var collectionView: UICollectionView! 70 | 71 | public override init(frame: CGRect) { 72 | super.init(frame: frame) 73 | initViews() 74 | } 75 | 76 | public required init?(coder aDecoder: NSCoder) { 77 | super.init(coder: aDecoder) 78 | initViews() 79 | } 80 | 81 | private func initViews() { 82 | autoresizingMask = .flexibleWidth 83 | 84 | let layout = UICollectionViewFlowLayout() 85 | layout.scrollDirection = .horizontal 86 | 87 | collectionView = UICollectionView(frame: bounds, collectionViewLayout: layout) 88 | collectionView.translatesAutoresizingMaskIntoConstraints = false 89 | collectionView.backgroundColor = backgroundColor 90 | collectionView.dataSource = self 91 | collectionView.delegate = self 92 | collectionView.showsHorizontalScrollIndicator = false 93 | collectionView.showsVerticalScrollIndicator = false 94 | collectionView.register(ToolbarCell.self, forCellWithReuseIdentifier: "cell") 95 | 96 | let visualView = UIVisualEffectView(frame: bounds) 97 | visualView.autoresizingMask = [.flexibleHeight, .flexibleWidth] 98 | visualView.effect = UIBlurEffect(style: .regular) 99 | visualView.contentView.addSubview(collectionView) 100 | 101 | pinViewEdges(of: collectionView, to: visualView) 102 | 103 | addSubview(visualView) 104 | } 105 | 106 | 107 | private func updateToolbar() { 108 | collectionView.reloadData() 109 | } 110 | 111 | func stringWidth(_ text: String, withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat { 112 | let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height) 113 | let boundingBox = text.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [.font: font], context: nil) 114 | 115 | return ceil(boundingBox.width) 116 | } 117 | 118 | // MARK: - CollectionView 119 | 120 | public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 121 | return options.count 122 | } 123 | 124 | public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 125 | let option = options[indexPath.item] 126 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! ToolbarCell 127 | cell.option = option 128 | 129 | return cell 130 | } 131 | 132 | public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 133 | let option = options[indexPath.item] 134 | 135 | if let cell = collectionView.cellForItem(at: indexPath) { 136 | option.action(self, sender: cell.contentView) 137 | } 138 | } 139 | 140 | public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { 141 | return itemMargin 142 | } 143 | 144 | public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 145 | let opt = options[indexPath.item] 146 | var width: CGFloat = 0 147 | if let image = opt.image { 148 | width = image.size.width 149 | } else { 150 | width = stringWidth(opt.title, withConstrainedHeight: bounds.height, font: DefaultFont) 151 | } 152 | return CGSize(width: width, height: bounds.height) 153 | } 154 | 155 | public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { 156 | return CGSize(width: itemMargin, height: 1) 157 | } 158 | 159 | public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { 160 | return CGSize(width: itemMargin, height: 1) 161 | } 162 | } 163 | 164 | 165 | private class ToolbarCell: UICollectionViewCell { 166 | var option: RichEditorOption! { 167 | didSet { 168 | // remove the previous subview 169 | contentView.subviews.first?.removeFromSuperview() 170 | 171 | var subview: UIView! 172 | 173 | // build the subview for the cell 174 | if let image = option.image { 175 | let imageView = UIImageView(frame: .zero) 176 | imageView.image = image 177 | imageView.contentMode = .scaleAspectFit 178 | subview = imageView 179 | } else { 180 | let label = UILabel(frame: .zero) 181 | label.text = option.title 182 | label.font = DefaultFont 183 | label.textColor = tintColor 184 | subview = label 185 | } 186 | 187 | subview.translatesAutoresizingMaskIntoConstraints = false 188 | subview.sizeToFit() 189 | contentView.addSubview(subview) 190 | pinViewEdges(of: subview, to: contentView) 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /RichEditorView/Classes/RichEditorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RichEditor.swift 3 | // 4 | // Created by Caesar Wirth on 4/1/15. 5 | // Updated/Modernized by C. Bess on 9/18/19. 6 | // 7 | // Copyright (c) 2015 Caesar Wirth. All rights reserved. 8 | // 9 | 10 | import UIKit 11 | import WebKit 12 | 13 | /// The value we hold in order to be able to set the line height before the JS completely loads. 14 | private let DefaultInnerLineHeight: Int = 21 15 | 16 | /// RichEditorDelegate defines callbacks for the delegate of the RichEditorView 17 | @objc public protocol RichEditorDelegate: AnyObject { 18 | /// Called when the inner height of the text being displayed changes 19 | /// Can be used to update the UI 20 | @objc optional func richEditor(_ editor: RichEditorView, heightDidChange height: Int) 21 | 22 | /// Called whenever the content inside the view changes 23 | @objc optional func richEditor(_ editor: RichEditorView, contentDidChange content: String) 24 | 25 | /// Called when the rich editor starts editing 26 | @objc optional func richEditorTookFocus(_ editor: RichEditorView) 27 | 28 | /// Called when the rich editor stops editing or loses focus 29 | @objc optional func richEditorLostFocus(_ editor: RichEditorView) 30 | 31 | /// Called when the RichEditorView has become ready to receive input 32 | /// More concretely, is called when the internal WKWebView loads for the first time, and contentHTML is set 33 | @objc optional func richEditorDidLoad(_ editor: RichEditorView) 34 | 35 | /// Called when the internal WKWebView begins loading a URL that it does not know how to respond to 36 | /// For example, if there is an external link, and then the user taps it 37 | @objc optional func richEditor(_ editor: RichEditorView, shouldInteractWith url: URL) -> Bool 38 | 39 | /// Called when the internal WKWebView response to the external link and the `richEditor(_ editor: RichEditorView, shouldInteractWith url: URL)` should return true 40 | /// You should open the external link in this function. 41 | @objc optional func richEditor(_ editor: RichEditorView, interactWith url: URL) 42 | 43 | /// Called when custom actions are called by callbacks in the JS 44 | /// By default, this method is not used unless called by some custom JS that you add 45 | @objc optional func richEditor(_ editor: RichEditorView, handle action: String) 46 | } 47 | 48 | /// RichEditorView is a UIView that displays richly styled text, and allows it to be edited in a WYSIWYG fashion. 49 | @objcMembers open class RichEditorView: UIView, UIScrollViewDelegate, WKNavigationDelegate, UIGestureRecognizerDelegate { 50 | /// The delegate that will receive callbacks when certain actions are completed. 51 | open weak var delegate: RichEditorDelegate? 52 | 53 | /// Input accessory view to display over they keyboard. 54 | /// Defaults to nil 55 | open override var inputAccessoryView: UIView? { 56 | get { return webView.accessoryView } 57 | set { webView.accessoryView = newValue } 58 | } 59 | 60 | /// The internal WKWebView that is used to display the editor. 61 | open private(set) var webView: RichEditorWebView 62 | 63 | /// Whether or not scroll is enabled on the view. 64 | open var isScrollEnabled: Bool = true { 65 | didSet { 66 | webView.scrollView.isScrollEnabled = isScrollEnabled 67 | } 68 | } 69 | 70 | /// Whether or not to allow user input in the view. 71 | open var editingEnabled: Bool = false { 72 | didSet { contentEditable = editingEnabled } 73 | } 74 | 75 | /// The content HTML of the text being displayed. 76 | /// Is continually updated as the text is being edited. 77 | open private(set) var contentHTML: String = "" { 78 | didSet { 79 | if isReady { 80 | delegate?.richEditor?(self, contentDidChange: contentHTML) 81 | } 82 | } 83 | } 84 | 85 | /// The internal height of the text being displayed. 86 | /// Is continually being updated as the text is edited. 87 | open private(set) var editorHeight: Int = 0 { 88 | didSet { 89 | delegate?.richEditor?(self, heightDidChange: editorHeight) 90 | } 91 | } 92 | 93 | /// The line height of the editor. Defaults to 21. 94 | open private(set) var lineHeight: Int = DefaultInnerLineHeight { 95 | didSet { 96 | runJS("RE.setLineHeight('\(lineHeight)px')") 97 | } 98 | } 99 | 100 | /// Whether or not the editor DOM element has finished loading or not yet. 101 | private var isEditorLoaded = false 102 | 103 | /// Indicates if the editor should begin sending events to the delegate 104 | private var isReady = false 105 | 106 | /// Value that stores whether or not the content should be editable when the editor is loaded. 107 | /// Is basically `isEditingEnabled` before the editor is loaded. 108 | private var editingEnabledVar = true 109 | 110 | /// The HTML that is currently loaded in the editor view, if it is loaded. If it has not been loaded yet, it is the 111 | /// HTML that will be loaded into the editor view once it finishes initializing. 112 | public var html: String = "" { 113 | didSet { 114 | setHTML(html) 115 | } 116 | } 117 | 118 | /// Private variable that holds the placeholder text, so you can set the placeholder before the editor loads. 119 | private var placeholderText: String = "" 120 | /// The placeholder text that should be shown when there is no user input. 121 | open var placeholder: String { 122 | get { return placeholderText } 123 | set { 124 | placeholderText = newValue 125 | if isEditorLoaded { 126 | runJS("RE.setPlaceholderText('\(newValue.escaped)')") 127 | } 128 | } 129 | } 130 | 131 | // MARK: Initialization 132 | 133 | public override init(frame: CGRect) { 134 | webView = RichEditorWebView() 135 | super.init(frame: frame) 136 | setup() 137 | } 138 | 139 | required public init?(coder aDecoder: NSCoder) { 140 | webView = RichEditorWebView() 141 | super.init(coder: aDecoder) 142 | setup() 143 | } 144 | 145 | private func setup() { 146 | // configure webview 147 | webView.frame = bounds 148 | webView.navigationDelegate = self 149 | webView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 150 | webView.configuration.dataDetectorTypes = WKDataDetectorTypes() 151 | webView.scrollView.isScrollEnabled = isScrollEnabled 152 | webView.scrollView.bounces = true 153 | webView.scrollView.delegate = self 154 | webView.scrollView.clipsToBounds = false 155 | addSubview(webView) 156 | 157 | reloadHTML(with: html) 158 | } 159 | 160 | /// Reloads the HTML for the editor. 161 | /// - parameter html: The HTML that will be loaded into the editor view once it finishes initializing. 162 | /// - parameter headerHTML: The header HTML that will be inserted after the default styles. 163 | /// - parameter footerHTML: The footer HTML that will be inserted after the default JavaScript. 164 | public func reloadHTML(with html: String, headerHTML: String = "", footerHTML: String = "") { 165 | guard let filePath = Bundle(for: RichEditorView.self).path(forResource: "rich_editor", ofType: "html") else { 166 | return 167 | } 168 | 169 | let readerHtmlTemplate = try! String(contentsOfFile: filePath) 170 | let fullHtml = readerHtmlTemplate 171 | .replacingOccurrences(of: "{{header}}", with: headerHTML) 172 | .replacingOccurrences(of: "{{footer}}", with: footerHTML) 173 | 174 | webView.loadHTMLString(fullHtml, baseURL: URL(fileURLWithPath: filePath, isDirectory: false).deletingLastPathComponent()) 175 | 176 | isEditorLoaded = false 177 | self.html = html 178 | } 179 | 180 | // MARK: - Rich Text Editing 181 | 182 | open func isEditingEnabled(handler: @escaping (Bool) -> Void) { 183 | isContentEditable(handler: handler) 184 | } 185 | 186 | private func getLineHeight(handler: @escaping (Int) -> Void) { 187 | if isEditorLoaded { 188 | runJS("RE.getLineHeight()") { r in 189 | if let r = Int(r) { 190 | handler(r) 191 | } else { 192 | handler(DefaultInnerLineHeight) 193 | } 194 | } 195 | } else { 196 | handler(DefaultInnerLineHeight) 197 | } 198 | } 199 | 200 | private func setHTML(_ value: String) { 201 | if isEditorLoaded { 202 | runJS("RE.setHtml('\(value.escaped)')") { _ in 203 | self.updateHeight() 204 | } 205 | } 206 | } 207 | 208 | /// The inner height of the editor div. 209 | /// Fetches it from JS every time, so might be slow! 210 | private func getClientHeight(handler: @escaping (Int) -> Void) { 211 | runJS("document.getElementById('editor').clientHeight") { r in 212 | if let r = Int(r) { 213 | handler(r) 214 | } else { 215 | handler(0) 216 | } 217 | } 218 | } 219 | 220 | public func getHtml(handler: @escaping (String) -> Void) { 221 | runJS("RE.getHtml()") { r in 222 | handler(r) 223 | } 224 | } 225 | 226 | /// Text representation of the data that has been input into the editor view, if it has been loaded. 227 | public func getText(handler: @escaping (String) -> Void) { 228 | runJS("RE.getText()") { r in 229 | handler(r) 230 | } 231 | } 232 | 233 | /// The href of the current selection, if the current selection's parent is an anchor tag. 234 | /// Will be nil if there is no href, or it is an empty string. 235 | public func getSelectedHref(handler: @escaping (String?) -> Void) { 236 | hasRangeSelection(handler: { r in 237 | if !r { 238 | handler(nil) 239 | return 240 | } 241 | self.runJS("RE.getSelectedHref()") { r in 242 | if r == "" { 243 | handler(nil) 244 | } else { 245 | handler(r) 246 | } 247 | } 248 | }) 249 | } 250 | 251 | /// Whether or not the selection has a type specifically of "Range". 252 | public func hasRangeSelection(handler: @escaping (Bool) -> Void) { 253 | runJS("RE.rangeSelectionExists()") { val in 254 | handler((val as NSString).boolValue) 255 | } 256 | } 257 | 258 | /// Whether or not the selection has a type specifically of "Range" or "Caret". 259 | public func hasRangeOrCaretSelection(handler: @escaping (Bool) -> Void) { 260 | runJS("RE.rangeOrCaretSelectionExists()") { val in 261 | handler((val as NSString).boolValue) 262 | } 263 | } 264 | 265 | // MARK: Methods 266 | 267 | public func removeFormat() { 268 | runJS("RE.removeFormat()") 269 | } 270 | 271 | public func setFontSize(_ size: Int) { 272 | runJS("RE.setFontSize('\(size)px')") 273 | } 274 | 275 | public func setEditorBackgroundColor(_ color: UIColor) { 276 | runJS("RE.setBackgroundColor('\(color.hex)')") 277 | } 278 | 279 | public func undo() { 280 | runJS("RE.undo()") 281 | } 282 | 283 | public func redo() { 284 | runJS("RE.redo()") 285 | } 286 | 287 | public func bold() { 288 | runJS("RE.setBold()") 289 | } 290 | 291 | public func italic() { 292 | runJS("RE.setItalic()") 293 | } 294 | 295 | // "superscript" is a keyword 296 | public func subscriptText() { 297 | runJS("RE.setSubscript()") 298 | } 299 | 300 | public func superscript() { 301 | runJS("RE.setSuperscript()") 302 | } 303 | 304 | public func strikethrough() { 305 | runJS("RE.setStrikeThrough()") 306 | } 307 | 308 | public func underline() { 309 | runJS("RE.setUnderline()") 310 | } 311 | 312 | private func getColorHex(with color: UIColor?) -> String { 313 | // if no color, then clear the color style css 314 | return color?.hex == nil ? "null" : "'\(color!.hex)'" 315 | } 316 | 317 | public func setTextColor(_ color: UIColor?) { 318 | runJS("RE.prepareInsert()") 319 | let color = getColorHex(with: color) 320 | runJS("RE.setTextColor(\(color))") 321 | } 322 | 323 | public func setEditorFontColor(_ color: UIColor) { 324 | runJS("RE.setBaseTextColor('\(color.hex)')") 325 | } 326 | 327 | public func setTextBackgroundColor(_ color: UIColor?) { 328 | runJS("RE.prepareInsert()") 329 | let color = getColorHex(with: color) 330 | runJS("RE.setTextBackgroundColor(\(color))") 331 | } 332 | 333 | public func header(_ h: Int) { 334 | runJS("RE.setHeading('\(h)')") 335 | } 336 | 337 | public func indent() { 338 | runJS("RE.setIndent()") 339 | } 340 | 341 | public func outdent() { 342 | runJS("RE.setOutdent()") 343 | } 344 | 345 | public func orderedList() { 346 | runJS("RE.setOrderedList()") 347 | } 348 | 349 | public func unorderedList() { 350 | runJS("RE.setUnorderedList()") 351 | } 352 | 353 | public func blockquote() { 354 | runJS("RE.setBlockquote()") 355 | } 356 | 357 | public func alignLeft() { 358 | runJS("RE.setJustifyLeft()") 359 | } 360 | 361 | public func alignCenter() { 362 | runJS("RE.setJustifyCenter()") 363 | } 364 | 365 | public func alignRight() { 366 | runJS("RE.setJustifyRight()") 367 | } 368 | 369 | public func insertImage(_ url: String, alt: String) { 370 | runJS("RE.prepareInsert()") 371 | runJS("RE.insertImage('\(url.escaped)', '\(alt.escaped)')") 372 | } 373 | 374 | public func insertLink(_ href: String, title: String) { 375 | runJS("RE.prepareInsert()") 376 | runJS("RE.insertLink('\(href.escaped)', '\(title.escaped)')") 377 | } 378 | 379 | public func focus() { 380 | runJS("RE.focus()") 381 | } 382 | 383 | public func focus(at: CGPoint) { 384 | runJS("RE.focusAtPoint(\(at.x), \(at.y))") 385 | } 386 | 387 | public func blur() { 388 | runJS("RE.blurFocus()") 389 | } 390 | 391 | /// Runs some JavaScript on the WKWebView and returns the result 392 | /// If there is no result, returns an empty string 393 | /// - parameter js: The JavaScript string to be run 394 | /// - returns: The result of the JavaScript that was run 395 | public func runJS(_ js: String, handler: ((String) -> Void)? = nil) { 396 | webView.evaluateJavaScript(js) { (result, error) in 397 | if let error = error { 398 | print("WKWebViewJavascriptBridge Error: \(String(describing: error)) - JS: \(js)") 399 | handler?("") 400 | return 401 | } 402 | 403 | guard let handler = handler else { 404 | return 405 | } 406 | 407 | if let resultInt = result as? Int { 408 | handler("\(resultInt)") 409 | return 410 | } 411 | 412 | if let resultBool = result as? Bool { 413 | handler(resultBool ? "true" : "false") 414 | return 415 | } 416 | 417 | if let resultStr = result as? String { 418 | handler(resultStr) 419 | return 420 | } 421 | 422 | // no result 423 | handler("") 424 | } 425 | } 426 | 427 | // MARK: - Delegate Methods 428 | 429 | // MARK: UIScrollViewDelegate 430 | 431 | public func scrollViewDidScroll(_ scrollView: UIScrollView) { 432 | // We use this to keep the scroll view from changing its offset when the keyboard comes up 433 | if !isScrollEnabled { 434 | scrollView.bounds = webView.bounds 435 | } 436 | } 437 | 438 | // MARK: WKWebViewDelegate 439 | 440 | public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { 441 | // empy 442 | } 443 | 444 | public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { 445 | // Handle pre-defined editor actions 446 | let callbackPrefix = "re-callback://" 447 | if navigationAction.request.url?.absoluteString.hasPrefix(callbackPrefix) == true { 448 | // When we get a callback, we need to fetch the command queue to run the commands 449 | // It comes in as a JSON array of commands that we need to parse 450 | runJS("RE.getCommandQueue()") { commands in 451 | if let data = commands.data(using: .utf8) { 452 | 453 | let jsonCommands: [String] 454 | do { 455 | jsonCommands = try JSONSerialization.jsonObject(with: data) as? [String] ?? [] 456 | } catch { 457 | jsonCommands = [] 458 | NSLog("RichEditorView: Failed to parse JSON Commands") 459 | } 460 | 461 | jsonCommands.forEach(self.performCommand) 462 | } 463 | } 464 | return decisionHandler(WKNavigationActionPolicy.cancel) 465 | } 466 | 467 | // User is tapping on a link, so we should react accordingly 468 | if navigationAction.navigationType == .linkActivated { 469 | if let url = navigationAction.request.url { 470 | if delegate?.richEditor?(self, shouldInteractWith: url) ?? false { 471 | delegate?.richEditor?(self, interactWith: url) 472 | return decisionHandler(WKNavigationActionPolicy.cancel) 473 | } 474 | } 475 | } 476 | 477 | return decisionHandler(WKNavigationActionPolicy.allow) 478 | } 479 | 480 | // MARK: UIGestureRecognizerDelegate 481 | 482 | /// Delegate method for our UITapGestureDelegate. 483 | /// Since the internal web view also has gesture recognizers, we have to make sure that we actually receive our taps. 484 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 485 | return true 486 | } 487 | 488 | // MARK: - Private Implementation Details 489 | 490 | private var contentEditable: Bool = false { 491 | didSet { 492 | editingEnabledVar = contentEditable 493 | if isEditorLoaded { 494 | let value = (contentEditable ? "true" : "false") 495 | runJS("RE.editor.contentEditable = \(value)") 496 | } 497 | } 498 | } 499 | private func isContentEditable(handler: @escaping (Bool) -> Void) { 500 | if isEditorLoaded { 501 | // to get the "editable" value is a different property, than to disable it 502 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/contentEditable 503 | runJS("RE.editor.isContentEditable") { value in 504 | self.editingEnabledVar = Bool(value) ?? false 505 | } 506 | } 507 | } 508 | 509 | /// The position of the caret relative to the currently shown content. 510 | /// For example, if the cursor is directly at the top of what is visible, it will return 0. 511 | /// This also means that it will be negative if it is above what is currently visible. 512 | /// Can also return 0 if some sort of error occurs between JS and here. 513 | private func relativeCaretYPosition(handler: @escaping (Int) -> Void) { 514 | runJS("RE.getRelativeCaretYPosition()") { r in 515 | handler(Int(r) ?? 0) 516 | } 517 | } 518 | 519 | private func updateHeight() { 520 | runJS("document.getElementById('editor').clientHeight") { heightString in 521 | let height = Int(heightString) ?? 0 522 | if self.editorHeight != height { 523 | self.editorHeight = height 524 | } 525 | } 526 | } 527 | 528 | /// Scrolls the editor to a position where the caret is visible. 529 | /// Called repeatedly to make sure the caret is always visible when inputting text. 530 | /// Works only if the `lineHeight` of the editor is available. 531 | private func scrollCaretToVisible() { 532 | let scrollView = self.webView.scrollView 533 | 534 | getClientHeight(handler: { clientHeight in 535 | let contentHeight = clientHeight > 0 ? CGFloat(clientHeight) : scrollView.frame.height 536 | scrollView.contentSize = CGSize(width: scrollView.frame.width, height: contentHeight) 537 | 538 | // XXX: Maybe find a better way to get the cursor height 539 | self.getLineHeight(handler: { lh in 540 | let lineHeight = CGFloat(lh) 541 | let cursorHeight = lineHeight - 4 542 | self.relativeCaretYPosition(handler: { r in 543 | let visiblePosition = CGFloat(r) 544 | var offset: CGPoint? 545 | 546 | if visiblePosition + cursorHeight > scrollView.bounds.size.height { 547 | // Visible caret position goes further than our bounds 548 | offset = CGPoint(x: 0, y: (visiblePosition + lineHeight) - scrollView.bounds.height + scrollView.contentOffset.y) 549 | } else if visiblePosition < 0 { 550 | // Visible caret position is above what is currently visible 551 | var amount = scrollView.contentOffset.y + visiblePosition 552 | amount = amount < 0 ? 0 : amount 553 | offset = CGPoint(x: scrollView.contentOffset.x, y: amount) 554 | } 555 | 556 | if let offset = offset { 557 | scrollView.setContentOffset(offset, animated: true) 558 | } 559 | }) 560 | }) 561 | }) 562 | } 563 | 564 | /// Called when actions are received from JavaScript 565 | /// - parameter method: String with the name of the method and optional parameters that were passed in 566 | private func performCommand(_ method: String) { 567 | if method.hasPrefix("ready") { 568 | // If loading for the first time, we have to set the content HTML to be displayed 569 | if !isEditorLoaded { 570 | isEditorLoaded = true 571 | setHTML(html) 572 | contentHTML = html 573 | contentEditable = editingEnabledVar 574 | placeholder = placeholderText 575 | lineHeight = DefaultInnerLineHeight 576 | 577 | delegate?.richEditorDidLoad?(self) 578 | isReady = true 579 | } 580 | updateHeight() 581 | } else if method.hasPrefix("input") { 582 | scrollCaretToVisible() 583 | runJS("RE.getHtml()") { content in 584 | self.contentHTML = content 585 | self.updateHeight() 586 | } 587 | } else if method.hasPrefix("updateHeight") { 588 | updateHeight() 589 | } else if method.hasPrefix("focus") { 590 | delegate?.richEditorTookFocus?(self) 591 | } else if method.hasPrefix("blur") { 592 | delegate?.richEditorLostFocus?(self) 593 | } else if method.hasPrefix("action/") { 594 | runJS("RE.getHtml()") { content in 595 | self.contentHTML = content 596 | 597 | // If there are any custom actions being called 598 | // We need to tell the delegate about it 599 | let actionPrefix = "action/" 600 | let range = method.range(of: actionPrefix)! 601 | let action = method.replacingCharacters(in: range, with: "") 602 | 603 | self.delegate?.richEditor?(self, handle: action) 604 | } 605 | } 606 | } 607 | 608 | // MARK: - Responder Handling 609 | 610 | override open func becomeFirstResponder() -> Bool { 611 | if !webView.isFirstResponder { 612 | focus() 613 | return true 614 | } else { 615 | return false 616 | } 617 | } 618 | 619 | open override func resignFirstResponder() -> Bool { 620 | blur() 621 | return true 622 | } 623 | 624 | } 625 | -------------------------------------------------------------------------------- /RichEditorView/Classes/RichEditorWebView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RichEditorWebView.swift 3 | // RichEditorView 4 | // 5 | // Created by C. Bess on 9/18/19. 6 | // 7 | 8 | import WebKit 9 | 10 | public class RichEditorWebView: WKWebView { 11 | 12 | public var accessoryView: UIView? 13 | 14 | public override var inputAccessoryView: UIView? { 15 | return accessoryView 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /RichEditorView/Classes/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extensions.swift 3 | // Pods 4 | // 5 | // Created by Caesar Wirth on 10/9/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | internal extension String { 12 | 13 | /// A string with the ' characters in it escaped. 14 | /// Used when passing a string into JavaScript, so the string is not completed too soon 15 | var escaped: String { 16 | let unicode = self.unicodeScalars 17 | var newString = "" 18 | for char in unicode { 19 | if char.value == 39 || // 39 == ' in ASCII 20 | char.value < 9 || // 9 == horizontal tab in ASCII 21 | (char.value > 9 && char.value < 32) // < 32 == special characters in ASCII 22 | { 23 | let escaped = char.escaped(asASCII: true) 24 | newString.append(escaped) 25 | } else { 26 | newString.append(String(char)) 27 | } 28 | } 29 | return newString 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /RichEditorView/Classes/UIColor+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Extensions.swift 3 | // Pods 4 | // 5 | // Created by Caesar Wirth on 10/9/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | internal extension UIColor { 12 | 13 | /// Hexadecimal representation of the UIColor. 14 | /// For example, UIColor.blackColor() becomes "#000000". 15 | var hex: String { 16 | var red: CGFloat = 0 17 | var green: CGFloat = 0 18 | var blue: CGFloat = 0 19 | self.getRed(&red, green: &green, blue: &blue, alpha: nil) 20 | 21 | let r = Int(255.0 * red) 22 | let g = Int(255.0 * green) 23 | let b = Int(255.0 * blue) 24 | 25 | let str = String(format: "#%02x%02x%02x", r, g, b) 26 | return str 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /RichEditorView/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 |4 | 27 | -------------------------------------------------------------------------------- /RichEditorView/RichEditorView-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // RichEditorView-Bridging-Header.h 3 | // RichEditorView 4 | // 5 | // Created by Caesar Wirth on 4/7/15. 6 | // 7 | // 8 | 9 | #ifndef RichEditorView_RichEditorView_Bridging_Header_h 10 | #define RichEditorView_RichEditorView_Bridging_Header_h 11 | 12 | #import "CJWWebView+HackishAccessoryHiding.h" 13 | 14 | #endif 15 | -------------------------------------------------------------------------------- /RichEditorView/RichEditorView.h: -------------------------------------------------------------------------------- 1 | // 2 | // RichEditorView.h 3 | // RichEditorView 4 | // 5 | // Created by Caesar Wirth on 4/7/15. 6 | // 7 | // 8 | 9 | #import5 | 26 |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 19 |CFBundleSignature 20 |???? 21 |CFBundleVersion 22 |$(CURRENT_PROJECT_VERSION) 23 |NSPrincipalClass 24 |25 | 10 | 11 | #import 12 | 13 | //! Project version number for RichEditorView. 14 | FOUNDATION_EXPORT double RichEditorViewVersionNumber; 15 | 16 | //! Project version string for RichEditorView. 17 | FOUNDATION_EXPORT const unsigned char RichEditorViewVersionString[]; 18 | 19 | // In this header, you should import all the public headers of your framework using statements like #import 20 | 21 | 22 | -------------------------------------------------------------------------------- /RichEditorViewSample/Podfile: -------------------------------------------------------------------------------- 1 | use_frameworks! 2 | platform :ios, '14.0' 3 | 4 | target 'RichEditorViewSample' do 5 | pod "RichEditorView", :path => "../" 6 | end 7 | -------------------------------------------------------------------------------- /RichEditorViewSample/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - RichEditorView (5.1) 3 | 4 | DEPENDENCIES: 5 | - RichEditorView (from `../`) 6 | 7 | EXTERNAL SOURCES: 8 | RichEditorView: 9 | :path: "../" 10 | 11 | SPEC CHECKSUMS: 12 | RichEditorView: 2aac47416da9ce045349f1d5e22092a8a3c73f6e 13 | 14 | PODFILE CHECKSUM: b9082cf7e9c6b4481e0b6505572cdb4b0674e276 15 | 16 | COCOAPODS: 1.11.3 17 | -------------------------------------------------------------------------------- /RichEditorViewSample/Pods/Local Podspecs/RichEditorView.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RichEditorView", 3 | "version": "5.1", 4 | "summary": "Rich Text Editor for iOS written in Swift", 5 | "homepage": "https://github.com/cjwirth/RichEditorView", 6 | "license": "BSD 3-clause", 7 | "authors": { 8 | "C. Bess": "cbess@users.noreply.github.com", 9 | "Caesar Wirth": "cjwirth@gmail.com" 10 | }, 11 | "source": { 12 | "git": "https://github.com/cbess/RichEditorView.git", 13 | "tag": "5.1" 14 | }, 15 | "platforms": { 16 | "ios": "12.0" 17 | }, 18 | "swift_versions": "5.0", 19 | "requires_arc": true, 20 | "source_files": "RichEditorView/Classes/*", 21 | "resources": [ 22 | "RichEditorView/Assets/icons/*", 23 | "RichEditorView/Assets/editor/*" 24 | ], 25 | "swift_version": "5.0" 26 | } 27 | -------------------------------------------------------------------------------- /RichEditorViewSample/Pods/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - RichEditorView (5.1) 3 | 4 | DEPENDENCIES: 5 | - RichEditorView (from `../`) 6 | 7 | EXTERNAL SOURCES: 8 | RichEditorView: 9 | :path: "../" 10 | 11 | SPEC CHECKSUMS: 12 | RichEditorView: 2aac47416da9ce045349f1d5e22092a8a3c73f6e 13 | 14 | PODFILE CHECKSUM: b9082cf7e9c6b4481e0b6505572cdb4b0674e276 15 | 16 | COCOAPODS: 1.10.0 17 | -------------------------------------------------------------------------------- /RichEditorViewSample/Pods/Target Support Files/RichEditorView/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 27 | -------------------------------------------------------------------------------- /RichEditorViewSample/Pods/Target Support Files/RichEditorView/RichEditorView-dummy.m: -------------------------------------------------------------------------------- 1 | #import5 | 26 |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 |$(MARKETING_VERSION) 19 |CFBundleSignature 20 |???? 21 |CFBundleVersion 22 |$(CURRENT_PROJECT_VERSION) 23 |NSPrincipalClass 24 |25 | 2 | @interface PodsDummy_RichEditorView : NSObject 3 | @end 4 | @implementation PodsDummy_RichEditorView 5 | @end 6 | -------------------------------------------------------------------------------- /RichEditorViewSample/Pods/Target Support Files/RichEditorView/RichEditorView-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 | -------------------------------------------------------------------------------- /RichEditorViewSample/Pods/Target Support Files/RichEditorView/RichEditorView-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 RichEditorViewVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char RichEditorViewVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /RichEditorViewSample/Pods/Target Support Files/RichEditorView/RichEditorView.modulemap: -------------------------------------------------------------------------------- 1 | framework module RichEditorView { 2 | umbrella header "RichEditorView-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /RichEditorViewSample/Pods/Target Support Files/RichEditorView/RichEditorView.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/RichEditorView 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 | -------------------------------------------------------------------------------- /RichEditorViewSample/RichEditorViewSample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0CCBA517EA66E1DE61C19F69 /* Pods_RichEditorViewSample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CA6B4DFCB1BCDED9BE91A70 /* Pods_RichEditorViewSample.framework */; }; 11 | 39883B491AD0DC270031FD16 /* KeyboardManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39883B481AD0DC270031FD16 /* KeyboardManager.swift */; }; 12 | 39BBCFB01AD0CC7A00A450D2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39BBCFAF1AD0CC7A00A450D2 /* AppDelegate.swift */; }; 13 | 39BBCFB21AD0CC7A00A450D2 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39BBCFB11AD0CC7A00A450D2 /* ViewController.swift */; }; 14 | 39BBCFB51AD0CC7A00A450D2 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 39BBCFB31AD0CC7A00A450D2 /* Main.storyboard */; }; 15 | 39BBCFB71AD0CC7A00A450D2 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 39BBCFB61AD0CC7A00A450D2 /* Images.xcassets */; }; 16 | 39BBCFBA1AD0CC7A00A450D2 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 39BBCFB81AD0CC7A00A450D2 /* LaunchScreen.xib */; }; 17 | 39BBCFC61AD0CC7A00A450D2 /* RichEditorViewSampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39BBCFC51AD0CC7A00A450D2 /* RichEditorViewSampleTests.swift */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXContainerItemProxy section */ 21 | 39BBCFC01AD0CC7A00A450D2 /* PBXContainerItemProxy */ = { 22 | isa = PBXContainerItemProxy; 23 | containerPortal = 39BBCFA21AD0CC7A00A450D2 /* Project object */; 24 | proxyType = 1; 25 | remoteGlobalIDString = 39BBCFA91AD0CC7A00A450D2; 26 | remoteInfo = RichEditorViewSample; 27 | }; 28 | /* End PBXContainerItemProxy section */ 29 | 30 | /* Begin PBXFileReference section */ 31 | 1CA6B4DFCB1BCDED9BE91A70 /* Pods_RichEditorViewSample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RichEditorViewSample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 32 | 3912A4531B966C34005E41FA /* RichEditorViewSample-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RichEditorViewSample-Bridging-Header.h"; sourceTree = " "; }; 33 | 39883B481AD0DC270031FD16 /* KeyboardManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardManager.swift; sourceTree = " "; }; 34 | 39BBCFAA1AD0CC7A00A450D2 /* RichEditorViewSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RichEditorViewSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 35 | 39BBCFAE1AD0CC7A00A450D2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = " "; }; 36 | 39BBCFAF1AD0CC7A00A450D2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = " "; }; 37 | 39BBCFB11AD0CC7A00A450D2 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = " "; }; 38 | 39BBCFB41AD0CC7A00A450D2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = " "; }; 39 | 39BBCFB61AD0CC7A00A450D2 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = " "; }; 40 | 39BBCFB91AD0CC7A00A450D2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = " "; }; 41 | 39BBCFBF1AD0CC7A00A450D2 /* RichEditorViewSampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RichEditorViewSampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 42 | 39BBCFC41AD0CC7A00A450D2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = " "; }; 43 | 39BBCFC51AD0CC7A00A450D2 /* RichEditorViewSampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RichEditorViewSampleTests.swift; sourceTree = " "; }; 44 | 39BBCFD01AD0CD4700A450D2 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = " "; }; 45 | 39BBCFD11AD0CD4700A450D2 /* RichEditorView.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = RichEditorView.podspec; path = ../RichEditorView.podspec; sourceTree = " "; }; 46 | 996D5C7A9F5BD7BF325118C8 /* Pods.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 47 | 9AF21FE45EA87DF1498534F6 /* Pods-RichEditorViewSample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RichEditorViewSample.release.xcconfig"; path = "Pods/Target Support Files/Pods-RichEditorViewSample/Pods-RichEditorViewSample.release.xcconfig"; sourceTree = " "; }; 48 | C6E2423BEEB12C12D184ACDD /* Pods-RichEditorViewSample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RichEditorViewSample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RichEditorViewSample/Pods-RichEditorViewSample.debug.xcconfig"; sourceTree = " "; }; 49 | /* End PBXFileReference section */ 50 | 51 | /* Begin PBXFrameworksBuildPhase section */ 52 | 39BBCFA71AD0CC7A00A450D2 /* Frameworks */ = { 53 | isa = PBXFrameworksBuildPhase; 54 | buildActionMask = 2147483647; 55 | files = ( 56 | 0CCBA517EA66E1DE61C19F69 /* Pods_RichEditorViewSample.framework in Frameworks */, 57 | ); 58 | runOnlyForDeploymentPostprocessing = 0; 59 | }; 60 | 39BBCFBC1AD0CC7A00A450D2 /* Frameworks */ = { 61 | isa = PBXFrameworksBuildPhase; 62 | buildActionMask = 2147483647; 63 | files = ( 64 | ); 65 | runOnlyForDeploymentPostprocessing = 0; 66 | }; 67 | /* End PBXFrameworksBuildPhase section */ 68 | 69 | /* Begin PBXGroup section */ 70 | 106F1EB9E1302E9C38A73C48 /* Frameworks */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | 996D5C7A9F5BD7BF325118C8 /* Pods.framework */, 74 | 1CA6B4DFCB1BCDED9BE91A70 /* Pods_RichEditorViewSample.framework */, 75 | ); 76 | name = Frameworks; 77 | sourceTree = " "; 78 | }; 79 | 39BBCFA11AD0CC7A00A450D2 = { 80 | isa = PBXGroup; 81 | children = ( 82 | 39BBCFCF1AD0CD3500A450D2 /* Pod Metadata */, 83 | 39BBCFAC1AD0CC7A00A450D2 /* RichEditorViewSample */, 84 | 39BBCFC21AD0CC7A00A450D2 /* RichEditorViewSampleTests */, 85 | 39BBCFAB1AD0CC7A00A450D2 /* Products */, 86 | 106F1EB9E1302E9C38A73C48 /* Frameworks */, 87 | 6B97E8B97DF9831B87D18089 /* Pods */, 88 | ); 89 | sourceTree = " "; 90 | }; 91 | 39BBCFAB1AD0CC7A00A450D2 /* Products */ = { 92 | isa = PBXGroup; 93 | children = ( 94 | 39BBCFAA1AD0CC7A00A450D2 /* RichEditorViewSample.app */, 95 | 39BBCFBF1AD0CC7A00A450D2 /* RichEditorViewSampleTests.xctest */, 96 | ); 97 | name = Products; 98 | sourceTree = " "; 99 | }; 100 | 39BBCFAC1AD0CC7A00A450D2 /* RichEditorViewSample */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | 39BBCFAF1AD0CC7A00A450D2 /* AppDelegate.swift */, 104 | 39BBCFB11AD0CC7A00A450D2 /* ViewController.swift */, 105 | 39BBCFB31AD0CC7A00A450D2 /* Main.storyboard */, 106 | 39BBCFB61AD0CC7A00A450D2 /* Images.xcassets */, 107 | 39BBCFB81AD0CC7A00A450D2 /* LaunchScreen.xib */, 108 | 39BBCFAD1AD0CC7A00A450D2 /* Supporting Files */, 109 | 39883B481AD0DC270031FD16 /* KeyboardManager.swift */, 110 | 3912A4531B966C34005E41FA /* RichEditorViewSample-Bridging-Header.h */, 111 | ); 112 | path = RichEditorViewSample; 113 | sourceTree = " "; 114 | }; 115 | 39BBCFAD1AD0CC7A00A450D2 /* Supporting Files */ = { 116 | isa = PBXGroup; 117 | children = ( 118 | 39BBCFAE1AD0CC7A00A450D2 /* Info.plist */, 119 | ); 120 | name = "Supporting Files"; 121 | sourceTree = " "; 122 | }; 123 | 39BBCFC21AD0CC7A00A450D2 /* RichEditorViewSampleTests */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | 39BBCFC51AD0CC7A00A450D2 /* RichEditorViewSampleTests.swift */, 127 | 39BBCFC31AD0CC7A00A450D2 /* Supporting Files */, 128 | ); 129 | path = RichEditorViewSampleTests; 130 | sourceTree = " "; 131 | }; 132 | 39BBCFC31AD0CC7A00A450D2 /* Supporting Files */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 39BBCFC41AD0CC7A00A450D2 /* Info.plist */, 136 | ); 137 | name = "Supporting Files"; 138 | sourceTree = " "; 139 | }; 140 | 39BBCFCF1AD0CD3500A450D2 /* Pod Metadata */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | 39BBCFD01AD0CD4700A450D2 /* README.md */, 144 | 39BBCFD11AD0CD4700A450D2 /* RichEditorView.podspec */, 145 | ); 146 | name = "Pod Metadata"; 147 | sourceTree = " "; 148 | }; 149 | 6B97E8B97DF9831B87D18089 /* Pods */ = { 150 | isa = PBXGroup; 151 | children = ( 152 | C6E2423BEEB12C12D184ACDD /* Pods-RichEditorViewSample.debug.xcconfig */, 153 | 9AF21FE45EA87DF1498534F6 /* Pods-RichEditorViewSample.release.xcconfig */, 154 | ); 155 | name = Pods; 156 | sourceTree = " "; 157 | }; 158 | /* End PBXGroup section */ 159 | 160 | /* Begin PBXNativeTarget section */ 161 | 39BBCFA91AD0CC7A00A450D2 /* RichEditorViewSample */ = { 162 | isa = PBXNativeTarget; 163 | buildConfigurationList = 39BBCFC91AD0CC7A00A450D2 /* Build configuration list for PBXNativeTarget "RichEditorViewSample" */; 164 | buildPhases = ( 165 | 4E74B098CF14DF0631DC2B86 /* [CP] Check Pods Manifest.lock */, 166 | 39BBCFA61AD0CC7A00A450D2 /* Sources */, 167 | 39BBCFA71AD0CC7A00A450D2 /* Frameworks */, 168 | 39BBCFA81AD0CC7A00A450D2 /* Resources */, 169 | 912CF3E5DB1B7FACFF97C026 /* [CP] Embed Pods Frameworks */, 170 | ); 171 | buildRules = ( 172 | ); 173 | dependencies = ( 174 | ); 175 | name = RichEditorViewSample; 176 | productName = RichEditorViewSample; 177 | productReference = 39BBCFAA1AD0CC7A00A450D2 /* RichEditorViewSample.app */; 178 | productType = "com.apple.product-type.application"; 179 | }; 180 | 39BBCFBE1AD0CC7A00A450D2 /* RichEditorViewSampleTests */ = { 181 | isa = PBXNativeTarget; 182 | buildConfigurationList = 39BBCFCC1AD0CC7A00A450D2 /* Build configuration list for PBXNativeTarget "RichEditorViewSampleTests" */; 183 | buildPhases = ( 184 | 39BBCFBB1AD0CC7A00A450D2 /* Sources */, 185 | 39BBCFBC1AD0CC7A00A450D2 /* Frameworks */, 186 | 39BBCFBD1AD0CC7A00A450D2 /* Resources */, 187 | ); 188 | buildRules = ( 189 | ); 190 | dependencies = ( 191 | 39BBCFC11AD0CC7A00A450D2 /* PBXTargetDependency */, 192 | ); 193 | name = RichEditorViewSampleTests; 194 | productName = RichEditorViewSampleTests; 195 | productReference = 39BBCFBF1AD0CC7A00A450D2 /* RichEditorViewSampleTests.xctest */; 196 | productType = "com.apple.product-type.bundle.unit-test"; 197 | }; 198 | /* End PBXNativeTarget section */ 199 | 200 | /* Begin PBXProject section */ 201 | 39BBCFA21AD0CC7A00A450D2 /* Project object */ = { 202 | isa = PBXProject; 203 | attributes = { 204 | LastSwiftUpdateCheck = 0700; 205 | LastUpgradeCheck = 1200; 206 | ORGANIZATIONNAME = "Caesar Wirth"; 207 | TargetAttributes = { 208 | 39BBCFA91AD0CC7A00A450D2 = { 209 | CreatedOnToolsVersion = 6.2; 210 | DevelopmentTeam = 89MZF7SG7M; 211 | LastSwiftMigration = ""; 212 | }; 213 | 39BBCFBE1AD0CC7A00A450D2 = { 214 | CreatedOnToolsVersion = 6.2; 215 | LastSwiftMigration = ""; 216 | TestTargetID = 39BBCFA91AD0CC7A00A450D2; 217 | }; 218 | }; 219 | }; 220 | buildConfigurationList = 39BBCFA51AD0CC7A00A450D2 /* Build configuration list for PBXProject "RichEditorViewSample" */; 221 | compatibilityVersion = "Xcode 3.2"; 222 | developmentRegion = en; 223 | hasScannedForEncodings = 0; 224 | knownRegions = ( 225 | en, 226 | Base, 227 | ); 228 | mainGroup = 39BBCFA11AD0CC7A00A450D2; 229 | productRefGroup = 39BBCFAB1AD0CC7A00A450D2 /* Products */; 230 | projectDirPath = ""; 231 | projectRoot = ""; 232 | targets = ( 233 | 39BBCFA91AD0CC7A00A450D2 /* RichEditorViewSample */, 234 | 39BBCFBE1AD0CC7A00A450D2 /* RichEditorViewSampleTests */, 235 | ); 236 | }; 237 | /* End PBXProject section */ 238 | 239 | /* Begin PBXResourcesBuildPhase section */ 240 | 39BBCFA81AD0CC7A00A450D2 /* Resources */ = { 241 | isa = PBXResourcesBuildPhase; 242 | buildActionMask = 2147483647; 243 | files = ( 244 | 39BBCFB51AD0CC7A00A450D2 /* Main.storyboard in Resources */, 245 | 39BBCFBA1AD0CC7A00A450D2 /* LaunchScreen.xib in Resources */, 246 | 39BBCFB71AD0CC7A00A450D2 /* Images.xcassets in Resources */, 247 | ); 248 | runOnlyForDeploymentPostprocessing = 0; 249 | }; 250 | 39BBCFBD1AD0CC7A00A450D2 /* Resources */ = { 251 | isa = PBXResourcesBuildPhase; 252 | buildActionMask = 2147483647; 253 | files = ( 254 | ); 255 | runOnlyForDeploymentPostprocessing = 0; 256 | }; 257 | /* End PBXResourcesBuildPhase section */ 258 | 259 | /* Begin PBXShellScriptBuildPhase section */ 260 | 4E74B098CF14DF0631DC2B86 /* [CP] Check Pods Manifest.lock */ = { 261 | isa = PBXShellScriptBuildPhase; 262 | buildActionMask = 2147483647; 263 | files = ( 264 | ); 265 | inputPaths = ( 266 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 267 | "${PODS_ROOT}/Manifest.lock", 268 | ); 269 | name = "[CP] Check Pods Manifest.lock"; 270 | outputPaths = ( 271 | "$(DERIVED_FILE_DIR)/Pods-RichEditorViewSample-checkManifestLockResult.txt", 272 | ); 273 | runOnlyForDeploymentPostprocessing = 0; 274 | shellPath = /bin/sh; 275 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 276 | showEnvVarsInLog = 0; 277 | }; 278 | 912CF3E5DB1B7FACFF97C026 /* [CP] Embed Pods Frameworks */ = { 279 | isa = PBXShellScriptBuildPhase; 280 | buildActionMask = 2147483647; 281 | files = ( 282 | ); 283 | inputPaths = ( 284 | "${PODS_ROOT}/Target Support Files/Pods-RichEditorViewSample/Pods-RichEditorViewSample-frameworks.sh", 285 | "${BUILT_PRODUCTS_DIR}/RichEditorView/RichEditorView.framework", 286 | ); 287 | name = "[CP] Embed Pods Frameworks"; 288 | outputPaths = ( 289 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RichEditorView.framework", 290 | ); 291 | runOnlyForDeploymentPostprocessing = 0; 292 | shellPath = /bin/sh; 293 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-RichEditorViewSample/Pods-RichEditorViewSample-frameworks.sh\"\n"; 294 | showEnvVarsInLog = 0; 295 | }; 296 | /* End PBXShellScriptBuildPhase section */ 297 | 298 | /* Begin PBXSourcesBuildPhase section */ 299 | 39BBCFA61AD0CC7A00A450D2 /* Sources */ = { 300 | isa = PBXSourcesBuildPhase; 301 | buildActionMask = 2147483647; 302 | files = ( 303 | 39883B491AD0DC270031FD16 /* KeyboardManager.swift in Sources */, 304 | 39BBCFB21AD0CC7A00A450D2 /* ViewController.swift in Sources */, 305 | 39BBCFB01AD0CC7A00A450D2 /* AppDelegate.swift in Sources */, 306 | ); 307 | runOnlyForDeploymentPostprocessing = 0; 308 | }; 309 | 39BBCFBB1AD0CC7A00A450D2 /* Sources */ = { 310 | isa = PBXSourcesBuildPhase; 311 | buildActionMask = 2147483647; 312 | files = ( 313 | 39BBCFC61AD0CC7A00A450D2 /* RichEditorViewSampleTests.swift in Sources */, 314 | ); 315 | runOnlyForDeploymentPostprocessing = 0; 316 | }; 317 | /* End PBXSourcesBuildPhase section */ 318 | 319 | /* Begin PBXTargetDependency section */ 320 | 39BBCFC11AD0CC7A00A450D2 /* PBXTargetDependency */ = { 321 | isa = PBXTargetDependency; 322 | target = 39BBCFA91AD0CC7A00A450D2 /* RichEditorViewSample */; 323 | targetProxy = 39BBCFC01AD0CC7A00A450D2 /* PBXContainerItemProxy */; 324 | }; 325 | /* End PBXTargetDependency section */ 326 | 327 | /* Begin PBXVariantGroup section */ 328 | 39BBCFB31AD0CC7A00A450D2 /* Main.storyboard */ = { 329 | isa = PBXVariantGroup; 330 | children = ( 331 | 39BBCFB41AD0CC7A00A450D2 /* Base */, 332 | ); 333 | name = Main.storyboard; 334 | sourceTree = " "; 335 | }; 336 | 39BBCFB81AD0CC7A00A450D2 /* LaunchScreen.xib */ = { 337 | isa = PBXVariantGroup; 338 | children = ( 339 | 39BBCFB91AD0CC7A00A450D2 /* Base */, 340 | ); 341 | name = LaunchScreen.xib; 342 | sourceTree = " "; 343 | }; 344 | /* End PBXVariantGroup section */ 345 | 346 | /* Begin XCBuildConfiguration section */ 347 | 39BBCFC71AD0CC7A00A450D2 /* Debug */ = { 348 | isa = XCBuildConfiguration; 349 | buildSettings = { 350 | ALWAYS_SEARCH_USER_PATHS = NO; 351 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 352 | CLANG_CXX_LIBRARY = "libc++"; 353 | CLANG_ENABLE_MODULES = YES; 354 | CLANG_ENABLE_OBJC_ARC = YES; 355 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 356 | CLANG_WARN_BOOL_CONVERSION = YES; 357 | CLANG_WARN_COMMA = YES; 358 | CLANG_WARN_CONSTANT_CONVERSION = YES; 359 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 360 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 361 | CLANG_WARN_EMPTY_BODY = YES; 362 | CLANG_WARN_ENUM_CONVERSION = YES; 363 | CLANG_WARN_INFINITE_RECURSION = YES; 364 | CLANG_WARN_INT_CONVERSION = YES; 365 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 366 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 367 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 368 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 369 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 370 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 371 | CLANG_WARN_STRICT_PROTOTYPES = YES; 372 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 373 | CLANG_WARN_UNREACHABLE_CODE = YES; 374 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 375 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 376 | COPY_PHASE_STRIP = NO; 377 | ENABLE_STRICT_OBJC_MSGSEND = YES; 378 | ENABLE_TESTABILITY = YES; 379 | GCC_C_LANGUAGE_STANDARD = gnu99; 380 | GCC_DYNAMIC_NO_PIC = NO; 381 | GCC_NO_COMMON_BLOCKS = YES; 382 | GCC_OPTIMIZATION_LEVEL = 0; 383 | GCC_PREPROCESSOR_DEFINITIONS = ( 384 | "DEBUG=1", 385 | "$(inherited)", 386 | ); 387 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 388 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 389 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 390 | GCC_WARN_UNDECLARED_SELECTOR = YES; 391 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 392 | GCC_WARN_UNUSED_FUNCTION = YES; 393 | GCC_WARN_UNUSED_VARIABLE = YES; 394 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 395 | MTL_ENABLE_DEBUG_INFO = YES; 396 | ONLY_ACTIVE_ARCH = YES; 397 | SDKROOT = iphoneos; 398 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 399 | TARGETED_DEVICE_FAMILY = "1,2"; 400 | }; 401 | name = Debug; 402 | }; 403 | 39BBCFC81AD0CC7A00A450D2 /* Release */ = { 404 | isa = XCBuildConfiguration; 405 | buildSettings = { 406 | ALWAYS_SEARCH_USER_PATHS = NO; 407 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 408 | CLANG_CXX_LIBRARY = "libc++"; 409 | CLANG_ENABLE_MODULES = YES; 410 | CLANG_ENABLE_OBJC_ARC = YES; 411 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 412 | CLANG_WARN_BOOL_CONVERSION = YES; 413 | CLANG_WARN_COMMA = YES; 414 | CLANG_WARN_CONSTANT_CONVERSION = YES; 415 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 416 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 417 | CLANG_WARN_EMPTY_BODY = YES; 418 | CLANG_WARN_ENUM_CONVERSION = YES; 419 | CLANG_WARN_INFINITE_RECURSION = YES; 420 | CLANG_WARN_INT_CONVERSION = YES; 421 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 422 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 423 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 424 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 425 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 426 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 427 | CLANG_WARN_STRICT_PROTOTYPES = YES; 428 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 429 | CLANG_WARN_UNREACHABLE_CODE = YES; 430 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 431 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 432 | COPY_PHASE_STRIP = NO; 433 | ENABLE_NS_ASSERTIONS = NO; 434 | ENABLE_STRICT_OBJC_MSGSEND = YES; 435 | GCC_C_LANGUAGE_STANDARD = gnu99; 436 | GCC_NO_COMMON_BLOCKS = YES; 437 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 438 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 439 | GCC_WARN_UNDECLARED_SELECTOR = YES; 440 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 441 | GCC_WARN_UNUSED_FUNCTION = YES; 442 | GCC_WARN_UNUSED_VARIABLE = YES; 443 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 444 | MTL_ENABLE_DEBUG_INFO = NO; 445 | SDKROOT = iphoneos; 446 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 447 | TARGETED_DEVICE_FAMILY = "1,2"; 448 | VALIDATE_PRODUCT = YES; 449 | }; 450 | name = Release; 451 | }; 452 | 39BBCFCA1AD0CC7A00A450D2 /* Debug */ = { 453 | isa = XCBuildConfiguration; 454 | baseConfigurationReference = C6E2423BEEB12C12D184ACDD /* Pods-RichEditorViewSample.debug.xcconfig */; 455 | buildSettings = { 456 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 457 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 458 | CLANG_ENABLE_MODULES = YES; 459 | DEVELOPMENT_TEAM = 89MZF7SG7M; 460 | INFOPLIST_FILE = RichEditorViewSample/Info.plist; 461 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 462 | PRODUCT_BUNDLE_IDENTIFIER = com.cbess.RichEditorViewSample; 463 | PRODUCT_NAME = "$(TARGET_NAME)"; 464 | SWIFT_OBJC_BRIDGING_HEADER = "RichEditorViewSample/RichEditorViewSample-Bridging-Header.h"; 465 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 466 | SWIFT_VERSION = 5.0; 467 | }; 468 | name = Debug; 469 | }; 470 | 39BBCFCB1AD0CC7A00A450D2 /* Release */ = { 471 | isa = XCBuildConfiguration; 472 | baseConfigurationReference = 9AF21FE45EA87DF1498534F6 /* Pods-RichEditorViewSample.release.xcconfig */; 473 | buildSettings = { 474 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 475 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 476 | CLANG_ENABLE_MODULES = YES; 477 | DEVELOPMENT_TEAM = 89MZF7SG7M; 478 | INFOPLIST_FILE = RichEditorViewSample/Info.plist; 479 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 480 | PRODUCT_BUNDLE_IDENTIFIER = com.cbess.RichEditorViewSample; 481 | PRODUCT_NAME = "$(TARGET_NAME)"; 482 | SWIFT_OBJC_BRIDGING_HEADER = "RichEditorViewSample/RichEditorViewSample-Bridging-Header.h"; 483 | SWIFT_VERSION = 5.0; 484 | }; 485 | name = Release; 486 | }; 487 | 39BBCFCD1AD0CC7A00A450D2 /* Debug */ = { 488 | isa = XCBuildConfiguration; 489 | buildSettings = { 490 | BUNDLE_LOADER = "$(TEST_HOST)"; 491 | GCC_PREPROCESSOR_DEFINITIONS = ( 492 | "DEBUG=1", 493 | "$(inherited)", 494 | ); 495 | INFOPLIST_FILE = RichEditorViewSampleTests/Info.plist; 496 | PRODUCT_BUNDLE_IDENTIFIER = "com.cjwirth.$(PRODUCT_NAME:rfc1034identifier)"; 497 | PRODUCT_NAME = "$(TARGET_NAME)"; 498 | SWIFT_VERSION = 5.0; 499 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RichEditorViewSample.app/RichEditorViewSample"; 500 | }; 501 | name = Debug; 502 | }; 503 | 39BBCFCE1AD0CC7A00A450D2 /* Release */ = { 504 | isa = XCBuildConfiguration; 505 | buildSettings = { 506 | BUNDLE_LOADER = "$(TEST_HOST)"; 507 | INFOPLIST_FILE = RichEditorViewSampleTests/Info.plist; 508 | PRODUCT_BUNDLE_IDENTIFIER = "com.cjwirth.$(PRODUCT_NAME:rfc1034identifier)"; 509 | PRODUCT_NAME = "$(TARGET_NAME)"; 510 | SWIFT_VERSION = 5.0; 511 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RichEditorViewSample.app/RichEditorViewSample"; 512 | }; 513 | name = Release; 514 | }; 515 | /* End XCBuildConfiguration section */ 516 | 517 | /* Begin XCConfigurationList section */ 518 | 39BBCFA51AD0CC7A00A450D2 /* Build configuration list for PBXProject "RichEditorViewSample" */ = { 519 | isa = XCConfigurationList; 520 | buildConfigurations = ( 521 | 39BBCFC71AD0CC7A00A450D2 /* Debug */, 522 | 39BBCFC81AD0CC7A00A450D2 /* Release */, 523 | ); 524 | defaultConfigurationIsVisible = 0; 525 | defaultConfigurationName = Release; 526 | }; 527 | 39BBCFC91AD0CC7A00A450D2 /* Build configuration list for PBXNativeTarget "RichEditorViewSample" */ = { 528 | isa = XCConfigurationList; 529 | buildConfigurations = ( 530 | 39BBCFCA1AD0CC7A00A450D2 /* Debug */, 531 | 39BBCFCB1AD0CC7A00A450D2 /* Release */, 532 | ); 533 | defaultConfigurationIsVisible = 0; 534 | defaultConfigurationName = Release; 535 | }; 536 | 39BBCFCC1AD0CC7A00A450D2 /* Build configuration list for PBXNativeTarget "RichEditorViewSampleTests" */ = { 537 | isa = XCConfigurationList; 538 | buildConfigurations = ( 539 | 39BBCFCD1AD0CC7A00A450D2 /* Debug */, 540 | 39BBCFCE1AD0CC7A00A450D2 /* Release */, 541 | ); 542 | defaultConfigurationIsVisible = 0; 543 | defaultConfigurationName = Release; 544 | }; 545 | /* End XCConfigurationList section */ 546 | }; 547 | rootObject = 39BBCFA21AD0CC7A00A450D2 /* Project object */; 548 | } 549 | -------------------------------------------------------------------------------- /RichEditorViewSample/RichEditorViewSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | -------------------------------------------------------------------------------- /RichEditorViewSample/RichEditorViewSample.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 |6 | 7 |4 | 11 | -------------------------------------------------------------------------------- /RichEditorViewSample/RichEditorViewSample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // RichEditorViewSample 4 | // 5 | // Created by Caesar Wirth on 4/5/15. 6 | // Copyright (c) 2015 Caesar Wirth. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // 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. 24 | // 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. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // 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. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // 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. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // 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. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /RichEditorViewSample/RichEditorViewSample/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 |6 | 7 |9 | 10 |3 | 39 | -------------------------------------------------------------------------------- /RichEditorViewSample/RichEditorViewSample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 |4 | 5 | 9 |6 | 7 | 8 | 10 | 33 |11 | 12 | 13 | 32 |14 | 15 | 16 | 22 | 23 |24 | 25 | 28 |26 | 27 | 29 | 30 | 31 | 34 | 38 |35 | 37 |36 | 3 | 101 | -------------------------------------------------------------------------------- /RichEditorViewSample/RichEditorViewSample/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" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /RichEditorViewSample/RichEditorViewSample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 |4 | 5 | 10 |6 | 7 | 8 | 9 | 11 | 12 | 89 |13 | 27 | 28 |14 | 25 |15 | 23 |16 | 19 |17 | 18 | 20 | 22 |21 | 24 | 26 | 29 | 88 |30 | 86 |31 | 84 |32 | 35 |33 | 34 | 36 | 65 |37 | 38 | 39 | 53 |40 | 45 |41 | 42 | 44 |43 | 46 | 52 |47 | 48 | 49 | 50 | 51 | 54 | 55 | 64 |56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 66 | 78 |67 | 76 | 77 |79 | 83 |80 | 81 | 82 | 85 | 87 | 90 | 100 |91 | 93 |92 | 94 | 96 |95 | 97 | 99 |98 | 4 | 48 | -------------------------------------------------------------------------------- /RichEditorViewSample/RichEditorViewSample/KeyboardManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardManager.swift 3 | // RichEditorViewSample 4 | // 5 | // Created by Caesar Wirth on 4/5/15. 6 | // Copyright (c) 2015 Caesar Wirth. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RichEditorView 11 | 12 | /** 13 | KeyboardManager is a class that takes care of showing and hiding the RichEditorToolbar when the keyboard is shown. 14 | As opposed to having this logic in multiple places, it is encapsulated in here. All that needs to change is the parent view. 15 | */ 16 | class KeyboardManager: NSObject { 17 | 18 | /** 19 | The parent view that the toolbar should be added to. 20 | Should normally be the top-level view of a UIViewController 21 | */ 22 | weak var view: UIView? 23 | 24 | /** 25 | The toolbar that will be shown and hidden. 26 | */ 27 | var toolbar: RichEditorToolbar 28 | 29 | init(view: UIView) { 30 | self.view = view 31 | toolbar = RichEditorToolbar(frame: CGRect(x: 0, y: view.bounds.height, width: view.bounds.width, height: 44)) 32 | // toolbar.options = RichEditorOptions.all() 33 | } 34 | 35 | /** 36 | Starts monitoring for keyboard notifications in order to show/hide the toolbar 37 | */ 38 | func beginMonitoring() { 39 | let sel = #selector(keyboardWillShowOrHide(_:)) 40 | NotificationCenter.default.addObserver(self, selector: sel, name: UIResponder.keyboardWillShowNotification, object: nil) 41 | NotificationCenter.default.addObserver(self, selector: sel, name: UIResponder.keyboardWillHideNotification, object: nil) 42 | } 43 | 44 | /** 45 | Stops monitoring for keyboard notifications 46 | */ 47 | func stopMonitoring() { 48 | NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) 49 | NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) 50 | } 51 | 52 | /** 53 | Called when a keyboard notification is recieved. Takes are of handling the showing or hiding of the toolbar 54 | */ 55 | @objc func keyboardWillShowOrHide(_ notification: Notification) { 56 | let info = notification.userInfo ?? [:] 57 | let duration = TimeInterval((info[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.floatValue ?? 0.25) 58 | let curve = UInt((info[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber)?.uintValue ?? 0) 59 | let options = UIView.AnimationOptions(rawValue: curve) 60 | let keyboardRect = (info[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue ?? CGRect.zero 61 | 62 | if notification.name == UIResponder.keyboardWillShowNotification { 63 | self.view?.addSubview(self.toolbar) 64 | UIView.animate(withDuration: duration, delay: 0, options: options, animations: { 65 | if let view = self.view { 66 | self.toolbar.frame.origin.y = view.frame.height - (keyboardRect.height + self.toolbar.frame.height) 67 | } 68 | }, completion: nil) 69 | 70 | 71 | } else if notification.name == UIResponder.keyboardWillHideNotification { 72 | UIView.animate(withDuration: duration, delay: 0, options: options, animations: { 73 | if let view = self.view { 74 | self.toolbar.frame.origin.y = view.frame.height 75 | } 76 | }, completion: nil) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /RichEditorViewSample/RichEditorViewSample/RichEditorViewSample-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | -------------------------------------------------------------------------------- /RichEditorViewSample/RichEditorViewSample/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // RichEditorViewSample 4 | // 5 | // Created by Caesar Wirth on 4/5/15. 6 | // Copyright (c) 2015 Caesar Wirth. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RichEditorView 11 | import SafariServices 12 | 13 | class ViewController: UIViewController { 14 | @IBOutlet var editorView: RichEditorView! 15 | @IBOutlet var htmlTextView: UITextView! 16 | var isTextColor = true 17 | @IBOutlet weak var editButton: UIBarButtonItem! 18 | 19 | lazy var toolbar: RichEditorToolbar = { 20 | let toolbar = RichEditorToolbar(frame: CGRect(x: 0, y: 0, width: self.view.bounds.width, height: 44)) 21 | // let options: [RichEditorDefaultOption] = [ 22 | // .bold, .italic, .underline, 23 | // .unorderedList, .orderedList, 24 | // .indent, .outdent, 25 | // .textColor, .textBackgroundColor, 26 | // .undo, .redo, 27 | // ] 28 | toolbar.options = RichEditorDefaultOption.all 29 | return toolbar 30 | }() 31 | 32 | override func viewDidLoad() { 33 | super.viewDidLoad() 34 | 35 | editorView.delegate = self 36 | editorView.inputAccessoryView = toolbar 37 | editorView.editingEnabled = false 38 | editorView.placeholder = "Edit here" 39 | let html = "Jesus is God. He saves by grace through faith alone. Soli Deo gloria! perfectGod.com" 40 | editorView.reloadHTML(with: html) 41 | 42 | toolbar.delegate = self 43 | toolbar.editor = editorView 44 | 45 | // This will create a custom action that clears all the input text when it is pressed 46 | let item = RichEditorOptionItem(title: "Clear") { (toolbar, sender) in 47 | toolbar.editor?.html = "" 48 | } 49 | 50 | var options = toolbar.options 51 | options.append(item) 52 | toolbar.options = options 53 | } 54 | 55 | @IBAction func changeEditState(_ sender: Any) { 56 | editorView.editingEnabled.toggle() 57 | 58 | let title: String 59 | if editorView.editingEnabled { 60 | _ = editorView.becomeFirstResponder() 61 | title = "Done" 62 | } else { 63 | _ = editorView.resignFirstResponder() 64 | title = "Edit" 65 | } 66 | 67 | self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: title, style: .done, target: self, action: #selector(changeEditState(_:))) 68 | } 69 | 70 | } 71 | 72 | extension ViewController: RichEditorDelegate { 73 | func richEditor(_ editor: RichEditorView, heightDidChange height: Int) { } 74 | 75 | func richEditor(_ editor: RichEditorView, contentDidChange content: String) { 76 | if content.isEmpty { 77 | htmlTextView.text = "HTML Preview" 78 | } else { 79 | htmlTextView.text = content 80 | } 81 | } 82 | 83 | func richEditorTookFocus(_ editor: RichEditorView) { } 84 | 85 | func richEditorLostFocus(_ editor: RichEditorView) { } 86 | 87 | func richEditorDidLoad(_ editor: RichEditorView) { } 88 | 89 | func richEditor(_ editor: RichEditorView, shouldInteractWith url: URL) -> Bool { return true } 90 | 91 | func richEditor(_ editor: RichEditorView, interactWith url: URL) { 92 | let configuration = SFSafariViewController.Configuration() 93 | configuration.entersReaderIfAvailable = true 94 | let safari = SFSafariViewController(url: url, configuration: configuration) 95 | present(safari, animated: true) 96 | } 97 | 98 | func richEditor(_ editor: RichEditorView, handleCustomAction content: String) { } 99 | } 100 | 101 | extension ViewController: RichEditorToolbarDelegate, UIColorPickerViewControllerDelegate { 102 | private func presentColorPicker(title: String?, color: UIColor?) { 103 | let picker = UIColorPickerViewController() 104 | picker.supportsAlpha = false 105 | picker.delegate = self 106 | picker.title = title 107 | if let color = color { 108 | picker.selectedColor = color 109 | } 110 | 111 | present(picker, animated: true, completion: nil) 112 | } 113 | 114 | private func getRGBA(from color: UIColor) -> [CGFloat] { 115 | var R: CGFloat = 0 116 | var G: CGFloat = 0 117 | var B: CGFloat = 0 118 | var A: CGFloat = 0 119 | 120 | color.getRed(&R, green: &G, blue: &B, alpha: &A) 121 | 122 | return [R, G, B, A] 123 | } 124 | 125 | private func isBlackOrWhite(_ color: UIColor) -> Bool { 126 | let RGBA = getRGBA(from: color) 127 | let isBlack = RGBA[0] < 0.09 && RGBA[1] < 0.09 && RGBA[2] < 0.09 128 | let isWhite = RGBA[0] > 0.91 && RGBA[1] > 0.91 && RGBA[2] > 0.91 129 | 130 | return isBlack || isWhite 131 | } 132 | 133 | func richEditorToolbarChangeTextColor(_ toolbar: RichEditorToolbar, sender: AnyObject) { 134 | isTextColor = true 135 | presentColorPicker(title: "Text Color", color: .black) 136 | } 137 | 138 | func richEditorToolbarChangeBackgroundColor(_ toolbar: RichEditorToolbar, sender: AnyObject) { 139 | isTextColor = false 140 | presentColorPicker(title: "Background Color", color: .white) 141 | } 142 | 143 | func richEditorToolbarInsertImage(_ toolbar: RichEditorToolbar) { 144 | toolbar.editor?.insertImage("https://avatars2.githubusercontent.com/u/10981?s=60", alt: "Gravatar") 145 | } 146 | 147 | func richEditorToolbarInsertLink(_ toolbar: RichEditorToolbar) { 148 | // Can only add links to selected text, so make sure there is a range selection first 149 | toolbar.editor?.hasRangeSelection(handler: { (hasSelection) in 150 | if hasSelection { 151 | self.toolbar.editor?.insertLink("https://github.com/cbess/RichEditorView", title: "GitHub Link") 152 | } 153 | }) 154 | } 155 | 156 | func colorPickerViewControllerDidSelectColor(_ viewController: UIColorPickerViewController) { 157 | var color: UIColor? = viewController.selectedColor 158 | 159 | // don't allow black or white color changes 160 | if isBlackOrWhite(viewController.selectedColor) { 161 | color = nil 162 | } 163 | 164 | if isTextColor { 165 | toolbar.editor?.setTextColor(color) 166 | } else { 167 | toolbar.editor?.setTextBackgroundColor(color) 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /RichEditorViewSample/RichEditorViewSampleTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 |5 | 47 |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 | 33 |armv7 32 |UISupportedInterfaceOrientations 34 |35 | 39 |UIInterfaceOrientationPortrait 36 |UIInterfaceOrientationLandscapeLeft 37 |UIInterfaceOrientationLandscapeRight 38 |UISupportedInterfaceOrientations~ipad 40 |41 | 46 |UIInterfaceOrientationPortrait 42 |UIInterfaceOrientationPortraitUpsideDown 43 |UIInterfaceOrientationLandscapeLeft 44 |UIInterfaceOrientationLandscapeRight 45 |4 | 25 | -------------------------------------------------------------------------------- /RichEditorViewSample/RichEditorViewSampleTests/RichEditorViewSampleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RichEditorViewSampleTests.swift 3 | // RichEditorViewSampleTests 4 | // 5 | // Created by Caesar Wirth on 4/5/15. 6 | // Copyright (c) 2015 Caesar Wirth. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | 12 | class RichEditorViewSampleTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | XCTAssert(true, "Pass") 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure() { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /RichEditorViewTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 |5 | 24 |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 |4 | 25 | -------------------------------------------------------------------------------- /RichEditorViewTests/RichEditorViewTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RichEditorViewTests.swift 3 | // RichEditorViewTests 4 | // 5 | // Created by Caesar Wirth on 4/7/15. 6 | // 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | 12 | class RichEditorViewTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | XCTAssert(true, "Pass") 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure() { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /art/Demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/art/Demo.gif -------------------------------------------------------------------------------- /art/Toolbar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbess/RichEditorView/27c47ff4b63b979b8d1b79bb239d2ddf011f016f/art/Toolbar.gif --------------------------------------------------------------------------------5 | 24 |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 |