stevengharris / markupeditor Goto Github PK
View Code? Open in Web Editor NEWWYSIWYG editing for SwiftUI and UIKit apps
License: MIT License
WYSIWYG editing for SwiftUI and UIKit apps
License: MIT License
Altho we notify when a local image is added (so, for example, the user can copy the image file somewhere), we also need to know when a local image is deleted (so, for example, we can remove the copy we made).
The table creation drop-down from the TableToolbar cannot be used properly on touch devices.
Hi Steve,
I am looking into the behaviour of the indentation for the ordered and unordered list.
This does not work or constructible.
H
where as this the current behaviour
Here is a numbered sublist.
With two items.
I
The second item can be indented and that becomes the first item on the newer indent level.
I went through the code and this line in the function _indentListItem
if (!previousListItem || (previousListItem.nodeName !== 'LI')) { return null };
returns null because the previous item is null when I tried to indent on the first item.
I have seen this comment that explains the reason to not indent.
// If previousElementSibling doesn't exist or is not
// an LI, then the indent is not allowed.
Is this logic intentional that we are not able to do multiple indent on the list for the first item on the indent level?
0x1052f8578 <+8>: b.lo 0x10528598; <+40> -》〉》Thread 1: signal SIGABRT
The MarkupEditor SelectionState depends on various click/mouse events in ways that don't work properly on touch devices. As a result, a lot of the basic editing operations don't work properly on touch devices.
As noted in #87, the MarkupEditorView does not avoid the keyboard when in a ScrollView. Raising this issue here just so it's clearer.
Here is an example that shows the different behavior between a MarkupEditorView and a TextField.
struct ContentView: View, MarkupDelegate {
@State private var text = "Hello World"
@State private var loaded: Bool = false
@State private var demoHtml: String = "<h1>Hello World</h1>"
var body: some View {
ScrollViewReader { proxy in
ScrollView {
Rectangle()
.frame(height: 1000)
MarkupEditorView(markupDelegate: self, html: $demoHtml).id(1)
.frame(height: 200)
.overlay(
RoundedRectangle(cornerRadius: 6.0)
.stroke(.gray, lineWidth: 1.0)
)
.onChange(of: loaded) { _ in
withAnimation {
proxy.scrollTo(1)
}
}
TextField("Name:", text: $text)
}
.padding()
}
}
func markup(_ view: MarkupWKWebView, heightDidChange height: Int) {
loaded = true
}
}
If you select in the TextField, the ScrollView scrolls upward to expose the insertion point. With MarkupEditorView, the insertion point will be covered by the keyboard. They MarkupEditorView needs to have the same behavior as TextField.
Although SelectionState is set properly, the selection caret is sometimes missing, especially when more than 1 MarkupWKWebView is present and selection is accomplished using MarkupEditor.firstResponder.
Weirdnesses since moving to newer Xcode and OS
@State var value = "sample"
MarkupEditorView(html:$value)
No errors the editor works, but any changes do not affect the "value" property, in this example it is always "sample". The demo in the repository is the same.
ios 16.1 simulator and device
Pasting plain text for formatted text works well, unless that text contains HTML within it. If it does, then the resulting pasted text is messed up.
When you "do" an action (e.g., bold, indent, etc) in the MarkupEditor, a change is pushed on the native undo stack by executing:
document.execCommand('selectAll');
document.execCommand('insertText', false, nextID);
while focused on a hidden div in the document. The nextID tracks a pointer into the internal undo stack. The code to do this is in markup.js
in the Undoer class.
Version 0.5.1 works properly on Big Sur. As you "do" actions, you can see that the Undo
menu option in the Edit
menu is enabled, because the execCommand('insertText')
runs and pushes the change to the div's content onto the native undo stack. Once the Undo
and/or Redo
are enabled, then the standard hot-keys (⌘Z and ⇧⌘Z) work which in turn invoke the undo operation in the MarkupEditor.
In Ventura, the same code executes, but the native undo stack is not changed. The Undo
and Redo
never get enabled in the Edit
as you invoke operations on the document, and therefore neither the menu or hot-keys work for Undo/Redo.
Editing operations that don't have to do with MarkupEditor actions (e.g., type some text then undo) work properly in both versions. Only the MarkupEditor operations (think: things that are exposed via the MarkupToolbar
) are impacted.
FWIW, the undo/redo tests work properly because the invoke the operations queued in the internal undo stack.
In a sad bit of irony, this is one of the very few places where the MarkupEditor uses the deprecated and browser-dependent execCommand
.
If you have a blockquote with multiple lines like:
This is a multiline
blockquote
and you try to outdent one line, both get outdented. This happens because Enter in a blockquote just creates a new paragraph within the blockquote, so outdent is just removing the one blockquote that surrounds both <p> elements. This is probably better done by having enter produce a separate blockquote for each <p>, especially now that multidenting allows you to select multiple dented paragraphs and take an action on them collectively.
assertionFailure is thrown: “Could not find markup.html, css, and js for this bundle.”
With operations supported when multiple elements are selected, the SelectionState needs to stop relying on anchorNode. In some cases the SelectionState is preventing an operation from being applied across multiple elements because it is used to determine is Toolbar buttons are disabled.
Hi @stevengharris, I was testing around the addition of span tag, alongside H1-H6, it seems possible to include in the StyleContext.swift file, but it also will need support on the markup.js file.
https://www.w3schools.com/tags/tag_span.asp
Since H1-H6 are block level styling, I'm wondering whether will you consider inline styling using span in the future?
I am looking at the possibility to define <span style="font-size: 23px";>Example</span>
syntax for a user who wants to have finer control on the styling.
The MarkupEditor handles keydown
specially in markup.js for some specific cases:
The special handling deals with things like creating a new list item, moving between table cells, etc. When no special handling is required, the standard browser behavior happens, which is the case almost all of the time. As an example, when pressing Enter when the selection is in a list, we want to create a new list item of the same type as the list we are in. However, the standard browser behavior is just to insert a <br>, which is not what anyone expects and makes it impossible to add items to a list just by typing and pressing Enter at the end of each item.
On Mac Catalyst as of MacOS 13 (Ventura), we never receive a keydown
event, so none of the special handling happens. As a result, only the standard (and as described above, incorrect) browser behavior occurs. As a result, from a user perspective, a lot of editing on Mac is buggy as of the Ventura release. Editing on iOS (as of iOS 16) works fine.
The problem is reproducible in the simplest app that uses a WKWebView. For example, using the following in your ContentView in a clean SwiftUI app created with Xcode will show the click
event triggering, but the keydown
event never triggers. Note:
input
element has the same problem as the contenteditable div
on Mac Catalyst on MacOS 13.import SwiftUI
import WebKit
struct ContentView: View {
var div: String = "<div contenteditable=\"true\" onkeydown=\"console.log('keydown: ' + event.key);\" onclick=\"console.log('click');\"<h1>Hello world.</h1></div>"
var input: String = "<input type=\"text\" onkeydown=\"console.log('keydown: ' + event.key);\" onclick=\"console.log('click');\">"
var body: some View {
VStack {
WebView(html: div)
WebView(html: input)
}
.padding()
.background(Color.red)
}
}
struct WebView: UIViewRepresentable {
var html: String
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ webView: WKWebView, context: Context) {
webView.loadHTMLString(html, baseURL: nil)
}
}
Hello,
I found possible problem. I would like to customize toolbar and I use following code:
`
MarkupEditor.style = .labeled
MarkupEditor.allowLocalImages = false
let myToolbarContents = ToolbarContents(leftToolbar: false, correction: false, insert: false, style: true, format: true, rightToolbar: false, insertContents: InsertContents(link: false, image: false, table: false), styleContents: StyleContents(list: true, dent: false), formatContents: FormatContents(code: false, strike: false, subSuper: false))
ToolbarContents.custom = myToolbarContents
`
but I still see button with select "normal", "header 1-6".
a need to hide this button. (stay only LIST (ul and li) and B I U)
Piotr
Indent/outdent only works on a single styled element at a time.
There is no easy way to exit from a blockquote. In lists, pressing Enter within an empty list item outdents. Blockquotes should do the same.
In environments with a non-hardware keyboard, the MarkupToolbar is presented as an inputAccessoryView with the keyboard. The TableToolbar is presented as a SubToolbar that extends from the MarkupToolbar. This approach works okay for the Mac, but it presents (pun intended) problems on dismissal. For example, when the TableSizer is presented, and the user drags across the grid, the TableSizer should be dismissed when the drag is complete. This doesn't work on iOS.
The second issue with the TableToolbar presentation is that when the MarkupToolbar is embedded in another view's toolbar along with other ToolbarItems, the SubToolbar type of style of extending from the MarkupToolbar just looks bad. The SubToolbar extends for the MarkupToolbar width, but the MarkupToolbar is embedded in a larger toolbar, so the SubToolbar is a kind of stub that extends below the larger toolbar and looks weird.
The TableToolbar should be presented as a .popover from the "insert table" button of the InsertToolbar. This behavior would also be consistent with the link and image presentation approach when those buttons are pressed.
The edges for cells within rows and headers are not settable by the user.
Context menus are required on touch devices at a minimum for copy/cut/paste, and should mirror the Format menu on Mac Catalyst.
Current image sizing maintains proportionality but is absolute. This is bad when the same document is edited on devices with different window size/resolution, like a Mac and an iPhone.
you should be able to customize the toolbar by being able to remove any of the toolbars (ex. if you want to remove the InsertToolbar )
and if you want to remove some of the features in a toolbar (ex.remove the underline in the FormatToolbar)
Currently the menus and hotkeys exposed in the demo apps only support some operations for the MarkupEditor (e.g., standard editing ones like Ctrl-B, Ctrl-I, etc). Features need to be fully supported in menus and hotkey.
Thread 1: signal SIGABRT
System version: 15.2
The inputAccessoryView in MarkupWKWebView disappears when you rotate the device, which is even more of a problem when that means there is no way to dismiss the keyboard that no longer has an inputAccessoryView.
Hi @stevengharris,
For the formatting logic (Bold, Italics, Underline, etc), the insertion of newline "p" appears to break the formatting structure of previous text. This is especially apparent when the structure becomes nested with permutations of B/I/U and P tags.
I am comparing this behaviour with email clients like outlook and gmail which provides similar formatting functionalities and it does not feel like this matches the behaviour of those email clients.
I have attached a screen recording of this occurrence.
https://user-images.githubusercontent.com/4402214/167060824-cecd4578-36df-4932-9813-d0f4be8f114e.mp4
Do let me know if this is an intended behaviour for formatting the text, and also do let me know if you require additional assistance on this.
The MarkupEditor does not treat <code> as a block element, since it's not one. It does, however, have markup.css set up to format code inside of <p> and other "style" block elements differently than when <code> is inside of a <blockquote>, which is a block element itself. For <p> and the other "style" elements, it <code> uses css "display: inline", while outside of the "style" elements (e.g., in <blockquote>) it uses css "display: block". This kind of works, but it means that in the MarkupEditor currently, the only time you can get code to display as a block is in an indented block, which is not what everyone (including me) wants. To fix this, I think <code> needs to be displayable inside of a <pre> block element. This in turn will require special handling for Enter and for multistyle operations.
An associated problem with <code> formatting is that it needs to be scrollable horizontally. This is fixable by including "overflow-x: scroll" in code's css. However, given the other problem above, the scrolling only works currently when code is in a blockquote, since the css uses "display: inline" inside of the styling elements.
Images can currently be resized using a stepper, but this should be doable using direct manipulation of handles at the corner of the image.
Hi @stevengharris, are you looking into the possibility of RTL language support in the future? I tested the editor with arabic language and while it does the basic BLU formatting, the bullet list and others are generally LTR supported only. The text carat is also on the right instead of the left for RTL language, but the characters are appended on the left.
I wonder why did you use SwiftUI inside UIKitDemo. I can't use this great control in my project that supports iOS 11+.
I'm using this in a SwiftUI project and the only thing I haven't been able to figure out is how to programmatically set focus into the editor.
I have found StackOverflow posts that suggest WKWebView intentionally tries to prevent automatically setting focus without user interaction.
Is there a way in MarkupEditor that I can have it assume focus? My scenario is that I present the editor in a sheet and want the user to be able to start typing in the editor without having to tap into the editor first.
Hi @stevengharris,
I have noticed that when the content is empty, hitting backspace will cause a div counter to be visible at the bottom of the main editable div. I went through the logic in markup.js file to get a gist of it, its related to the undo and redo function, and there is a logic that modifies the visibility style of the div. I am not sure what is the purpose of it being visible.
I have attached a screen recording of this occurring.
https://user-images.githubusercontent.com/4402214/167055277-ac743213-878e-4e9a-be7a-8282dc5e37da.mp4
Paragraph and other elements should leave space at the bottom, not the top
While it's convenient for the selection to be set at HTML load time (thereby enabling and updating the MarkupToolbar), it causes unnecessary updates when an app uses multiple MarkupWKWebViews and the selection will be made by the user afterward.
Implement library port to macOS platform that requires a separate framework for macOS platform.
When you insert a link, the text selection is not backed up before the modal input for the link information. The same is true of inserting images. The result is that the url is placed at the previous selection point.
Hello,
I have a problem with run Yours MarkupEditor
import SwiftUI
import MarkupEditor
struct ContentView: View {
@State private var demoHtml: String = "<h1>Hello World</h1>"
var body: some View {
MarkupEditorView(html: $demoHtml)
}
}
and I get error
Cannot find 'MarkupEditorView' in scope
when I open I Package Dependencies tree with MarkupEditor 0.3.3 I does not see MarkupEditorView file. This is correct?
Thanks for help,
Piotr
<blockquote>
<p><b>Hello</b></p>
</blockquote>
and hitting Enter after "Hello" results in:
<blockquote>
<p><b>Hello</b></p>
</blockquote>
<blockquote>
<p></p><b><br></b>
</blockquote>
The new paragraph should be:
<blockquote>
<p><b><br></b></p>
</blockquote>
EditorToolbar is not going to top of Keyboard inside scrollview? any solutions?
These toolbars are inappropriate for small format devices in general and are better dealt with using a standard popover style everywhere.
The handles on images that work well with a mouse are useless on touch devices.
The prepImage function in markup.js ensures that images are resizable, selectable, and specify their width and height. In prepImage, we _callback('input') when done. Ultimately this causes the HTML to be fetched on the Swift side even in the case when prepImage made no changes to the HTML. This seems like a minor change, but if you are relying on callbacks to 'input' to indicate changes in the document, these are both unnecessary and may have side-effects for you.
I'm trying to implement auto formatting markdown syntax similar to Typora. i.e convert **Bold Text**
to <b>Bold Text</b>
on the fly without needing the tool bar. I managed to get the replacement to work by adding a regex function to markup.js function const _doEnter
which triggers when returning to a new line. But using the demo project I can't get my changes to render:
I'm sure this isn't the best way to implement this so I'd love some suggestions on how to implement this
function parseMarkdown(markdownText) {
const htmlText = markdownText
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
.replace(/^\> (.*$)/gim, '<blockquote>$1</blockquote>')
.replace(/\*\*(.*)\*\*/gim, '<b>$1</b>')
.replace(/\*(.*)\*/gim, '<i>$1</i>')
.replace(/!\[(.*?)\]\((.*?)\)/gim, "<img alt='$1' src='$2' />")
.replace(/\[(.*?)\]\((.*?)\)/gim, "<a href='$2'>$1</a>")
.replace(/\n$/gim, '<br />')
return htmlText.trim()
}
/**
* Handle the Enter key to avoid <div> being inserted instead of <p>
*
* @returns {HTML Paragraph Element || null} The newly created P to preventDefault handling; else, null.
*/
const _doEnter = function(undoable=true) {
let sel = document.getSelection();
...
const parent = _findFirstParentElementInNodeNames(selNode, _paragraphStyleTags);
_consoleLog(selNode.textContent)
selNode.textContent = parseMarkdown(selNode.textContent) // *** Parse markdown from line selection
_consoleLog(selNode.textContent)
if (selectionAtEnd && nextSiblingIsEmpty && parent) {
...
};
return null; // Let the MarkupWKWebView do its normal thing
};
When selecting multiple paragraphs, you cannot emphasize multiple paragraphs when using list numbers or ordinal numbers
What is an editor without a proper way to search for text?
When specifying a resourcesUrl for local images, new local images should be created within the caches directory at the relativePath for resourcesUrl. For example, if you paste an image in, and resourcesUrl is 'resources' relative to the base URL, then the image src should be 'resources/.'. Currently it is just '.'.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.