Giter Site home page Giter Site logo

rajdeep / proton Goto Github PK

View Code? Open in Web Editor NEW
1.2K 29.0 81.0 81.81 MB

Purely native and extensible rich text editor for iOS and macOS Catalyst apps

License: Other

Swift 98.31% Objective-C 1.69%
editor plugin-architecture extensible textkit wysiwyg-editor native rich-text-editor richtexteditor swift maccatalyst

proton's Introduction

Proton logo

Note: While Proton is already a very powerful and flexible framework, it is still in early stages of development. The APIs and public interfaces are still undergoing revisions and may introduce breaking changes with every version bump before reaching stable version 1.0.0.

Build codecov License

Proton is a simple library that allows you to extend the behavior of a textview to add rich content that you always wanted. It provides simple API that allows you to extend the textView to include complex content like nested textViews or for that matter, any other UIView. In the simplest terms - It's what you always wanted UITextView to be.

Proton is designed keeping the following requirements in mind:

  • Be a standalone component with nothing that is specific to anything that is required in complex Text Editor. At it's most basic form, it should be able to be used as a UITextView and in most complex form, it should be able to provide rich text editing capabilities which are beyond just text formatting.
  • Should be extensible to support adding any view as content in the Editor such that it flows with the text.
  • Resizing of content views should automatically resize the containing Editor and support this to nth nesting level.
  • Should support extending the appearance of text as the content is typed - for e.g. changing text as it is typed using mark-up syntax and yet, not be aware of any of these requirements directly.
  • Should allow for working on multiple editors through the same toolbar based on where the focus is, and yet not be aware of the toolbar itself.
  • Respect the bounds of the container i.e. resize to change bounds when the device orientation changes.
  • Support a default font and styling like alignment and head indentation.
  • And of course, support all this on macOS Catalyst as well with almost no additional effort.

Core Concepts

At it's core, Proton constitutes of following key components:

  • EditorView: A substitute for UITextView that can be extended to add custom views including other EditorViews.
  • TextProcessor: Allows you to inject a behavior that is invoked as you type text in the EditorView. This can be used to change text, add/remove attributes like color or replace the added text with an entirely different text/view. For e.g. as you type markup syntax, you can convert the markup text into a formatted text by adding corresponding behavior to the TextProcessor.
  • EditorCommand: Allows you to add a behavior that can be invoked on demand on the given EditorView. For e.g. selecting some text and making it bold.
  • Attachment: A container capable of hosting a custom view including another EditorView. Attachment is a supercharged NSTextAttachment that can have automatic constraints applied on it to size it in various configurations like matching content, range of width, fixed width and so on. It also has helper functions to get it's range in it's container as well as to remove itself from the container.

A practical use case

The power of EditorView to host rich content is made possible by the use of Attachment which allows hosting any UIView in the EditorView. This is further enhanced by use of TextProcessor and EditorCommand to add interactive behavior to the editing experience.

Let's take an example of a Panel and see how that can be created in the EditorView. Following are the key requirements for a Panel:

  1. A text block that is indented and has a custom UI besides the Editor.
  2. Change height based on the content being typed.
  3. Have a different font color than the main text.
  4. Able to be inserted using a button.
  5. Able to be inserted by selecting text and clicking a button.
  6. Able to be inserted in a given Editor by use of >> char.
  7. Nice to have: delete using backspace key when empty similar to a Blockquote.

Panel view

  1. The first thing that is required is to create a view that represents the Panel. Once we have created this view, we can add it to an attachment and insert it in the EditorView.

    extension EditorContent.Name {
        static let panel = EditorContent.Name("panel")
    }
    class PanelView: UIView, BlockContent, EditorContentView {
        let container = UIView()
        let editor: EditorView
        let iconView = UIImageView()    
        var name: EditorContent.Name {
            return .panel
        }   
        override init(frame: CGRect) {
            self.editor = EditorView(frame: frame)
            super.init(frame: frame)    
            setup()
        }   
        var textColor: UIColor {
            get { editor.textColor }
            set { editor.textColor = newValue }
        }   
        override var backgroundColor: UIColor? {
            get { container.backgroundColor }
            set {
                container.backgroundColor = newValue
                editor.backgroundColor = newValue
            }
        }   
        private func setup() {
            // setup view by creating required constraints
        }
    }
  2. As the Panel contains an Editor inside itself, the height will automatically change based on the content as it is typed in. To restrict the height to a given maximum value, an absolute size or autolayout constraint may be used.

  3. Using the textColor property, the default font color may be changed.

  4. For the ability to add Panel to the Editor using a button, we can make use of EditorCommand. A Command can be executed on a given EditorView or via CommandExecutor that automatically takes care of executing the command on the focussed EditorView. To insert an EditorView inside another, we need to first create an Attachment and then used a Command to add to the desired position:

    class PanelAttachment: Attachment {
        var view: PanelView 
        init(frame: CGRect) {
            view = PanelView(frame: frame)
            super.init(view, size: .fullWidth)
            view.delegate = self
            view.boundsObserver = self
        }   
        var attributedText: NSAttributedString {
            get { view.attributedText }
            set { view.attributedText = newValue }
        }   
    }   
    class PanelCommand: EditorCommand {
        func execute(on editor: EditorView) {
            let selectedText = editor.selectedText  
            let attachment = PanelAttachment(frame: .zero)
            attachment.selectBeforeDelete = true
            editor.insertAttachment(in: editor.selectedRange, attachment: attachment)   
            let panel = attachment.view
            panel.editor.maxHeight = 300
            panel.editor.replaceCharacters(in: .zero, with: selectedText)
            panel.editor.selectedRange = panel.editor.textEndRange
        }
    }
  5. The code in PanelCommand.execute reads the selectedText from editor and sets it back in panel.editor. This makes it possible to take the selected text from main editor, wrap it in a panel and then insert the panel in the main editor replacing the selected text.

  6. To allow insertion of a Panel using a shortcut text input instead of clicking a button, you can use a TextProcessor:

    class PanelTextProcessor: TextProcessing {  
     private let trigger = ">> "
     var name: String {
         return "PanelTextProcessor"
     }  
     var priority: TextProcessingPriority {
         return .medium
     }  
     func process(editor: EditorView, range editedRange: NSRange, changeInLength delta: Int, processed: inout Bool) {
         let line = editor.currentLine
         guard line.text.string == trigger else {
             return
         }
         let attachment = PanelAttachment(frame: .zero)
         attachment.selectBeforeDelete = true        
         editor.insertAttachment(in: line.range, attachment: attachment)
     }
    
  7. For a requirement like deleting the Panel when backspace is tapped at index 0 on an empty Panel, EdtiorViewDelegate may be utilized:

    extension PanelAttachment: PanelViewDelegate {
    
    func panel(_ panel: PanelView, didReceiveKey key: EditorKey, at range: NSRange, handled: inout Bool) {
        if key == .backspace, range == .zero, panel.editor.attributedText.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
            removeFromContainer()
            handled = true
            }
        }
    }    

    In the code above, PanelViewDelegate is acting as a passthrough for EditorViewDelegate for the Editor inside the PanelView.

    Checkout the complete code in the ExamplesApp.

Example usages

  1. Changing text as it is typed using custom TextProcessor:

    Markup text processor
  2. Adding attributes as it is typed using custom TextProcessor:

    Mentions text processor
  3. Nested editors

    Nested editors
  4. Panel from existing text:

    Panel from text
  5. Relaying attributes to editor contained in an attachment:

    Relay attributes
  6. Highlighting using custom command in Editor:

    Highlight in Renderer
  7. Find text and scroll in Editor:

    Find in Renderer

Basic SWIFT UI integration example

Proton's Editor may be used with SwiftUI same was as a standard UIKit component. SwiftUI support is provided as is, and will be refined on in future.

struct ProtonView: View {
    
    @Binding var attributedText: NSAttributedString
    @State var height: CGFloat = 0
    var body: some View {
        ProtonWrapperView(attributedText: $attributedText) { view in
            let height = view.systemLayoutSizeFitting(UIView.layoutFittingExpandedSize).height

            self.height = height
        }
        .frame(height: height)
    }
}

struct ProtonWrapperView: UIViewRepresentable {
    
    @Binding var attributedText: NSAttributedString
    let textDidChange: (EditorView) -> Void

    
    func makeUIView(context: Context) -> EditorView {
        let view = EditorView()
        view.becomeFirstResponder()
        view.attributedText = attributedText
        view.isScrollEnabled = false
        view.setContentCompressionResistancePriority(.required, for: .vertical)

        view.heightAnchor.constraint(greaterThanOrEqualToConstant: 300).isActive = true
        
        DispatchQueue.main.async {
            self.textDidChange(view)
        }
        
        EditorViewContext.shared.delegate = context.coordinator

        return view
    }
    
    func updateUIView(_ view: EditorView, context: Context) {

    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, EditorViewDelegate {
        var parent: ProtonWrapperView
        
        init(_ parent: ProtonWrapperView) {
            self.parent = parent
        }
        
        func editor(_ editor: EditorView, didChangeTextAt range: NSRange) {
            editor.isScrollEnabled = false
            parent.attributedText = editor.attributedText
            DispatchQueue.main.async {
                self.parent.textDidChange(editor)
            }
        }
    }
}

Learn more

  • Proton API reference is available here.
  • For sample code, including the ones for examples shown above, please refer to the Example app.

Questions and feature requests

Feel free to create issues in github should you have any questions or feature requests. While Proton is created as a side project, I'll endeavour to respond to your issues at earliest possible.

License

Proton is released under the Apache 2.0 license. Please see LICENSE for details.

proton's People

Contributors

erudel avatar moriquendi avatar rajdeep avatar rjchatfield avatar tomaslinhart avatar vmanot avatar vox-humana avatar yox89 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

proton's Issues

NSRangeException when using lists

Hi, this is a crash that is 100% reproducible in the sample app in the "Commands" example.

  1. Tap "List" command.
  2. Tap enter.
  3. Tap "List" command again
  4. Tap enter again
  5. Crash
2021-05-14 08:34:40.949847+0200 ExampleApp[4044:79319] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[NSBigMutableString _getBlockStart:end:contentsEnd:forRange:stopAtLineSeparators:]: Range {4, 0} out of bounds; string length 3'
*** First throw call stack:
(
	0   CoreFoundation                      0x00007fff20422fba __exceptionPreprocess + 242
	1   libobjc.A.dylib                     0x00007fff20193ff5 objc_exception_throw + 48
	2   Foundation                          0x00007fff2084c066 -[NSString _getBlockStart:end:contentsEnd:forRange:stopAtLineSeparators:] + 900
	3   Foundation                          0x00007fff2084c66a -[NSString paragraphRangeForRange:] + 75
	4   UIKitCore                           0x00007fff24a224c0 __58-[UITextInputController setBaseWritingDirection:forRange:]_block_invoke + 86
	5   UIFoundation                        0x00007fff23a5584c -[NSTextStorage coordinateEditing:] + 35
	6   UIKitCore                           0x00007fff24a22361 -[UITextInputController setBaseWritingDirection:forRange:] + 152
	7   UIKitCore                           0x00007fff24a42023 -[UITextView setBaseWritingDirection:forRange:] + 68
	8   UIKitCore                           0x00007fff244bab9b -[UIKeyboardImpl(UIKitInternal) setInitialDirection] + 294
	9   UIKitCore                           0x00007fff244a5ffd -[UIKeyboardImpl completeAddInputString:generateCandidates:] + 124
	10  UIKitCore                           0x00007fff244a5f44 __100-[UIKeyboardImpl addWordTerminator:afterSpace:afterAcceptingCandidate:elapsedTime:executionContext:]_block_invoke + 177
	11  UIKitCore                           0x00007fff244cba5d -[UIKeyboardTaskExecutionContext returnExecutionToParentWithInfo:] + 109
	12  UIKitCore                           0x00007fff2449a41c __55-[UIKeyboardImpl handleKeyboardInput:executionContext:]_block_invoke_2 + 799
	13  UIKitCore                           0x00007fff244cd2a1 -[UIKeyboardTaskEntry execute:] + 147
	14  UIKitCore                           0x00007fff244cbf35 -[UIKeyboardTaskQueue continueExecutionOnMainThread] + 310
	15  Foundation                          0x00007fff2085cd08 __NSThreadPerformPerform + 204
	16  CoreFoundation                      0x00007fff20390ede __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
	17  CoreFoundation                      0x00007fff20390dd6 __CFRunLoopDoSource0 + 180
	18  CoreFoundation                      0x00007fff2039029e __CFRunLoopDoSources0 + 242
	19  CoreFoundation                      0x00007fff2038a9f7 __CFRunLoopRun + 875
	20  CoreFoundation                      0x00007fff2038a1a7 CFRunLoopRunSpecific + 567
	21  GraphicsServices                    0x00007fff2b874d85 GSEventRunModal + 139
	22  UIKitCore                           0x00007fff246c14df -[UIApplication _run] + 912
	23  UIKitCore                           0x00007fff246c639c UIApplicationMain + 101
	24  ExampleApp                          0x000000010e19c8cb main + 75
	25  libdyld.dylib                       0x00007fff2025abbd start + 1
	26  ???                                 0x0000000000000001 0x0 + 1
)
libc++abi: terminating with uncaught exception of type NSException

Encode/Decode Lists

Hello! Congratulations and thanks for this great UITextView extension.

It seems that lists are not encoded/decoded yet, do you think that will be supported soon? ;)

Best regards,
Franky

Can I register a type of command, but not an instance?

I'd like to filter some types of commands for some editors. registeredCommands seems the right place to do it.
However, EditorView::isCommandRegistered checks for instance equality but not for type equality. So, for example, if I don't have exact instance of command (mention with some userId is a good example) I can't register it and then it won't be executed.
Theoretically, I can skip this check by calling directly command.execute(on editor: EditorView), but I don't know active editor. So if I need to execute command from keyboard shortcut or toolbar button I need to find active EditorView first, which is quite hard (see #27)

Xcode 13

Proton fails to compile with Xcode 13.0 beta. Error: CompileSwiftSources failed with nonzero exit code

Cursor is bigger than font line height when editor has spacing

When TextView has paragraphSpacingBefore or paragraphSpacing cursor covers this spaces and this seems not right especially when you have line selection.

Screenshots from Example app with these properties are set in paragraphStyle
image
image
PR is coming...

Question: How to get current active `EditorView`?

One of the use cases: show typeahead picker while typing following the cursor in the current editor. It should work in nested editors as well.

EditorCommandExecutor uses internal RichTextEditorContext to get activeTextView. However, integrators don't have access to EditorViewContext internal properties.

The only way for now to get active editor I found is somehow detect active firstResponder and try to cast its superview to EditorView. Which is obviously a) not very handy b) exposes internal details of EditorView

Range exception on deleting panel attachment from the inside

Steps:

  • Open ExampleApp
  • Insert panel
  • Remove empty line after the inserted panel
  • Put cursor back inside the panel
  • Press backspace once to delete the panel
  • Press backspace second time to trigger the crash

I suspect that before calling deleteBackward in parent RichTextView selectedRange hasn't been update and instead of empty range from 0 position contains empty range from 1 position. Not sure what's the right way to update this property 🤔

EditorViewDelegate didChangeTextAt issue

My app needs to respond to text changes in order to update its model data. I have implemented func editor(_ editor: EditorView, didChangeTextAt range: NSRange) to do this. If I enter some text, place the caret anywhere but the end, then enter a character, the character goes in the correct place but the caret is moved to the end. If I remove the delegate method it works OK (but obviously doesn't update the model). My delegate method does not invoke any Proton methods.

I have tried tracing through the code but can't pinpoint why this is happening, but it looks like it has to do with the way delegate methods are handled.

Problem with Strikethrough when changing attributedText

Setting attributedText containing an attribute containing strikethrough results in strikethrough applied to all attributes. Setting the text with an attribute containing underline results in strikethrough=0 appearing in the other attributes. Maybe the fact that both underline and strikethrough attributes use the same value type - namely NSUnderlineStyle.single.rawvalue?

Here are dumps taken from the setter at line 381 of EditorView.attributed text.

First the underline:

(lldb) po newValue
This {
EasiStyle = "Easiwriter.StyleIdentifier(name: Optional("title1"), id: Optional("401D4EBD-B20F-4DE8-8A7F-A32A77F73CB4"))";
NSColor = "<UIDynamicSystemColor: 0x600001eb57c0; name = labelColor>";
NSFont = "<UICTFont: 0x7fbb4fd6f4e0> font-family: "Helvetica Neue"; font-weight: bold; font-style: normal; font-size: 28.00pt";
NSParagraphStyle = "Alignment 0, LineSpacing 1, ParagraphSpacing 0, ParagraphSpacingBefore 10, HeadIndent 0, TailIndent 0, FirstLineHeadIndent 0, LineHeight 0/0, LineHeightMultiple 1, LineBreakMode 0, Tabs (\n 28L,\n 56L,\n 84L,\n 112L,\n 140L,\n 168L,\n 196L,\n 224L,\n 252L,\n 280L,\n 308L,\n 336L\n), DefaultTabInterval 0, Blocks (null), Lists (null), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (\n) ListIntentOrdinal 0 CodeBlockIntentLanguageHint '(null)'";
}test{
EasiStyle = "Easiwriter.StyleIdentifier(name: Optional("underline"), id: Optional("16C65DC4-94FB-4851-96BE-8FE4FF04EC72"))";
NSColor = "<UIDynamicSystemColor: 0x600001eb57c0; name = labelColor>";
NSFont = "<UICTFont: 0x7fbb4fd6f4e0> font-family: "Helvetica Neue"; font-weight: bold; font-style: normal; font-size: 28.00pt";
NSParagraphStyle = "Alignment 0, LineSpacing 1, ParagraphSpacing 0, ParagraphSpacingBefore 10, HeadIndent 0, TailIndent 0, FirstLineHeadIndent 0, LineHeight 0/0, LineHeightMultiple 1, LineBreakMode 0, Tabs (\n 28L,\n 56L,\n 84L,\n 112L,\n 140L,\n 168L,\n 196L,\n 224L,\n 252L,\n 280L,\n 308L,\n 336L\n), DefaultTabInterval 0, Blocks (null), Lists (null), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (\n) ListIntentOrdinal 0 CodeBlockIntentLanguageHint '(null)'";
NSUnderline = 1;
}

(lldb) po richTextView.attributedText
▿ Optional

  • some : This {
    EasiStyle = "Easiwriter.StyleIdentifier(name: Optional("largeTitle"), id: Optional("FCDB90A0-557E-42D9-B64A-E18F702DC66A"))";
    NSColor = "<UIDynamicSystemColor: 0x600001eb57c0; name = labelColor>";
    NSFont = "<UICTFont: 0x7fbb3fc3f710> font-family: "Helvetica Neue"; font-weight: bold; font-style: normal; font-size: 34.00pt";
    NSParagraphStyle = "Alignment 1, LineSpacing 1, ParagraphSpacing 0, ParagraphSpacingBefore 10, HeadIndent 0, TailIndent 0, FirstLineHeadIndent 0, LineHeight 0/0, LineHeightMultiple 1, LineBreakMode 0, Tabs (\n 28L,\n 56L,\n 84L,\n 112L,\n 140L,\n 168L,\n 196L,\n 224L,\n 252L,\n 280L,\n 308L,\n 336L\n), DefaultTabInterval 0, Blocks (null), Lists (null), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (\n) ListIntentOrdinal 0 CodeBlockIntentLanguageHint '(null)'";
    NSStrikethrough = 0;
    }test{
    EasiStyle = "Easiwriter.StyleIdentifier(name: Optional("underline"), id: Optional("16C65DC4-94FB-4851-96BE-8FE4FF04EC72"))";
    NSColor = "<UIDynamicSystemColor: 0x600001eb57c0; name = labelColor>";
    NSFont = "<UICTFont: 0x7fbb3fc3f710> font-family: "Helvetica Neue"; font-weight: bold; font-style: normal; font-size: 34.00pt";
    NSParagraphStyle = "Alignment 1, LineSpacing 1, ParagraphSpacing 0, ParagraphSpacingBefore 10, HeadIndent 0, TailIndent 0, FirstLineHeadIndent 0, LineHeight 0/0, LineHeightMultiple 1, LineBreakMode 0, Tabs (\n 28L,\n 56L,\n 84L,\n 112L,\n 140L,\n 168L,\n 196L,\n 224L,\n 252L,\n 280L,\n 308L,\n 336L\n), DefaultTabInterval 0, Blocks (null), Lists (null), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (\n) ListIntentOrdinal 0 CodeBlockIntentLanguageHint '(null)'";
    NSUnderline = 1;
    }

Now the strikethrough case:

(lldb) po newValue
This {
EasiStyle = "Easiwriter.StyleIdentifier(name: Optional("title1"), id: Optional("401D4EBD-B20F-4DE8-8A7F-A32A77F73CB4"))";
NSColor = "<UIDynamicSystemColor: 0x600001eb57c0; name = labelColor>";
NSFont = "<UICTFont: 0x7fbb4fd6f4e0> font-family: "Helvetica Neue"; font-weight: bold; font-style: normal; font-size: 28.00pt";
NSParagraphStyle = "Alignment 0, LineSpacing 1, ParagraphSpacing 0, ParagraphSpacingBefore 10, HeadIndent 0, TailIndent 0, FirstLineHeadIndent 0, LineHeight 0/0, LineHeightMultiple 1, LineBreakMode 0, Tabs (\n 28L,\n 56L,\n 84L,\n 112L,\n 140L,\n 168L,\n 196L,\n 224L,\n 252L,\n 280L,\n 308L,\n 336L\n), DefaultTabInterval 0, Blocks (null), Lists (null), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (\n) ListIntentOrdinal 0 CodeBlockIntentLanguageHint '(null)'";
}test{
EasiStyle = "Easiwriter.StyleIdentifier(name: Optional("strikethrough"), id: Optional("3B68996A-E9E7-497D-B20C-B4A1DA843DE3"))";
NSColor = "<UIDynamicSystemColor: 0x600001eb57c0; name = labelColor>";
NSFont = "<UICTFont: 0x7fbb4fd6f4e0> font-family: "Helvetica Neue"; font-weight: bold; font-style: normal; font-size: 28.00pt";
NSParagraphStyle = "Alignment 0, LineSpacing 1, ParagraphSpacing 0, ParagraphSpacingBefore 10, HeadIndent 0, TailIndent 0, FirstLineHeadIndent 0, LineHeight 0/0, LineHeightMultiple 1, LineBreakMode 0, Tabs (\n 28L,\n 56L,\n 84L,\n 112L,\n 140L,\n 168L,\n 196L,\n 224L,\n 252L,\n 280L,\n 308L,\n 336L\n), DefaultTabInterval 0, Blocks (null), Lists (null), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (\n) ListIntentOrdinal 0 CodeBlockIntentLanguageHint '(null)'";
NSStrikethrough = 1;
}

(lldb) po richTextView.attributedText
▿ Optional

  • some : This {
    EasiStyle = "Easiwriter.StyleIdentifier(name: Optional("title1"), id: Optional("401D4EBD-B20F-4DE8-8A7F-A32A77F73CB4"))";
    NSColor = "<UIDynamicSystemColor: 0x600001eb57c0; name = labelColor>";
    NSFont = "<UICTFont: 0x7fbb4fd6f4e0> font-family: "Helvetica Neue"; font-weight: bold; font-style: normal; font-size: 28.00pt";
    NSParagraphStyle = "Alignment 0, LineSpacing 1, ParagraphSpacing 0, ParagraphSpacingBefore 10, HeadIndent 0, TailIndent 0, FirstLineHeadIndent 0, LineHeight 0/0, LineHeightMultiple 1, LineBreakMode 0, Tabs (\n 28L,\n 56L,\n 84L,\n 112L,\n 140L,\n 168L,\n 196L,\n 224L,\n 252L,\n 280L,\n 308L,\n 336L\n), DefaultTabInterval 0, Blocks (null), Lists (null), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (\n) ListIntentOrdinal 0 CodeBlockIntentLanguageHint '(null)'";
    NSStrikethrough = 1;
    }test{
    EasiStyle = "Easiwriter.StyleIdentifier(name: Optional("strikethrough"), id: Optional("3B68996A-E9E7-497D-B20C-B4A1DA843DE3"))";
    NSColor = "<UIDynamicSystemColor: 0x600001eb57c0; name = labelColor>";
    NSFont = "<UICTFont: 0x7fbb4fd6f4e0> font-family: "Helvetica Neue"; font-weight: bold; font-style: normal; font-size: 28.00pt";
    NSParagraphStyle = "Alignment 0, LineSpacing 1, ParagraphSpacing 0, ParagraphSpacingBefore 10, HeadIndent 0, TailIndent 0, FirstLineHeadIndent 0, LineHeight 0/0, LineHeightMultiple 1, LineBreakMode 0, Tabs (\n 28L,\n 56L,\n 84L,\n 112L,\n 140L,\n 168L,\n 196L,\n 224L,\n 252L,\n 280L,\n 308L,\n 336L\n), DefaultTabInterval 0, Blocks (null), Lists (null), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0 PresentationIntents (\n) ListIntentOrdinal 0 CodeBlockIntentLanguageHint '(null)'";
    NSStrikethrough = 1;
    }

Tab on screen keyboard

Given that there is no tab key on the screen kbd have you any idea how to simulate one given that List Processing depends on it. I can set up an item using UIMenuController, but can't work out how to invoke handleKeyCommand.

Adding a list between two paragraphs results in the bullet point and indentation to disappear

Trying to add a list in between two existing paragraphs results in an unexpected behaviour. Executing the ListCommand renders an indented bullet point between the two paragraphs but as soon as a character is entered, both the bullet point and the indentation disappear.

Repro steps:

  1. Enter "Text", hit enter and type "Text" again
  2. Move the cursor to the end of the first "Text"
  3. Hit enter
  4. Execute ListCommand to start a new list
  5. Type any character

Expected behaviour:
The entered test is drawn next to the indented bullet point

Actual behaviour:
The bullet point and the indentation disappears.

List Items - bullets not displayed

I'm trying to get lists working. The attributed text looks the same as that produced by the example app, but the bullets don't appear, so I guess I'm missing a step somewhere. I've tried setting the listItem value to a bullet character and also the string "listItemValue".

I've traced through the code but can't see what's going wrong, although I did come across a TODO that questions the code at line 141 of LayoutManager.

Here's the attributed text for three lines:

Line 1{
NSColor = "<UIDynamicSystemColor: 0x60000318dc00; name = labelColor>";
NSFont = "<UICTFont: 0x7ffb9e42c4f0> font-family: "Helvetica"; font-weight: normal; font-style: normal; font-size: 12.00pt";
NSParagraphStyle = "Alignment 0, LineSpacing 1, ParagraphSpacing 0, ParagraphSpacingBefore 0, HeadIndent 0, TailIndent 0, FirstLineHeadIndent 0, LineHeight 12/24, LineHeightMultiple 1, LineBreakMode 0, Tabs (\n 28L,\n 56L,\n 84L,\n 112L,\n 140L,\n 168L,\n 196L,\n 224L,\n 252L,\n 280L,\n 308L,\n 336L\n), DefaultTabInterval 0, Blocks (\n), Lists (\n), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0";
"_listItem" = listItemValue;
}
{
NSColor = "<UIDynamicSystemColor: 0x60000318dc00; name = labelColor>";
NSFont = "<UICTFont: 0x7ffb9e42c4f0> font-family: "Helvetica"; font-weight: normal; font-style: normal; font-size: 12.00pt";
NSParagraphStyle = "Alignment 0, LineSpacing 1, ParagraphSpacing 0, ParagraphSpacingBefore 0, HeadIndent 0, TailIndent 0, FirstLineHeadIndent 0, LineHeight 12/24, LineHeightMultiple 1, LineBreakMode 0, Tabs (\n 28L,\n 56L,\n 84L,\n 112L,\n 140L,\n 168L,\n 196L,\n 224L,\n 252L,\n 280L,\n 308L,\n 336L\n), DefaultTabInterval 0, Blocks (\n), Lists (\n), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0";
"_blockContentType" = "Proton.EditorContent.Name(rawValue: "_newline")";
"_listItem" = listItemValue;
}Line 2{
NSColor = "<UIDynamicSystemColor: 0x60000318dc00; name = labelColor>";
NSFont = "<UICTFont: 0x7ffb9e42c4f0> font-family: "Helvetica"; font-weight: normal; font-style: normal; font-size: 12.00pt";
NSParagraphStyle = "Alignment 0, LineSpacing 1, ParagraphSpacing 0, ParagraphSpacingBefore 0, HeadIndent 0, TailIndent 0, FirstLineHeadIndent 0, LineHeight 12/24, LineHeightMultiple 1, LineBreakMode 0, Tabs (\n 28L,\n 56L,\n 84L,\n 112L,\n 140L,\n 168L,\n 196L,\n 224L,\n 252L,\n 280L,\n 308L,\n 336L\n), DefaultTabInterval 0, Blocks (\n), Lists (\n), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0";
"_listItem" = listItemValue;
}
{
NSColor = "<UIDynamicSystemColor: 0x60000318dc00; name = labelColor>";
NSFont = "<UICTFont: 0x7ffb9e42c4f0> font-family: "Helvetica"; font-weight: normal; font-style: normal; font-size: 12.00pt";
NSParagraphStyle = "Alignment 0, LineSpacing 1, ParagraphSpacing 0, ParagraphSpacingBefore 0, HeadIndent 0, TailIndent 0, FirstLineHeadIndent 0, LineHeight 12/24, LineHeightMultiple 1, LineBreakMode 0, Tabs (\n 28L,\n 56L,\n 84L,\n 112L,\n 140L,\n 168L,\n 196L,\n 224L,\n 252L,\n 280L,\n 308L,\n 336L\n), DefaultTabInterval 0, Blocks (\n), Lists (\n), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0";
"_blockContentType" = "Proton.EditorContent.Name(rawValue: "_newline")";
"_listItem" = listItemValue;
}Line 3{
NSColor = "<UIDynamicSystemColor: 0x60000318dc00; name = labelColor>";
NSFont = "<UICTFont: 0x7ffb9e42c4f0> font-family: "Helvetica"; font-weight: normal; font-style: normal; font-size: 12.00pt";
NSParagraphStyle = "Alignment 0, LineSpacing 1, ParagraphSpacing 0, ParagraphSpacingBefore 0, HeadIndent 0, TailIndent 0, FirstLineHeadIndent 0, LineHeight 12/24, LineHeightMultiple 1, LineBreakMode 0, Tabs (\n 28L,\n 56L,\n 84L,\n 112L,\n 140L,\n 168L,\n 196L,\n 224L,\n 252L,\n 280L,\n 308L,\n 336L\n), DefaultTabInterval 0, Blocks (\n), Lists (\n), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0";
"_listItem" = listItemValue;
}

Here's a screenshot:
Screenshot 2021-01-07 at 13 09 07

TextStorage should cleanup typeahead attributes for text in 0 location

Steps to reproduce:

  • Open "Text Processors" demo
  • Type "@" as the first character in TextView that will trigger text processor and will change .foregroundColor attribute
  • Remove this character
  • Type any other character which now will be .systemBlue color

Looks like setting . foregroundColor attribute also changes typingAttributes and textColor properties of UITextView. Then UITextView uses these properties for further input. Because there is no text before this range where other attributes can be inherited.
Setting typingAttributes right after cleaning . foregroundColor doesn't help.

Tested in demo project. This is default UITextView behaviour.

Installing with SPM fails due to mixed language source files

When trying to include Proton in my project using SPM I get the following error message:
target at '[...]/SourcePackages/checkouts/proton/Proton/Sources' contains mixed language source files; feature not supported

I only get this issue on the master branch. Installing from the 0.5.0 tag works fine.

How to support multiple list formats

I want to let the user create both ordered and unordered lists but I'm not sure how to support this.

I tried adding a @Binding to the EditorListFormattingProvider but for obvious reasons this affects all lists in the EditorView:

class ListFormattingProvider: EditorListFormattingProvider {

    @Binding var listStyle: ListStyle

    init(listStyle: Binding<ListStyle>) {
        _listStyle = listStyle
    }

    let listLineFormatting: LineFormatting = LineFormatting(indentation: 25, spacingBefore: 0)

    func listLineMarkerFor(editor: EditorView, index: Int, level: Int, previousLevel: Int, attributeValue: Any?) -> ListLineMarker {

        switch listStyle {
        case .bullet:
            return BulletSequenceProvider().value(at: index)
        case .numbered:
            if level == 1 {
                return NumberSequenceProvider().value(at: index)
            } else {
                return DashSequenceProvider().value(at: index)
            }
        }


    }
}

Is there a way to do this?
Thank you

Build ExampleApp fails with Undefined symbols for architecture x86_64: "_OBJC_CLASS_$_PREditorContentName"

Trying to build "proton-main.zip" or "proton-0.6.1.zip" ExampleApp fails with:

_Undefined symbols for architecture x86_64:
"OBJC_CLASS$PREditorContentName", referenced from:
objc-class-ref in JSONDecoder.o
objc-class-ref in JSONEncoder.o
objc-class-ref in ParagraphEncoder.o
objc-class-ref in WidthRangeAttachmentExampleViewController.o
objc-class-ref in AutogrowingTextField.o
objc-class-ref in PanelView.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

Environment: macOS 10.15.7, Xcode 12.4.

I note that there does not seem to be any build target for ProtonExtensions in the Proton.xcworkspace.

I could find no documentation on how one is supposed to integrate Proton into a project. Shouldn't there be a documented Swift Package Manager URL?

SwiftUI support require

Could you please support SWiftUI framework?It will be pretty good~ Thanks for your great job.

Inconsistent List exit behaviour in middle of two non-list paragraphs

Add a line of text and press return
Add another line of text
Create a multi-level list in the middle of these two lines (preferable 5-6 levels deep)
Hit enter at the last element of the list
The list item is outdented
Keep doing it all the way until all lists are exited.

Observed:
Selected range moves outside list at some point without exiting all the levels.

Expected:
List should outdent all the way to zeroth level without loosing focus.

list-exit-issue

Question: What is the best way to get notifications from all nested EditorView?

Let's imagine we have VC that holds parent EditorView. It subscribes to its changes via EditorViewDelegate. However, it doesn't receive notifications from nested editors in attachments.

Some posible use cases:

  1. Check changes in content and enable/disable "submit" button while editing content
  2. Show typeahead picker at cursor while typing

Some possible solutions:

  1. Do not subscribe to nested EditorView in EditorContentView and pass main EditorViewDelegate to nested EditorView
  2. Have some proxy delegate in EditorContentView so this view can get notifications and provide specific callbacks like in PanelViewDelegate examples.

Maybe, I missed some other solution. And another question is should it be part of the Core library like some utility code?

LayoutConstraints issue

My app embeds an EditorView in a SwiftUI UIViewRepresentable struct which is the documented way to use UIKit views. The problem is that the text does not scroll when it reaches the bottom of the view. However, if I close the view and reopen it things wok as expected.

I have found that AutogrowingTextView.LayoutSubviews is only called once when the view opens, so isScrollEnabled is left set to false. Closing and reopening the view sorts this because there is sufficient text available.

The problem seems to be the layoutConstraints because the console contains the following immediately after EditorView.setup is called.

I can overcome the issue by setting isScrollEnabled after every keystroke.

2021-08-08 10:15:17.067778+0100 Easiwriter[3611:91422] [LayoutConstraints] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
"<NSAutoresizingMaskLayoutConstraint:0x60000243ae40 h=--& v=--& Proton.EditorView:0x7fb90d839800.height == 759.333 (active)>",
"<NSLayoutConstraint:0x60000240ecb0 V:|-(0)-[Proton.RichTextView:0x7fb90d0b4800] (active, names: '|':Proton.EditorView:0x7fb90d839800 )>",
"<NSLayoutConstraint:0x60000240eb70 Proton.RichTextView:0x7fb90d0b4800.bottom == Proton.EditorView:0x7fb90d839800.bottom (active)>",
"<NSLayoutConstraint:0x600002409ae0 Proton.RichTextView:0x7fb90d0b4800.height >= 926 (active)>"
)

Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x600002409ae0 Proton.RichTextView:0x7fb90d0b4800.height >= 926 (active)>

No list indicator after pasting

The paragraph style is kept but there is no starting list indicator.

This can be reproduced in the CommandsExampleViewController

  1. Add a list
  2. Paste text (in the screenshot I pasted "List")
  3. Tap enter/return

Simulator Screen Shot - iPhone 12 mini - 2020-10-22 at 15 42 50

As a user, I can move between nested Editors using cursors

I believe this will be important in two scenarios.

Imagine two paragraphs with a Panel in between them which contains a paragraph worth of content. If my cursor is half way along the top paragraph, tapping down should move to “the next line” which is either inside the same paragraph or inside the Panel. I’d expect my cursor to jump into the middle of the first line of the Panel.

Imagine we build a Table with a 2-dimensional array of Editors. If I tap down in a cell, I’d expect the cursor to move into the editor below. Similarly, if I move my cursor to the end of the content within a cell, tapping right should jump to the next cell to the right.

I believe we can leverage the protocol used for “us focusable”.

NumericList Items Don't Initially Render Properly

The pops up on several numbers. I tried to do dig a bit into LayoutManager and drawListItem but failed to really find much.

I also switched out the ListLineMarker and returned a UIImage, the images also failed to render correctly at the same indices.

Simulator Screen Shot - iPhone 11 - 2021-02-19 at 13 07 58

SwiftUI UIRepresentable EditorView extends beyond swiftUI view

I am using Proton with swiftUI through a UIRepresentableView. I am having trouble enabling the scroll functionality when the editor reaches its container size.

I set up my view this way:

struct RepresentableRichTextEditor: UIViewRepresentable {
    
    @Binding var attributedText: NSAttributedString
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    let commandExecutor = EditorCommandExecutor()
    
    func makeUIView(context: Context) -> UIView {
        
        let editor = EditorView()
        editor.delegate = context.coordinator

        //This forces the editor view to scroll however disables the auto layout provided by the representable view
        //editor.translatesAutoresizingMaskIntoConstraints = false 
        //editor.maxHeight = 300

        EditorViewContext.shared.delegate = context.coordinator
        editor.setFocus()
        return editor
    }

    func updateUIView(_ uiView: UIView, context: Context) {

       //Update logic here
        
    }
}

I have tried using .frame(height) in the parent SwiftUI View but this didn't seem to affect the underlying editorView.

Does anyone know how to get scrolling working with SwiftUI?

Many thanks

Ask framework if selected range is a list

Hi,

Is there a way to ask the framework, for example the ListTextProcessor if the selected range is a list?

This would be good if you have a control to toggle lists on/off.

Thanks for a great framework!
Jonathan

EditorView & UITextViewDelegate

I am trying to add Proton to a SwiftUI app I've been developing for about a year. It contains a UITextView editor embedded in a UIViewRepresentable class. I want to have bullet/numbered lists and embedded images, which is not easy as I'm sure you know. In fact, trying to solve the problem is how I found Proton. Congratulations on a great piece of work!

I have experimented with the ExampleApp to get a feel for things and have managed to embed a Proton EditorView in my app as a replacement for the UITextView. I can type into it, but that's about all. What I am missing is the Proton equivalents for UITextViewDelegate methods. I am also struggling to detect the EditorView's firstResponder state.

Finally, as you know, the Master package doesn't compile because of the mixed language issue, so I am using version 0.5 at the moment.

V0.6.0 won't compile with Xcode 12.5

Just upgraded Xcode and Proton's ObjC code has stopped compiling - NS_ASSUME_NONNULL_BEGIN unknown.

I've tried going back to v0.5.0 but that won't compile because ListParser is not implemented in that version. So I'm stuck, although I could copy the ListParser code as a workaround.

EditorListFormattingProvider

I have implemented my own EditorListFormattingProvider. Looking at your code it looks like I need to have a LayoutManagerDelegate that overrides listLineMarkerFor somehow. Is that correct?

Feature Request: Add `Attachment` to `EditorContentType` case

Currently, we have contents() method that returns [EditorContent]. But its case contains only view, but not an attachment.

public struct EditorContent {
    public let type: EditorContentType
    ...
}

public enum EditorContentType {
    case attachment(name: EditorContent.Name, contentView: UIView, type: AttachmentType)
    ....
}

So if user creates Attachment, then put it into editor there is no designated way to get them back (well except iterating over attributedString of course).

I can create small PR if @rajdeep approves the idea 🙂

List formatting issue

I have an issue with displaying a list. The list is displayed OK until I hit enter, after which the output is incorrect.

Here's the list before typing Enter:

Screenshot 2021-04-09 at 11 46 01

Here's the list after:

Screenshot 2021-04-09 at 11 46 18

And here's the NSAttributedString. There is a custom attribute called EasiwriterStyle, but that shouldn't upset it should it?

One
{
    EasiwriterStyle = "Easiwriter.StyleIdentifier(name: \"Body\", id: \"325A872C-7289-4319-8DEB-CE06B111E956\")";
    NSColor = "<UIDynamicSystemColor: 0x600002ae5600; name = labelColor>";
    NSFont = "<UICTFont: 0x7feff6909f30> font-family: \"UICTFontTextStyleBody\"; font-weight: normal; font-style: normal; font-size: 17.00pt";
    NSParagraphStyle = "Alignment 0, LineSpacing 1, ParagraphSpacing 0, ParagraphSpacingBefore 0, HeadIndent 0, TailIndent 0, FirstLineHeadIndent 0, LineHeight 0/0, LineHeightMultiple 1, LineBreakMode 0, Tabs (\n    28L,\n    56L,\n    84L,\n    112L,\n    140L,\n    168L,\n    196L,\n    224L,\n    252L,\n    280L,\n    308L,\n    336L\n), DefaultTabInterval 0, Blocks (\n), Lists (\n), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0";
    "_blockContentType" = "Proton.EditorContent.Name(rawValue: \"_newline\")";
    "_listItem" = ListValue;
}Two{
    EasiwriterStyle = "Easiwriter.StyleIdentifier(name: \"Body\", id: \"325A872C-7289-4319-8DEB-CE06B111E956\")";
    NSColor = "<UIDynamicSystemColor: 0x600002ae5600; name = labelColor>";
    NSFont = "<UICTFont: 0x7feff6909f30> font-family: \"UICTFontTextStyleBody\"; font-weight: normal; font-style: normal; font-size: 17.00pt";
    NSParagraphStyle = "Alignment 0, LineSpacing 1, ParagraphSpacing 0, ParagraphSpacingBefore 0, HeadIndent 0, TailIndent 0, FirstLineHeadIndent 0, LineHeight 0/0, LineHeightMultiple 1, LineBreakMode 0, Tabs (\n    28L,\n    56L,\n    84L,\n    112L,\n    140L,\n    168L,\n    196L,\n    224L,\n    252L,\n    280L,\n    308L,\n    336L\n), DefaultTabInterval 0, Blocks (\n), Lists (\n), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0";
    "_listItem" = ListValue;
}
{
    EasiwriterStyle = "Easiwriter.StyleIdentifier(name: \"Body\", id: \"325A872C-7289-4319-8DEB-CE06B111E956\")";
    NSColor = "<UIDynamicSystemColor: 0x600002ae5600; name = labelColor>";
    NSFont = "<UICTFont: 0x7feff6909f30> font-family: \"UICTFontTextStyleBody\"; font-weight: normal; font-style: normal; font-size: 17.00pt";
    NSParagraphStyle = "Alignment 0, LineSpacing 1, ParagraphSpacing 0, ParagraphSpacingBefore 0, HeadIndent 0, TailIndent 0, FirstLineHeadIndent 0, LineHeight 0/0, LineHeightMultiple 1, LineBreakMode 0, Tabs (\n    28L,\n    56L,\n    84L,\n    112L,\n    140L,\n    168L,\n    196L,\n    224L,\n    252L,\n    280L,\n    308L,\n    336L\n), DefaultTabInterval 0, Blocks (\n), Lists (\n), BaseWritingDirection -1, HyphenationFactor 0, TighteningForTruncation NO, HeaderLevel 0 LineBreakStrategy 0";
    "_blockContentType" = "Proton.EditorContent.Name(rawValue: \"_newline\")";
    "_listItem" = ListValue;
}

Storing & setting of contents in EditorView programatically

I am trying to store and set the data in EditorView to/from core data persistence.

I came across EditorView.contents() to extract the array of EditorContent that I could store to CoreData. However I'm not quite sure how to set this data to the EditorView from the persistent store? Is this the best way to handle data storage from the EditorView?

I don't just want to store the attributed string, but everything needed to recreate the layout of the editor view.

Many thanks for the great work being done with proton, this is a great library and I'm looking forward to where it can go.

ProtonExtensions.framework: Introduce a “Standard Library” of common plugins

This repo currently contains the core Editor as a framework and the example project. I believe there is space for a framework which contains text processors and commands for common editor behaviour.

  • H1/2/3/4/5/6
  • Bold/Italics/Strike-through
  • Bullet point

Implementing the Text Processor for markdown (* _ #) would be great. Additionally and separately we can co-develop a Command for allowing the Integrator to implement a key command or UIButton action.

These ProtonExtensions are optional and can be ignored by the Integrator - but it is assumed that these will be valuable to most Editor usages. The Example app in this repo will use these extensions, as well as demoing some exotic examples not suitable for production.

Feature Request: Current TextProcessor notification on selection (cursor position) change

To show selected state (formatting option or selected type ahead processor) in a toolbar (by highlighting corresponding button) or to show/hide options picker on changing cursor position.

As a suggestion EditorView can ask TextProcessor for active text processors for current range. Maybe, it requires new method in TextProcessing that return whether this processor should be active in selected range.

ListCommand issues

My app uses a style mechanism similar to the one in Pages. To this end it maintains model data describing the style structure of each document. This data is used to generate the attributed text whenever necessary. The issue I have faced is that Proton keeps the nesting structure of lists close to its chest, so it is impossible to regenerate it from model data. I have resolved this for now by saving a copy of the attributed text for each list and replacing it when the document is reloaded/updated. This is not ideal, but does work.

The other issue that I have yet to resolve is how to detect (a) double returns terminating a list and (b) backspacing into a list. It would be great if events could be signalled/state maintained so that these changes can be identified. Maybe this is already possible and I just haven't found it.

New tag

Hi,
I really enjoy proton :). I was wondering if you have any plans to create a new tag soon?

Placeholder doesn't appear after text deletion

Before TextStorage replacement in #66 deleteBackward caused didCompleteLayout layout manager delegate call which has updatePlaceholderVisibility inside.

image

However, after this TextStorage update didCompleteLayout isn't being called anymore and after removing the last character placeholder doesn't appear.

Trying to figure out why it happens 🤔

FontTraitToggleCommand overwrites last executed command.

Hi,

This bug can be reproduced using the example app using the "Commands" example.

  1. Write any letter.
  2. Select Bold and Italic.
  3. Write any letter.

The letter will now only be italic.

The code in question is the following in FontTraitToggleCommand.

        if selectedText.length == 0 {
            guard let font = editor.attributedText.attribute(.font, at: editor.selectedRange.location - 1, effectiveRange: nil) as? UIFont else { return }
            editor.typingAttributes[.font] = font.toggled(trait: trait)
            return
        }

It only looks at the attributes for the letter before the selected range. If you previously set something to editor.typingAttributes without typing it will be reset.

The code in the previous if-statement does the correct thing for this case.

        if editor.isEmpty || editor.selectedRange == .zero {
            guard let font = editor.typingAttributes[.font] as? UIFont else { return }
            editor.typingAttributes[.font] = font.toggled(trait: trait)
            return
        }

"_blockContentType" doesn't conform to NSSecureCoding

Hi, sorry for bombarding you with issues recently.

I am trying to save the attributedString created by Proton to Core Data. To save the NSAttributedString to core data I'm converting it to NSData using an NSSecureUnarchiveFromDataTransformer. This requires that everything conform to NSSecureCoding before being transformed into NSData. Here lies the problem

I believe the custom NSAttributedString.Key - in my case "_blockContentType" came up a lot - don't conform to NSSecureCoding hence not allowing the NSAttributedString to be stored to core data. Would it be possible to make it conform in some way?

Support Multiple `EditorListFormattingProvider`s in an editor

I'm looking into how to support multiple list formats in the same editor. At minimum it feels natural to support ordered and unordered lists in one editor. Before going too far, is this something already in progress? Would it be a welcome addition?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.