Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Placeholder support for empty document #102

Merged
merged 3 commits into from
Apr 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion MarkupEditor/MarkupEditorUIView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ public class MarkupEditorUIView: UIView, MarkupDelegate {
wkUIDelegate: WKUIDelegate? = nil,
userScripts: [String]? = nil,
html: String?,
placeholder: String? = nil,
selectAfterLoad: Bool = true,
resourcesUrl: URL? = nil,
id: String? = nil) {
super.init(frame: CGRect.zero)
webView = MarkupWKWebView(html: html, selectAfterLoad: selectAfterLoad, resourcesUrl: resourcesUrl, id: "Document", markupDelegate: markupDelegate ?? self)
webView = MarkupWKWebView(html: html, placeholder: placeholder, selectAfterLoad: selectAfterLoad, resourcesUrl: resourcesUrl, id: "Document", markupDelegate: markupDelegate ?? self)
// The coordinator acts as the WKScriptMessageHandler and will receive callbacks
// from markup.js using window.webkit.messageHandlers.markup.postMessage(<message>)
coordinator = MarkupCoordinator(markupDelegate: markupDelegate, webView: webView)
Expand Down
6 changes: 5 additions & 1 deletion MarkupEditor/MarkupEditorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,16 @@ public struct MarkupEditorView: View, MarkupDelegate {
private var id: String?
private var html: Binding<String>?
private var selectAfterLoad: Bool = true
/// The placeholder text that should be shown when there is no user input.
public var placeholder: String?

public var body: some View {
VStack(spacing: 0) {
if MarkupEditor.toolbarLocation == .top {
MarkupToolbar(markupDelegate: markupDelegate).makeManaged()
Divider()
}
MarkupWKWebViewRepresentable(markupDelegate: markupDelegate, wkNavigationDelegate: wkNavigationDelegate, wkUIDelegate: wkUIDelegate, userScripts: userScripts, html: html, selectAfterLoad: selectAfterLoad, resourcesUrl: resourcesUrl, id: id)
MarkupWKWebViewRepresentable(markupDelegate: markupDelegate, wkNavigationDelegate: wkNavigationDelegate, wkUIDelegate: wkUIDelegate, userScripts: userScripts, html: html, placeholder: placeholder, selectAfterLoad: selectAfterLoad, resourcesUrl: resourcesUrl, id: id)
if MarkupEditor.toolbarLocation == .bottom {
Divider()
MarkupToolbar(markupDelegate: markupDelegate).makeManaged()
Expand All @@ -49,6 +51,7 @@ public struct MarkupEditorView: View, MarkupDelegate {
wkUIDelegate: WKUIDelegate? = nil,
userScripts: [String]? = nil,
html: Binding<String>? = nil,
placeholder: String? = nil,
selectAfterLoad: Bool = true,
resourcesUrl: URL? = nil,
id: String? = nil) {
Expand All @@ -60,6 +63,7 @@ public struct MarkupEditorView: View, MarkupDelegate {
self.selectAfterLoad = selectAfterLoad
self.resourcesUrl = resourcesUrl
self.id = id
self.placeholder = placeholder
}

}
34 changes: 24 additions & 10 deletions MarkupEditor/MarkupWKWebView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public class MarkupWKWebView: WKWebView, ObservableObject {
/// The HTML that is currently loaded, if it is loaded. If it has not been loaded yet, it is the
/// HTML that will be loaded once it finishes initializing.
private var html: String?
private var placeholder: String? // A string to show when html is nil or empty
public var selectAfterLoad: Bool = true // Whether to set the selection after loading html
private var resourcesUrl: URL?
public var id: String = UUID().uuidString
Expand Down Expand Up @@ -87,9 +88,10 @@ public class MarkupWKWebView: WKWebView, ObservableObject {
initForEditing()
}

public init(html: String? = nil, selectAfterLoad: Bool = true, resourcesUrl: URL? = nil, id: String? = nil, markupDelegate: MarkupDelegate? = nil) {
public init(html: String? = nil, placeholder: String? = nil, selectAfterLoad: Bool = true, resourcesUrl: URL? = nil, id: String? = nil, markupDelegate: MarkupDelegate? = nil) {
super.init(frame: CGRect.zero, configuration: WKWebViewConfiguration())
self.html = html
self.placeholder = placeholder
self.selectAfterLoad = selectAfterLoad
self.resourcesUrl = resourcesUrl
if id != nil {
Expand Down Expand Up @@ -324,19 +326,21 @@ public class MarkupWKWebView: WKWebView, ObservableObject {
/// MarkupWKWebView becomeFirstResponder and trigger a SelectionState
/// update to refresh the MarkupToolbar as each one loads its HTML.
public func loadInitialHtml() {
setHtml(html ?? "") {
//print("isReady: \(self.id)")
self.isReady = true
if let delegate = self.markupDelegate {
delegate.markupDidLoad(self) {
setPlaceholder {
self.setHtml(self.html ?? "") {
//print("isReady: \(self.id)")
self.isReady = true
if let delegate = self.markupDelegate {
delegate.markupDidLoad(self) {
if self.selectAfterLoad {
self.becomeFirstResponderIfReady()
}
}
} else {
if self.selectAfterLoad {
self.becomeFirstResponderIfReady()
}
}
} else {
if self.selectAfterLoad {
self.becomeFirstResponderIfReady()
}
}
}
}
Expand Down Expand Up @@ -622,6 +626,16 @@ public class MarkupWKWebView: WKWebView, ObservableObject {
}
}

public func setPlaceholder(handler: (()->Void)? = nil) {
guard let placeholder else {
handler?()
return
}
evaluateJavaScript("MU.setPlaceholder('\(placeholder.escaped)')") { result, error in
handler?()
}
}

public func setHtml(_ html: String, handler: (()->Void)? = nil) {
self.html = html // Our local record of what we set, used by setHtmlIfChanged
evaluateJavaScript("MU.setHTML('\(html.escaped)', \(selectAfterLoad))") { result, error in
Expand Down
5 changes: 4 additions & 1 deletion MarkupEditor/MarkupWKWebViewRepresentable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public struct MarkupWKWebViewRepresentable: UIViewRepresentable {
private var id: String?
@Binding private var html: String
private var selectAfterLoad: Bool
private var placeholder: String?

/// Initialize with html content that is bound to an externally-held String (and therefore changable)
///
Expand All @@ -40,6 +41,7 @@ public struct MarkupWKWebViewRepresentable: UIViewRepresentable {
wkUIDelegate: WKUIDelegate? = nil,
userScripts: [String]? = nil,
html: Binding<String>? = nil,
placeholder: String? = nil,
selectAfterLoad: Bool = true,
resourcesUrl: URL? = nil,
id: String? = nil) {
Expand All @@ -48,6 +50,7 @@ public struct MarkupWKWebViewRepresentable: UIViewRepresentable {
self.wkUIDelegate = wkUIDelegate
self.userScripts = userScripts
_html = html ?? .constant("")
self.placeholder = placeholder
self.selectAfterLoad = selectAfterLoad
self.resourcesUrl = resourcesUrl
self.id = id
Expand All @@ -58,7 +61,7 @@ public struct MarkupWKWebViewRepresentable: UIViewRepresentable {
}

public func makeUIView(context: Context) -> MarkupWKWebView {
let webView = MarkupWKWebView(html: html, selectAfterLoad: selectAfterLoad, resourcesUrl: resourcesUrl, id: id, markupDelegate: markupDelegate)
let webView = MarkupWKWebView(html: html, placeholder: placeholder, selectAfterLoad: selectAfterLoad, resourcesUrl: resourcesUrl, id: id, markupDelegate: markupDelegate)
// By default, the webView responds to no navigation events unless the navigationDelegate is set
// during initialization of MarkupEditorUIView.
webView.navigationDelegate = wkNavigationDelegate
Expand Down
7 changes: 7 additions & 0 deletions MarkupEditor/Resources/markup.css
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,10 @@ p code, h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
bottom: -8px;
right: -8px;
}

.placeholder[placeholder]:after {
content: attr(placeholder);
position: absolute;
top: 8px;
color: #ccc;
}
42 changes: 42 additions & 0 deletions MarkupEditor/Resources/markup.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const _callback = function(message) {
*/
window.addEventListener('load', function () {
undoer.enable();
_updatePlaceholder();
_callback('ready');
});

Expand Down Expand Up @@ -1612,6 +1613,7 @@ document.addEventListener('selectionchange', function(ev) {
});

MU.editor.addEventListener('input', function() {
_updatePlaceholder();
_backupSelection();
_callback('input');
});
Expand Down Expand Up @@ -1859,6 +1861,11 @@ MU.editor.addEventListener('keydown', function(ev) {
// the cursor should be positioned on undo.
_deleteSelectedResizableImage('AFTER');
};
// Avoid deleting the <p> when MU.editor is empty
if (_isEmptyEditor()) {
ev.preventDefault();
break;
}
break;
case 'Delete':
// Note Delete is handled by ResizableImage if it's selected
Expand Down Expand Up @@ -2636,6 +2643,7 @@ MU.emptyDocument = function() {
sel.removeAllRanges();
sel.addRange(range);
_backupSelection();
_updatePlaceholder();
};

/**
Expand Down Expand Up @@ -2674,6 +2682,7 @@ MU.setHTML = function(contents, select=true) {
if (select) {
_initializeRange(); // Causes a selectionChange event
};
_updatePlaceholder()
_callback('updateHeight');
};

Expand Down Expand Up @@ -6619,6 +6628,25 @@ const _tripleClickSelect = function(sel) {
}
};

/**
* Placeholder
*/

MU.setPlaceholder = function(text) {
MU.editor.setAttribute('placeholder', text);
};

const _updatePlaceholder = function() {
// Do nothing if we don't have a placeholder
if (!MU.editor.getAttribute('placeholder')) { return };
// Else, add/remove the placeholder class as identified in css
if (_isEmptyEditor()) {
MU.editor.classList.add('placeholder');
} else {
MU.editor.classList.remove('placeholder');
};
};

/********************************************************************************
* Selection
*/
Expand Down Expand Up @@ -8507,6 +8535,20 @@ const _isEmpty = function(element) {
}
return empty;
};

/**
* Return true if MU.editor is truly empty.
*
* By definition, an "empty" MU.editor contains <p><br></p>.
*/
const _isEmptyEditor = function() {
const childNodes = MU.editor.childNodes;
if (childNodes.length !== 1) { return false };
const childNode = childNodes[0];
if (!_isElementNode(childNode)) { return false };
if (childNode.childNodes.length != 1) { return false };
return _isBRElement(childNode.childNodes[0]);
};

/**
* Remove all non-printing zero-width chars in element.
Expand Down
9 changes: 7 additions & 2 deletions MarkupEditor/ToolbarButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,16 @@ public struct ToolbarTextButton: View {

}

struct ToolbarButtonStyle: ButtonStyle {
public struct ToolbarButtonStyle: ButtonStyle {
@Binding var active: Bool
let activeColor: Color

func makeBody(configuration: Self.Configuration) -> some View {
public init(active: Binding<Bool>, activeColor: Color) {
_active = active
self.activeColor = activeColor
}

public func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.cornerRadius(3)
.foregroundColor(active ? Color(UIColor.systemBackground) : activeColor)
Expand Down