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

feat(admin-ui): React useProseMirror hook #2675

Merged
merged 2 commits into from
Feb 20, 2024

Conversation

AleksanderBondar
Copy link
Contributor

@AleksanderBondar AleksanderBondar commented Feb 12, 2024

Description

Adding useProseMirror for React plugins.

Screenshots

image

Checklist

📌 Always:

  • I have set a clear title
  • My PR is small and contains a single feature
  • I have checked my own PR

👍 Most of the time:

  • I have added or updated test cases
  • I have updated the README if needed

PS. I am not pretty sure how to add it into docs (not sure about section where it should live) but if someone will pass me some instructions I can add it too :). But we should inform about missing css which need to be added into plugin to make prosemirror looking like everywhere.

@michaelbromley
Copy link
Member

michaelbromley commented Feb 14, 2024

Hi,

Thanks for this great work!

Do you think it is best to expose this as a hook, rather than a component? I'm not really a React dev so I'm not familiar with what would be more idiomatic. But to me I would think of this more as a component that you can drop in your template and pass some props to. Is there an advantage to exposing it as a hook?

Also I think I'd prefer it to be named more generically, like useRichTextEditor, because

  1. It better aligns with the Angular version, <vdr-rich-text-editor />
  2. "prose mirror" is an implementation detail and I think it could be confusing or harder to find since I'd guess a lot of devs have no idea what "prose mirror" is.

Regarding docs: you can run the docs:generate-typescript-docs script which will create an .md file based on the doc block you already included. You can then also commit that new file.

@AleksanderBondar
Copy link
Contributor Author

Hello :D.
I think it can be better to keep it as hook it gives more flexibility for users. Because probably when someone will build some form to collect data it needs to be collected in some kind of useState. So we have easy access to store it. I think that is not a huge problem to just print <div ref={ref} /> instead of importing a new component.

I agree with naming. Will change it today and try with docs.

If You would like to have a component too (I can create it). Maybe that is a good case to expose Component and Hook so people can decide what to use by their use cases.

@michaelbromley
Copy link
Member

The component would just be a thin wrapper around the hook right? Very much like the example component in the doc?

If so, then yeah I think it is nice to expose both, and include in the docs for each that there is an option depending on how much control is needed by the developer.

@AleksanderBondar
Copy link
Contributor Author

Yeap exactly it will be like thin wrapper around this hook. I will try to do this today and push it back to review :)

@michaelbromley michaelbromley merged commit 68e0fa5 into vendure-ecommerce:minor Feb 20, 2024
11 of 12 checks passed
@michaelbromley
Copy link
Member

Thank you!

@andriinuts
Copy link
Contributor

@AleksanderBondar looks like it's not loading default styles when you are using this component inside the widget when the page does not contain an angular component version with styles.

@AleksanderBondar
Copy link
Contributor Author

To be truth, I just copy and paste whole styling from base angular component.

.ProseMirror {
  position: relative;
}
.ProseMirror {
  word-wrap: break-word;
  white-space: pre-wrap;
  -webkit-font-variant-ligatures: none;
  font-variant-ligatures: none;
}
.ProseMirror pre {
  white-space: pre-wrap;
}
.ProseMirror li {
  position: relative;
}
.ProseMirror-hideselection *::selection {
  background: transparent;
}
.ProseMirror-hideselection *::-moz-selection {
  background: transparent;
}
.ProseMirror-hideselection {
  caret-color: transparent;
}
.ProseMirror-selectednode {
  outline: 2px solid var(--color-primary-500);
}
/* Make sure li selections wrap around markers */
li.ProseMirror-selectednode {
  outline: none;
}
li.ProseMirror-selectednode:after {
  content: "";
  position: absolute;
  left: -32px;
  right: -2px;
  top: -2px;
  bottom: -2px;
  border: 2px solid var(--color-primary-500);
  pointer-events: none;
}
.ProseMirror-textblock-dropdown {
  min-width: 3em;
}
.ProseMirror-menu {
  margin: 0 -4px;
  line-height: 1;
}
.ProseMirror-tooltip .ProseMirror-menu {
  width: -webkit-fit-content;
  width: fit-content;
  white-space: pre;
}
.ProseMirror-menuitem {
  margin-inline-end: 3px;
  display: inline-block;
}
.ProseMirror-menuseparator {
  border-inline-end: 1px solid var(--color-component-border-200);
  margin: 0 12px 0 8px;
  height: 18px;
}
.ProseMirror-menu-dropdown,
.ProseMirror-menu-dropdown-menu {
  font-size: 90%;
  white-space: nowrap;
  border-radius: var(--border-radius-input);
}
.ProseMirror-menu-dropdown {
  vertical-align: 1px;
  cursor: pointer;
  position: relative;
  padding-inline-end: 15px;
}
.ProseMirror-menu-dropdown-wrap {
  padding: 1px 3px 1px 6px;
  display: inline-block;
  position: relative;
}
.ProseMirror-menu-dropdown:after {
  content: "";
  border-inline-start: 4px solid transparent;
  border-inline-end: 4px solid transparent;
  border-top: 4px solid currentColor;
  opacity: 0.6;
  position: absolute;
  right: 4px;
  top: calc(50% - 2px);
}
.ProseMirror-menu-dropdown-menu,
.ProseMirror-menu-submenu {
  position: absolute;
  background: var(--color-component-bg-100);
  border: 1px solid var(--color-component-border-200);
  padding: 2px;
}
.ProseMirror-menu-dropdown-menu {
  z-index: 15;
  min-width: 6em;
  color: var(--color-text-200);
}
.ProseMirror-menu-dropdown-item {
  cursor: pointer;
  padding: 2px 8px 2px 4px;
}
.ProseMirror-menu-dropdown-item:hover {
  background: var(--color-component-bg-200);
}
.ProseMirror-menu-submenu-wrap {
  position: relative;
  margin-inline-end: 4px;
}
.ProseMirror-menu-submenu-label:after {
  content: "";
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  border-inline-start: 4px solid currentColor;
  opacity: 0.6;
  position: absolute;
  right: -8px;
  top: calc(50% - 4px);
}
.ProseMirror-menu-submenu {
  display: none;
  min-width: 4em;
  left: 100%;
  top: -3px;
}
.ProseMirror-menu-active {
  background: var(--color-component-bg-100);
  border-radius: 4px;
}
.ProseMirror-menu-active {
  background: var(--color-component-bg-100);
  border-radius: 4px;
}
.ProseMirror-menu-disabled {
  opacity: 0.3;
}
.ProseMirror-menu-submenu-wrap:hover .ProseMirror-menu-submenu,
.ProseMirror-menu-submenu-wrap-active .ProseMirror-menu-submenu {
  display: block;
}
.ProseMirror-menubar {
  border-top-left-radius: inherit;
  border-top-right-radius: inherit;
  position: relative;
  min-height: 1em;
  color: var(--color-grey-600);
  padding: 1px 6px;
  top: 0;
  left: 0;
  right: 0;
  background: var(--color-component-bg-100);
  z-index: 10;
  -moz-box-sizing: border-box;
  box-sizing: border-box;
  overflow: visible;
  align-items: center;
}
.ProseMirror-menubar {
  position: sticky;
  border: 1px solid var(--color-weight-200);
  border-bottom: none;
  background-color: var(--color-component-bg-200);
  color: var(--color-icon-button);
  border-radius: var(--border-radius-input) var(--border-radius-input) 0 0;
  padding: 6px 12px;
  display: flex;
  flex-wrap: wrap;
}
.ProseMirror-icon {
  display: inline-block;
  line-height: 0.8;
  vertical-align: -2px;
  /* Compensate for padding */
  padding: 2px 8px;
  cursor: pointer;
}
.ProseMirror-menu-disabled.ProseMirror-icon {
  cursor: default;
}
.ProseMirror-icon svg {
  fill: currentColor;
  height: 1em;
}
.ProseMirror-icon span {
  vertical-align: text-top;
}
.ProseMirror-gapcursor {
  display: none;
  pointer-events: none;
  position: absolute;
}
.ProseMirror-gapcursor:after {
  content: "";
  display: block;
  position: absolute;
  top: -2px;
  width: 20px;
  border-top: 1px solid black;
  animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
}
@keyframes ProseMirror-cursor-blink {
  to {
    visibility: hidden;
  }
}
.ProseMirror-focused .ProseMirror-gapcursor {
  display: block;
}
.ProseMirror ul,
.ProseMirror ol {
  padding-inline-start: 30px;
  list-style-position: initial;
}
.ProseMirror blockquote {
  padding-inline-start: 1em;
  border-inline-start: 3px solid var(--color-grey-100);
  margin-inline-start: 0;
  margin-inline-end: 0;
}
.ProseMirror-prompt {
  background: white;
  padding: 5px 10px 5px 15px;
  border: 1px solid silver;
  position: fixed;
  border-radius: 3px;
  z-index: 11;
  box-shadow: -0.5px 2px 5px rgba(0, 0, 0, 0.2);
}
.ProseMirror-prompt h5 {
  margin: 0;
  font-weight: normal;
  font-size: 100%;
  color: var(--color-grey-500);
}
.ProseMirror-prompt input[type="text"],
.ProseMirror-prompt textarea {
  background: var(--color-component-bg-100);
  border: none;
  outline: none;
}
.ProseMirror-prompt input[type="text"] {
  padding: 0 4px;
}
.ProseMirror-prompt-close {
  position: absolute;
  left: 2px;
  top: 1px;
  color: var(--color-grey-400);
  border: none;
  background: transparent;
  padding: 0;
}
.ProseMirror-prompt-close:after {
  content: "✕";
  font-size: 12px;
}
.ProseMirror-invalid {
  background: var(--color-warning-200);
  border: 1px solid var(--color-warning-300);
  border-radius: 4px;
  padding: 5px 10px;
  position: absolute;
  min-width: 10em;
}
.ProseMirror-prompt-buttons {
  margin-top: 5px;
  display: none;
}
#editor,
.editor {
  background: var(--color-form-input-bg);
  color: black;
  background-clip: padding-box;
  border-radius: 4px;
  border: 2px solid rgba(0, 0, 0, 0.2);
  padding: 5px 0;
  margin-bottom: 23px;
}
.ProseMirror p:first-child,
.ProseMirror h1:first-child,
.ProseMirror h2:first-child,
.ProseMirror h3:first-child,
.ProseMirror h4:first-child,
.ProseMirror h5:first-child,
.ProseMirror h6:first-child {
  margin-top: 10px;
}
.ProseMirror {
  padding: 4px 8px 4px 14px;
  line-height: 1.2;
  outline: none;
}
.ProseMirror p {
  margin-bottom: 0.5rem;
  color: var(--color-text-100) !important;
}
.ProseMirror .tableWrapper td,
.ProseMirror .tableWrapper th {
  border: 1px solid var(--color-grey-300);
  padding: 3px 6px;
}
.ProseMirror .tableWrapper td p,
.ProseMirror .tableWrapper th p {
  margin-top: 0;
}
.ProseMirror .tableWrapper th,
.ProseMirror .tableWrapper th p {
  font-weight: bold;
}
.ProseMirror table {
  border-collapse: collapse;
  table-layout: fixed;
  width: 100%;
  overflow: hidden;
}
.ProseMirror td,
.ProseMirror th {
  vertical-align: top;
  box-sizing: border-box;
  position: relative;
}
.ProseMirror .column-resize-handle {
  position: absolute;
  right: -2px;
  top: 0;
  bottom: 0;
  width: 4px;
  z-index: 20;
  background-color: #adf;
  pointer-events: none;
}
.ProseMirror.resize-cursor {
  cursor: ew-resize;
  cursor: col-resize;
}
/* Give selected cells a blue overlay */
.ProseMirror .selectedCell:after {
  z-index: 2;
  position: absolute;
  content: "";
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  background: #afdaf3 55;
  pointer-events: none;
}
.menu-separator {
  border-bottom: 1px solid var(--color-grey-400);
  height: 0;
  margin: 6px 0;
  pointer-events: none;
}
.menu-item-with-icon {
  display: flex;
  align-items: center;
}
.menu-item-with-icon clr-icon,
.menu-item-with-icon .custom-icon {
  margin-inline-end: 4px;
  color: var(--color-text-200);
}
.menu-item-with-icon .hr-icon {
  width: 13px;
  height: 8px;
  border-bottom: 2px solid var(--color-text-100);
  margin: -8px 5px 0 2px;
}
.menu-item-with-icon .h-icon {
  width: 16px;
  text-align: center;
  font-weight: bold;
  font-size: 12px;
}
.context-menu {
  position: fixed;
}

label.rich-text-label {
  font-size: var(--font-size-sm);
  color: var(--font-weight-700);
}
.ProseMirror-menubar {
  position: sticky;
  border: 1px solid var(--color-weight-200);
  border-bottom: none;
  background-color: var(--color-component-bg-200);
  color: var(--color-icon-button);
  border-radius: var(--border-radius-input) var(--border-radius-input) 0 0;
  padding: 6px 12px;
  display: flex;
  flex-wrap: wrap;
}
.vdr-prosemirror {
  background: var(--color-form-input-bg);
  color: var(--color-text-100);
  min-height: 128px;
  max-height: 600px;
  min-width: 200px;
  border: 1px solid var(--color-weight-200);
  border-radius: 0 0 var(--border-radius-input) var(--border-radius-input);
  transition: border-color 0.2s;
  overflow: auto;
  text-align: initial;
  /* Add space around the hr to make clicking it easier */
}
.vdr-prosemirror:focus {
  border-color: var(--color-primary-500) !important;
  box-shadow: 0 0 1px 1px var(--color-primary-100);
}
.vdr-prosemirror hr {
  padding: 2px 10px;
  border: none;
  margin: 1em 0;
}
.vdr-prosemirror hr:after {
  content: "";
  display: block;
  height: 1px;
  background-color: silver;
  line-height: 2px;
}
.vdr-prosemirror img {
  cursor: default;
  max-width: 100%;
}
.vdr-prosemirror a:link,
.vdr-prosemirror a:visited {
  color: var(--color-primary-700);
  text-decoration: none;
}
.vdr-prosemirror .iframe-wrapper {
  width: 100%;
  text-align: center;
  padding: 6px;
  transition: background-color 0.3s;
}
.vdr-prosemirror .iframe-wrapper:hover {
  background-color: var(--color-primary-100);
}

I can try to implement it too in this component.

@andriinuts
Copy link
Contributor

I guess makes sense to move these styles to shared or also include them with the react component because for now, this component is not usable. If you will need any help give me know.

@AleksanderBondar
Copy link
Contributor Author

I have a bit of a problem with accessibility. The fastest I can fix it is sometime during the weekend. If You want to do that go ahead.

@andriinuts
Copy link
Contributor

I'm also not sure when I'll have time to do it, if I'm free I'll let you know not to do the same things

@AleksanderBondar
Copy link
Contributor Author

+1 here. I will put a message here when I got time for that, but glad to hear some1 is using it :P

@andriinuts
Copy link
Contributor

and also probably there is a bug with RichTextEditor as you are not setting the name property to input, so the form is not registering it.

@AleksanderBondar
Copy link
Contributor Author

Will check that

@phuoymakara
Copy link

@AleksanderBondar Hello, how can I set the default value on RichTextEditor?

@AleksanderBondar
Copy link
Contributor Author

Hello there :). You should get editor instance

const { ref, editor } = useRichTextEditor({
   isReadOnly: () => false,
   onTextInput: (text) => updateSummaries({ main: text }),
});

Then I am setting default values by useEffect

useEffect(() => {
   editor.update("Some default");
}, []);

Probably this weekend I will get some time to update this hook with css and other fixes.

@phuoymakara
Copy link

@AleksanderBondar Thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants