diff --git a/.changeset/lemon-actors-report.md b/.changeset/lemon-actors-report.md new file mode 100644 index 00000000..dfd144ab --- /dev/null +++ b/.changeset/lemon-actors-report.md @@ -0,0 +1,5 @@ +--- +"rhino-editor": minor +--- + +Adding a "faux selection" for link insertions to give a visual indicator of insertion / replacement points diff --git a/.changeset/many-poems-chew.md b/.changeset/many-poems-chew.md new file mode 100644 index 00000000..84072fa1 --- /dev/null +++ b/.changeset/many-poems-chew.md @@ -0,0 +1,5 @@ +--- +"rhino-editor": patch +--- + +Fixed a bug around "progress" finishing prematurely diff --git a/.changeset/witty-trainers-report.md b/.changeset/witty-trainers-report.md new file mode 100644 index 00000000..a00aac75 --- /dev/null +++ b/.changeset/witty-trainers-report.md @@ -0,0 +1,5 @@ +--- +"rhino-editor": patch +--- + +Fixed a bug with direct upload events not dispatching under the proper name diff --git a/esbuild.config.js b/esbuild.config.js index 18e571e7..a04fe750 100644 --- a/esbuild.config.js +++ b/esbuild.config.js @@ -31,7 +31,8 @@ function AppendCssStyles () { const { hostStyles, - toolbarButtonStyles + toolbarButtonStyles, + cursorStyles } = await import(`./src/exports/styles/editor.js?cache=${date.toString()}`) const finalString = `/* THIS FILE IS AUTO-GENERATED. DO NOT EDIT BY HAND! */ @@ -39,6 +40,9 @@ ${styles.toString()} /* src/exports/styles/editor.js:hostStyles */ ${hostStyles.toString()} +/* src/exports/styles/editor.js:toolbarButtonStyles */ +${cursorStyles.toString()} + /* src/exports/styles/editor.js:toolbarButtonStyles */ ${toolbarButtonStyles.toString()} ` diff --git a/src/exports/attachment-upload.ts b/src/exports/attachment-upload.ts index 4d610d65..4193d622 100644 --- a/src/exports/attachment-upload.ts +++ b/src/exports/attachment-upload.ts @@ -5,7 +5,7 @@ import { LOADING_STATES } from "./elements/attachment-editor.js"; import { BaseEvent } from "./events/base-event.js"; -class AttachmentUploadStartEvent< +export class AttachmentUploadStartEvent< T extends AttachmentUpload = AttachmentUpload, > extends BaseEvent { static eventName = "rhino-direct-upload:start" as const; @@ -14,7 +14,7 @@ class AttachmentUploadStartEvent< public attachmentUpload: T, options?: EventInit | undefined, ) { - super(AttachmentUploadStartEvent.name, options); + super(AttachmentUploadStartEvent.eventName, options); this.attachmentUpload = attachmentUpload; } } @@ -28,7 +28,7 @@ export class AttachmentUploadProgressEvent< public attachmentUpload: T, options?: EventInit | undefined, ) { - super(AttachmentUploadProgressEvent.name, options); + super(AttachmentUploadProgressEvent.eventName, options); this.attachmentUpload = attachmentUpload; } } @@ -42,7 +42,7 @@ export class AttachmentUploadErrorEvent< public attachmentUpload: T, options?: EventInit | undefined, ) { - super(AttachmentUploadErrorEvent.name, options); + super(AttachmentUploadErrorEvent.eventName, options); this.attachmentUpload = attachmentUpload; } } @@ -56,7 +56,7 @@ export class AttachmentUploadSucceedEvent< public attachmentUpload: T, options?: EventInit | undefined, ) { - super(AttachmentUploadSucceedEvent.name, options); + super(AttachmentUploadSucceedEvent.eventName, options); this.attachmentUpload = attachmentUpload; } } @@ -69,7 +69,7 @@ export class AttachmentUploadCompleteEvent< public attachmentUpload: T, options?: EventInit | undefined, ) { - super(AttachmentUploadCompleteEvent.name, options); + super(AttachmentUploadCompleteEvent.eventName, options); this.attachmentUpload = attachmentUpload; } } @@ -159,9 +159,13 @@ export class AttachmentUpload implements DirectUploadDelegate { }); // TODO: This may create problems for non-images, could use something like an `` instead. + const template = document.createElement("template") const obj = document.createElement("object"); + obj.toggleAttribute("hidden", true) + template.append(obj) obj.onload = () => { + template.remove() this.progress = 100; this.setUploadProgress(); this.element.dispatchEvent(new AttachmentUploadSucceedEvent(this)); @@ -169,11 +173,13 @@ export class AttachmentUpload implements DirectUploadDelegate { }; obj.onerror = () => { + template.remove() this.handleError(); }; - // obj.type = new MimeType() obj.data = blobUrl; + // Needs to append to for onerror / onload to fire. + document.body.append(template) } setUploadProgress() { diff --git a/src/exports/elements/tip-tap-editor-base.ts b/src/exports/elements/tip-tap-editor-base.ts index 9dc3f784..6c249dd6 100644 --- a/src/exports/elements/tip-tap-editor-base.ts +++ b/src/exports/elements/tip-tap-editor-base.ts @@ -16,7 +16,7 @@ import { TemplateResult, } from "lit"; -import { AttachmentUpload } from "../attachment-upload.js"; +import { AttachmentUpload, AttachmentUploadCompleteEvent, AttachmentUploadStartEvent } from "../attachment-upload.js"; import { AttachmentManager } from "../attachment-manager.js"; import { normalize } from "../styles/normalize.js"; @@ -95,6 +95,11 @@ export class TipTapEditorBase extends BaseElement { */ hasInitialized = false; + /** + * An array of "AttachmentUploads" added via direct upload. Check this for any attachment uploads that have not completed. + */ + pendingAttachments: AttachmentUpload[] = [] + /** * The hidden input to attach to */ @@ -387,11 +392,32 @@ export class TipTapEditorBase extends BaseElement { this.registerDependencies(); this.addEventListener(AddAttachmentEvent.eventName, this.handleAttachment); + this.__addPendingAttachment = this.__addPendingAttachment.bind(this) + this.__removePendingAttachment = this.__removePendingAttachment.bind(this) + + this.addEventListener(AttachmentUploadStartEvent.eventName, this.__addPendingAttachment) + this.addEventListener(AttachmentUploadCompleteEvent.eventName, this.__removePendingAttachment) + this.addEventListener("drop", this.handleNativeDrop); this.addEventListener("rhino-paste", this.handlePaste); this.addEventListener("rhino-file-accept", this.handleFileAccept); } + __addPendingAttachment (e: { attachmentUpload: AttachmentUpload }) { + this.pendingAttachments.push(e.attachmentUpload) + } + + __removePendingAttachment (e: { attachmentUpload: AttachmentUpload }) { + const index = this.pendingAttachments.findIndex((attachment) => { + return attachment === e.attachmentUpload + }) + + console.log("complete") + if (index > -1) { + this.pendingAttachments.splice(index, 1) + } + } + async connectedCallback(): Promise { super.connectedCallback(); diff --git a/src/exports/extensions/attachment.ts b/src/exports/extensions/attachment.ts index cd6a059a..e6b72067 100644 --- a/src/exports/extensions/attachment.ts +++ b/src/exports/extensions/attachment.ts @@ -747,7 +747,7 @@ export const Attachment = Node.create({ file-name=${fileName || ""} file-size=${String(fileSize || 0)} loading-state=${loadingState || LOADING_STATES.notStarted} - progress=${String(sgid || content || !fileSize ? 100 : progress)} + progress=${String(progress ? progress : (sgid || content || !fileSize ? 100 : progress))} contenteditable="false" ?show-metadata=${isPreviewable} .fileUploadErrorMessage=${this.options.fileUploadErrorMessage} diff --git a/src/exports/extensions/selection.ts b/src/exports/extensions/selection.ts index 28594d11..b2fe21b1 100644 --- a/src/exports/extensions/selection.ts +++ b/src/exports/extensions/selection.ts @@ -19,11 +19,11 @@ const selectionPlugin = (options: RhinoSelectionOptions) => { set = set.remove(set.find()); + // Whether selection was explicitly updated by this transaction. // if (!tr.selectionSet) { // return set // } - // Whether selection was explicitly updated by this transaction. const { doc, selection } = tr; let deco: Decoration | null = null; @@ -38,7 +38,8 @@ const selectionPlugin = (options: RhinoSelectionOptions) => { } else { // Show a fake cursor. let widget = document.createElement("placeholder"); - widget.setAttribute("class", "fake-cursor-selection"); + // TODO: Make this configurable. + widget.setAttribute("class", "rhino-fake-cursor-selection"); widget.setAttribute("readonly", ""); widget.setAttribute("contenteditable", "false"); deco = Decoration.widget(selection.to, widget, {}); diff --git a/src/exports/styles/editor.js b/src/exports/styles/editor.js index 1a0198b3..3bd7eab9 100644 --- a/src/exports/styles/editor.js +++ b/src/exports/styles/editor.js @@ -59,6 +59,73 @@ export const hostStyles = css` } `; +// TODO: Should these cursor styles be made a separate CSS files? I worry about having too many external stylesheets, but I know some users are not using `trix.css` and will miss out on these. +export const cursorStyles = css` +/** + * Cursor styles that are useful for providing a more "pleasant" editor experience. + */ +/** +* https://github.com/curvenote/editor/blob/main/packages/prosemirror-codemark/src/codemark.css +*/ +@keyframes rhino-blink { + 49% { + border-color: unset; + } + 50% { + border-color: Canvas; + } + 99% { + border-color: Canvas; + } +} +.rhino-editor .no-cursor { + caret-color: transparent; +} + +:where(.rhino-editor) .fake-cursor { + margin: 0; + padding: 0; + margin-right: -1px; + border-left-width: 1px; + border-left-style: solid; + animation: rhino-blink 1s; + animation-iteration-count: infinite; + position: relative; + z-index: 1; +} + +/** This is for actual "selection" which are highlighting more than 1 character. */ +:where(.rhino-editor .ProseMirror):not(:focus-within) .rhino-selection { + background: var(--rhino-fake-selection-color); +} + +/** .fake-cursor-selection is for link "insertions" without selected text. */ +:where(.rhino-editor) .rhino-fake-cursor-selection { + display: none; + user-select: none; +} + +/** +This is used for showing a fake cursor for selections like link insertions +*/ +:where(.rhino-editor)[link-dialog-expanded] .rhino-fake-cursor-selection { + margin: 0; + padding: 0; + margin-right: -1px; + margin-left: -2px; + border-left-width: 4px; + border-left-style: solid; + border-color: Highlight; + position: relative; + z-index: 1; + display: inline; +} + +.ProseMirror-separator { + display: none !important; +} +` + export const toolbarButtonStyles = css` .rhino-toolbar-button { appearance: none; diff --git a/src/exports/styles/trix-core.css b/src/exports/styles/trix-core.css index 234073c1..43a63069 100644 --- a/src/exports/styles/trix-core.css +++ b/src/exports/styles/trix-core.css @@ -1,66 +1,6 @@ /* These all come from Trix / ActionText. This should probably be cleaned up into a regular .css for users to include. */ -/* @import "prosemirror-codemark/dist/codemark.css"; */ - -/** -* https://github.com/curvenote/editor/blob/main/packages/prosemirror-codemark/src/codemark.css -*/ -@keyframes rhino-blink { - 49% { - border-color: unset; - } - 50% { - border-color: Canvas; - } - 99% { - border-color: Canvas; - } -} -.rhino-editor .no-cursor { - caret-color: transparent; -} - -.ProseMirror:not(:focus-within) .rhino-selection { - background: var(--rhino-fake-selection-color); -} - -.rhino-editor .fake-cursor { - margin: 0; - padding: 0; - margin-right: -1px; - border-left-width: 1px; - border-left-style: solid; - animation: rhino-blink 1s; - animation-iteration-count: infinite; - position: relative; - z-index: 1; -} - -.ProseMirror .fake-cursor-selection { - display: none; - user-select: none; -} - -/** -This is used for showing a fake cursor for selections like link insertions -*/ -.ProseMirror:not(:focus-within) .fake-cursor-selection { - margin: 0; - padding: 0; - margin-right: -2px; - margin-left: -2px; - border-left-width: 4px; - border-left-style: solid; - border-color: Highlight; - position: relative; - z-index: 1; - display: inline; -} - -.ProseMirror-separator { - display: none !important; -} .trix-content { border: 1px solid var(--rhino-border-color); diff --git a/src/exports/styles/trix.css b/src/exports/styles/trix.css index da08acc6..5d223406 100644 --- a/src/exports/styles/trix.css +++ b/src/exports/styles/trix.css @@ -2,66 +2,6 @@ /* These all come from Trix / ActionText. This should probably be cleaned up into a regular .css for users to include. */ -/* @import "prosemirror-codemark/dist/codemark.css"; */ - -/** -* https://github.com/curvenote/editor/blob/main/packages/prosemirror-codemark/src/codemark.css -*/ -@keyframes rhino-blink { - 49% { - border-color: unset; - } - 50% { - border-color: Canvas; - } - 99% { - border-color: Canvas; - } -} -.rhino-editor .no-cursor { - caret-color: transparent; -} - -.ProseMirror:not(:focus-within) .rhino-selection { - background: var(--rhino-fake-selection-color); -} - -.rhino-editor .fake-cursor { - margin: 0; - padding: 0; - margin-right: -1px; - border-left-width: 1px; - border-left-style: solid; - animation: rhino-blink 1s; - animation-iteration-count: infinite; - position: relative; - z-index: 1; -} - -.ProseMirror .fake-cursor-selection { - display: none; - user-select: none; -} - -/** -This is used for showing a fake cursor for selections like link insertions -*/ -.ProseMirror:not(:focus-within) .fake-cursor-selection { - margin: 0; - padding: 0; - margin-right: -2px; - margin-left: -2px; - border-left-width: 4px; - border-left-style: solid; - border-color: Highlight; - position: relative; - z-index: 1; - display: inline; -} - -.ProseMirror-separator { - display: none !important; -} .trix-content { border: 1px solid var(--rhino-border-color); @@ -459,6 +399,73 @@ This is used for showing a fake cursor for selections like link insertions } +/* src/exports/styles/editor.js:toolbarButtonStyles */ + +/** + * Cursor styles that are useful for providing a more "pleasant" editor experience. + */ +/** +* https://github.com/curvenote/editor/blob/main/packages/prosemirror-codemark/src/codemark.css +*/ +@keyframes rhino-blink { + 49% { + border-color: unset; + } + 50% { + border-color: Canvas; + } + 99% { + border-color: Canvas; + } +} +.rhino-editor .no-cursor { + caret-color: transparent; +} + +:where(.rhino-editor) .fake-cursor { + margin: 0; + padding: 0; + margin-right: -1px; + border-left-width: 1px; + border-left-style: solid; + animation: rhino-blink 1s; + animation-iteration-count: infinite; + position: relative; + z-index: 1; +} + +/** This is for actual "selection" which are highlighting more than 1 character. */ +:where(.rhino-editor .ProseMirror):not(:focus-within) .rhino-selection { + background: var(--rhino-fake-selection-color); +} + +/** .fake-cursor-selection is for link "insertions" without selected text. */ +:where(.rhino-editor) .rhino-fake-cursor-selection { + display: none; + user-select: none; +} + +/** +This is used for showing a fake cursor for selections like link insertions +*/ +:where(.rhino-editor)[link-dialog-expanded] .rhino-fake-cursor-selection { + margin: 0; + padding: 0; + margin-right: -1px; + margin-left: -2px; + border-left-width: 4px; + border-left-style: solid; + border-color: Highlight; + position: relative; + z-index: 1; + display: inline; +} + +.ProseMirror-separator { + display: none !important; +} + + /* src/exports/styles/editor.js:toolbarButtonStyles */ .rhino-toolbar-button {