Merge branch 'main' into develop

This commit is contained in:
Nick the Sick 2024-06-13 07:05:09 +02:00
commit ca2a2284f0
No known key found for this signature in database
GPG Key ID: F575992F156E5BCC
310 changed files with 11474 additions and 5716 deletions

View File

@ -17,6 +17,7 @@
<div class="overflow-hidden"> <div class="overflow-hidden">
<div <div
class="bg-white" class="bg-white"
:class="[hidePreview ? 'hidden' : '']"
> >
<demo-frame <demo-frame
:src="currentIframeUrl" :src="currentIframeUrl"
@ -140,6 +141,10 @@ export default {
return this.query.hideSource || false return this.query.hideSource || false
}, },
hidePreview() {
return this.query.hidePreview || false
},
githubUrl() { githubUrl() {
return `https://github.com/ueberdosis/tiptap/tree/main/demos/src/${this.name}` return `https://github.com/ueberdosis/tiptap/tree/main/demos/src/${this.name}`
}, },

View File

@ -1,4 +1,23 @@
$colorBlack: #000; /* Base HTML and global element styles*/
:root {
--white: #FFF;
--black: #2E2B29;
--black-contrast: #110F0E;
--gray-1: rgba(61, 37, 20, 0.05);
--gray-2: rgba(61, 37, 20, 0.08);
--gray-3: rgba(61, 37, 20, 0.12);
--gray-4: rgba(53, 38, 28, 0.30);
--gray-5: rgba(28, 25, 23, 0.60);
--purple: #6A00F5;
--purple-contrast: #5800CC;
--purple-light: rgba(88, 5, 255, 0.05);
--yellow-contrast: #FACC15;
--yellow: rgba(250, 204, 21, 0.4);
--yellow-light: #FFFAE5;
--red: #FF5C33;
--red-light: #FFEBE5;
--shadow: 0px 12px 33px 0px rgba(0, 0, 0, 0.06), 0px 3.618px 9.949px 0px rgba(0, 0, 0, 0.04);
}
*, *,
*::before, *::before,
@ -9,32 +28,46 @@ $colorBlack: #000;
html { html {
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
line-height: 1.5; line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
} }
body { body {
margin: 1rem;
min-height: 10rem; min-height: 10rem;
margin: 0;
} }
:first-child {
margin-top: 0;
}
.tiptap {
caret-color: var(--purple);
margin: 1.5rem;
&:focus {
outline: none;
}
}
/* Custom scrollbar styles */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 14px;
height: 14px; height: 14px;
width: 14px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
border: 4px solid transparent;
background-clip: padding-box; background-clip: padding-box;
border-radius: 8px;
background-color: transparent; background-color: transparent;
border: 4px solid transparent;
border-radius: 8px;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
border: 4px solid rgba(0, 0, 0, 0);
background-clip: padding-box; background-clip: padding-box;
border-radius: 8px;
background-color: rgba(0, 0, 0, 0); background-color: rgba(0, 0, 0, 0);
border: 4px solid rgba(0, 0, 0, 0);
border-radius: 8px;
} }
:hover::-webkit-scrollbar-thumb { :hover::-webkit-scrollbar-thumb {
@ -47,37 +80,276 @@ body {
::-webkit-scrollbar-button { ::-webkit-scrollbar-button {
display: none; display: none;
width: 0;
height: 0; height: 0;
width: 0;
} }
::-webkit-scrollbar-corner { ::-webkit-scrollbar-corner {
background-color: transparent; background-color: transparent;
} }
/* Specific element and component styles */
button, button,
input, input,
select { select,
font-size: inherit; textarea {
background: var(--gray-2);
border-radius: 0.5rem;
border: none;
color: var(--black);
font-family: inherit; font-family: inherit;
color: black; font-size: 0.875rem;
margin: 0.1rem; font-weight: 500;
border: 1px solid black; line-height: 1.15;
border-radius: 0.3rem; margin: none;
padding: 0.1rem 0.4rem; padding: 0.375rem 0.625rem;
background: white; transition: all 0.2s cubic-bezier(0.65,0.05,0.36,1);
accent-color: black;
&:hover {
background-color: var(--gray-3);
color: var(--black-contrast);
}
&[disabled] { &[disabled] {
opacity: 0.3; background: var(--gray-1);
color: var(--gray-4);
}
&:checked {
accent-color: var(--purple);
}
&.primary {
background: var(--black);
color: var(--white);
&:hover {
background-color: var(--black-contrast);
}
&[disabled] {
background: var(--gray-1);
color: var(--gray-4);
}
}
&.is-active {
background: var(--purple);
color: var(--white);
&:hover {
background-color: var(--purple-contrast);
color: var(--white);
}
} }
} }
.tiptap:focus { button:not([disabled]),
outline: none; select:not([disabled]) {
cursor: pointer;
} }
.is-active { input[type="text"],
background: black; textarea {
color: white; background-color: unset;
border: 1px solid var(--gray-3);
border-radius: 0.5rem;
color: var(--black);
&::placeholder {
color: var(--gray-4);
}
&:hover {
background-color: unset;
border-color: var(--gray-4);
}
&:focus-visible, &:focus {
border-color: var(--purple);
outline: none;
}
} }
select {
/* reset */
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="Gray" d="M7 10l5 5 5-5z"/></svg>');
background-repeat: no-repeat;
background-position: right 0.1rem center;
background-size: 1.25rem 1.25rem;
padding-right: 1.25rem;
&:focus {
outline: 0;
}
}
form {
align-items: flex-start;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.hint {
align-items: center;
background-color: var(--yellow-light);
border-radius: 0.5rem;
border: 1px solid var(--gray-2);
display: flex;
flex-direction: row;
font-size: 0.75rem;
gap: 0.5rem;
line-height: 1.15;
min-height: 1.75rem;
padding: 0.25rem 0.5rem;
&.purple-spinner,
&.error {
justify-content: center;
text-align: center;
width: 100%;
}
&.purple-spinner {
background-color: var(--purple-light);
&::after {
content: "";
background-image: url("data:image/svg+xml;utf8,<svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='28px' height='30px' viewBox='0 0 24 30' style='enable-background:new 0 0 50 50;' xml:space='preserve'><rect x='0' y='10' width='6' height='10' fill='%236A00F5' rx='3' ry='3'><animateTransform attributeType='xml' attributeName='transform' type='translate' values='0 0; 0 5; 0 -5; 0 0' begin='0' dur='0.6s' repeatCount='indefinite'/></rect><rect x='10' y='10' width='6' height='10' fill='%236A00F5' rx='3' ry='3'><animateTransform attributeType='xml' attributeName='transform' type='translate' values='0 0; 0 5; 0 -5; 0 0' begin='0.2s' dur='0.6s' repeatCount='indefinite'/></rect><rect x='20' y='10' width='6' height='10' fill='%236A00F5' rx='3' ry='3'><animateTransform attributeType='xml' attributeName='transform' type='translate' values='0 0; 0 5; 0 -5; 0 0' begin='0.4s' dur='0.6s' repeatCount='indefinite'/></rect></svg>");
background-size: cover;
background-repeat: no-repeat;
background-position: center;
height: 1rem;
width: 1rem;
}
}
&.error {
background-color: var(--red-light);
}
}
.label,
.label-small,
.label-large {
color: var(--black);
font-size: 0.8125rem;
font-weight: 500;
line-height: 1.15;
}
.label-small {
color: var(--gray-5);
font-size: 0.75rem;
font-weight: 400;
}
.label-large {
font-size: 0.875rem;
font-weight: 700;
}
hr {
border: none;
border-top: 1px solid var(--gray-3);
margin: 0;
width: 100%;
}
/* Layout and structure */
#app,
.container {
display: flex;
flex-direction: column;
}
.button-group {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
}
.control-group {
align-items: flex-start;
display: flex;
flex-direction: column;
gap: 1rem;
margin: 1.5rem;
}
[data-node-view-wrapper] > .control-group {
margin: 1.5rem 0;
}
.flex-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 1rem;
justify-content: space-between;
width: 100%;
}
.switch-group {
align-items: center;
background: var(--gray-2);
border-radius: 0.5rem;
display: flex;
flex-direction: row;
flex-wrap: wrap;
flex: 0 1 auto;
justify-content: flex-start;
padding: 0.125rem;
label {
align-items: center;
border-radius: 0.375rem;
color: var(--gray-5);
cursor: pointer;
display: flex;
flex-direction: row;
font-size: 0.75rem;
font-weight: 500;
gap: 0.25rem;
line-height: 1.15;
min-height: 1.5rem;
padding: 0 0.375rem;
transition: all 0.2s cubic-bezier(0.65,0.05,0.36,1);
&:has(input:checked) {
background-color: var(--white);
color: var(--black-contrast);
}
&:hover {
color: var(--black);
}
input {
display: none;
margin: unset;
}
}
}
.output-group {
background-color: var(--gray-1);
display: flex;
flex-direction: column;
font-family: 'JetBrainsMono', monospace;
font-size: 0.75rem;
gap: 1rem;
margin-top: 2.5rem;
padding: 1.5rem;
label {
color: var(--black);
font-size: 0.875rem;
font-weight: 700;
line-height: 1.15;
}
}

View File

@ -21,10 +21,12 @@ const MenuBar = ({ editor }) => {
}, [editor]) }, [editor])
return ( return (
<> <div className="control-group">
<button onClick={onCutToStart}>Cut content to start of document</button> <div className="button-group">
<button onClick={onCutToEnd}>Cut content to end of document</button> <button onClick={onCutToStart}>Cut content to start of document</button>
</> <button onClick={onCutToEnd}>Cut content to end of document</button>
</div>
</div>
) )
} }
@ -49,7 +51,7 @@ export default () => {
Hi there, Hi there,
</h2> </h2>
<p> <p>
this is a <em>basic</em> example of <strong>tiptap</strong>. Sure, there are all kind of basic text styles youd probably expect from a text editor. But wait until you see the lists: this is a <em>basic</em> example of <strong>Tiptap</strong>. Sure, there are all kind of basic text styles youd probably expect from a text editor. But wait until you see the lists:
</p> </p>
<ul> <ul>
<li> <li>
@ -77,9 +79,9 @@ export default () => {
}) })
return ( return (
<div> <>
<MenuBar editor={editor} /> <MenuBar editor={editor} />
<EditorContent editor={editor} /> <EditorContent editor={editor} />
</div> </>
) )
} }

View File

@ -21,41 +21,41 @@ context('/src/Examples/Default/React/', () => {
}) })
const buttonMarks = [ const buttonMarks = [
{ label: 'bold', tag: 'strong' }, { label: 'Bold', tag: 'strong' },
{ label: 'italic', tag: 'em' }, { label: 'Italic', tag: 'em' },
{ label: 'strike', tag: 's' }, { label: 'Strike', tag: 's' },
] ]
buttonMarks.forEach(m => { buttonMarks.forEach(m => {
it(`should disable ${m.label} when the code tag is enabled for cursor`, () => { it(`should disable ${m.label} when the code tag is enabled for cursor`, () => {
cy.get('.tiptap').type('{selectall}Hello world') cy.get('.tiptap').type('{selectall}Hello world')
cy.get('button').contains('code').click() cy.get('button').contains('Code').click()
cy.get('button').contains(m.label).should('be.disabled') cy.get('button').contains(m.label).should('be.disabled')
}) })
it(`should enable ${m.label} when the code tag is disabled for cursor`, () => { it(`should enable ${m.label} when the code tag is disabled for cursor`, () => {
cy.get('.tiptap').type('{selectall}Hello world') cy.get('.tiptap').type('{selectall}Hello world')
cy.get('button').contains('code').click() cy.get('button').contains('Code').click()
cy.get('button').contains('code').click() cy.get('button').contains('Code').click()
cy.get('button').contains(m.label).should('not.be.disabled') cy.get('button').contains(m.label).should('not.be.disabled')
}) })
it(`should disable ${m.label} when the code tag is enabled for selection`, () => { it(`should disable ${m.label} when the code tag is enabled for selection`, () => {
cy.get('.tiptap').type('{selectall}Hello world{selectall}') cy.get('.tiptap').type('{selectall}Hello world{selectall}')
cy.get('button').contains('code').click() cy.get('button').contains('Code').click()
cy.get('button').contains(m.label).should('be.disabled') cy.get('button').contains(m.label).should('be.disabled')
}) })
it(`should enable ${m.label} when the code tag is disabled for selection`, () => { it(`should enable ${m.label} when the code tag is disabled for selection`, () => {
cy.get('.tiptap').type('{selectall}Hello world{selectall}') cy.get('.tiptap').type('{selectall}Hello world{selectall}')
cy.get('button').contains('code').click() cy.get('button').contains('Code').click()
cy.get('button').contains('code').click() cy.get('button').contains('Code').click()
cy.get('button').contains(m.label).should('not.be.disabled') cy.get('button').contains(m.label).should('not.be.disabled')
}) })
it(`should apply ${m.label} when the button is pressed`, () => { it(`should apply ${m.label} when the button is pressed`, () => {
cy.get('.tiptap').type('{selectall}Hello world') cy.get('.tiptap').type('{selectall}Hello world')
cy.get('button').contains('paragraph').click() cy.get('button').contains('Paragraph').click()
cy.get('.tiptap').type('{selectall}') cy.get('.tiptap').type('{selectall}')
cy.get('button').contains(m.label).click() cy.get('button').contains(m.label).click()
cy.get(`.tiptap ${m.tag}`).should('exist').should('have.text', 'Hello world') cy.get(`.tiptap ${m.tag}`).should('exist').should('have.text', 'Hello world')
@ -64,40 +64,40 @@ context('/src/Examples/Default/React/', () => {
it('should clear marks when the button is pressed', () => { it('should clear marks when the button is pressed', () => {
cy.get('.tiptap').type('{selectall}Hello world') cy.get('.tiptap').type('{selectall}Hello world')
cy.get('button').contains('paragraph').click() cy.get('button').contains('Paragraph').click()
cy.get('.tiptap').type('{selectall}') cy.get('.tiptap').type('{selectall}')
cy.get('button').contains('bold').click() cy.get('button').contains('Bold').click()
cy.get('.tiptap strong').should('exist').should('have.text', 'Hello world') cy.get('.tiptap strong').should('exist').should('have.text', 'Hello world')
cy.get('button').contains('clear marks').click() cy.get('button').contains('Clear marks').click()
cy.get('.tiptap strong').should('not.exist') cy.get('.tiptap strong').should('not.exist')
}) })
it('should clear nodes when the button is pressed', () => { it('should clear nodes when the button is pressed', () => {
cy.get('.tiptap').type('{selectall}Hello world') cy.get('.tiptap').type('{selectall}Hello world')
cy.get('button').contains('bullet list').click() cy.get('button').contains('Bullet list').click()
cy.get('.tiptap ul').should('exist').should('have.text', 'Hello world') cy.get('.tiptap ul').should('exist').should('have.text', 'Hello world')
cy.get('.tiptap').type('{enter}A second item{enter}A third item{selectall}') cy.get('.tiptap').type('{enter}A second item{enter}A third item{selectall}')
cy.get('button').contains('clear nodes').click() cy.get('button').contains('Clear nodes').click()
cy.get('.tiptap ul').should('not.exist') cy.get('.tiptap ul').should('not.exist')
cy.get('.tiptap p').should('have.length', 3) cy.get('.tiptap p').should('have.length', 3)
}) })
const buttonNodes = [ const buttonNodes = [
{ label: 'h1', tag: 'h1' }, { label: 'H1', tag: 'h1' },
{ label: 'h2', tag: 'h2' }, { label: 'H2', tag: 'h2' },
{ label: 'h3', tag: 'h3' }, { label: 'H3', tag: 'h3' },
{ label: 'h4', tag: 'h4' }, { label: 'H4', tag: 'h4' },
{ label: 'h5', tag: 'h5' }, { label: 'H5', tag: 'h5' },
{ label: 'h6', tag: 'h6' }, { label: 'H6', tag: 'h6' },
{ label: 'bullet list', tag: 'ul' }, { label: 'Bullet list', tag: 'ul' },
{ label: 'ordered list', tag: 'ol' }, { label: 'Ordered list', tag: 'ol' },
{ label: 'code block', tag: 'pre code' }, { label: 'Code block', tag: 'pre code' },
{ label: 'blockquote', tag: 'blockquote' }, { label: 'Blockquote', tag: 'blockquote' },
] ]
buttonNodes.forEach(n => { buttonNodes.forEach(n => {
it(`should set ${n.label} when the button is pressed`, () => { it(`should set ${n.label} when the button is pressed`, () => {
cy.get('button').contains('paragraph').click() cy.get('button').contains('Paragraph').click()
cy.get('.tiptap').type('{selectall}Hello world{selectall}') cy.get('.tiptap').type('{selectall}Hello world{selectall}')
cy.get('button').contains(n.label).click() cy.get('button').contains(n.label).click()
@ -109,35 +109,35 @@ context('/src/Examples/Default/React/', () => {
it('should add a hr when on the same line as a node', () => { it('should add a hr when on the same line as a node', () => {
cy.get('.tiptap').type('{rightArrow}') cy.get('.tiptap').type('{rightArrow}')
cy.get('button').contains('horizontal rule').click() cy.get('button').contains('Horizontal rule').click()
cy.get('.tiptap hr').should('exist') cy.get('.tiptap hr').should('exist')
cy.get('.tiptap h1').should('exist') cy.get('.tiptap h1').should('exist')
}) })
it('should add a hr when on a new line', () => { it('should add a hr when on a new line', () => {
cy.get('.tiptap').type('{rightArrow}{enter}') cy.get('.tiptap').type('{rightArrow}{enter}')
cy.get('button').contains('horizontal rule').click() cy.get('button').contains('Horizontal rule').click()
cy.get('.tiptap hr').should('exist') cy.get('.tiptap hr').should('exist')
cy.get('.tiptap h1').should('exist') cy.get('.tiptap h1').should('exist')
}) })
it('should add a br', () => { it('should add a br', () => {
cy.get('.tiptap').type('{rightArrow}') cy.get('.tiptap').type('{rightArrow}')
cy.get('button').contains('hard break').click() cy.get('button').contains('Hard break').click()
cy.get('.tiptap h1 br').should('exist') cy.get('.tiptap h1 br').should('exist')
}) })
it('should undo', () => { it('should undo', () => {
cy.get('.tiptap').type('{selectall}{backspace}') cy.get('.tiptap').type('{selectall}{backspace}')
cy.get('button').contains('undo').click() cy.get('button').contains('Undo').click()
cy.get('.tiptap').should('contain', 'Hello world') cy.get('.tiptap').should('contain', 'Hello world')
}) })
it('should redo', () => { it('should redo', () => {
cy.get('.tiptap').type('{selectall}{backspace}') cy.get('.tiptap').type('{selectall}{backspace}')
cy.get('button').contains('undo').click() cy.get('button').contains('Undo').click()
cy.get('.tiptap').should('contain', 'Hello world') cy.get('.tiptap').should('contain', 'Hello world')
cy.get('button').contains('redo').click() cy.get('button').contains('Redo').click()
cy.get('.tiptap').should('not.contain', 'Hello world') cy.get('.tiptap').should('not.contain', 'Hello world')
}) })
}) })

View File

@ -1,56 +1,91 @@
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
ul, /* List styles */
ul,
ol { ol {
padding: 0 1rem; padding: 0 1rem;
} margin: 1.25rem 1rem 1.25rem 0.4rem;
h1, li p {
h2, margin-top: 0.25em;
h3, margin-bottom: 0.25em;
h4,
h5,
h6 {
line-height: 1.1;
}
code {
background-color: rgba(#616161, 0.1);
color: #616161;
}
pre {
background: #0D0D0D;
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
} }
} }
img { /* Heading styles */
max-width: 100%; h1,
height: auto; h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
}
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code {
background-color: var(--purple-light);
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}
pre {
background: var(--black);
border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
} }
blockquote { blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem; padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
} }
hr { hr {
border: none; border: none;
border-top: 2px solid rgba(#0D0D0D, 0.1); border-top: 1px solid var(--gray-2);
margin: 2rem 0; margin: 2rem 0;
} }
} }

View File

@ -29,17 +29,19 @@ const MenuBar = () => {
} }
return ( return (
<> <div className="control-group">
<button data-test-id="html-content" onClick={() => editor.chain().insertContent(htmlContent).focus().run()}> <div className="button-group">
Insert html content <button data-test-id="html-content" onClick={() => editor.chain().insertContent(htmlContent).focus().run()}>
</button> Insert HTML content
<button data-test-id="html-content-spans" onClick={() => editor.chain().insertContent('<p><b>Hello</b> <i>World</i></p>').focus().run()}> </button>
Insert html with span tags content <button data-test-id="html-content-spans" onClick={() => editor.chain().insertContent('<p><b>Hello</b> <i>World</i></p>').focus().run()}>
</button> Insert HTML with span tags content
<button data-test-id="text-content" onClick={() => editor.chain().insertContent(textContent).focus().run()}> </button>
Insert text content <button data-test-id="text-content" onClick={() => editor.chain().insertContent(textContent).focus().run()}>
</button> Insert text content
</> </button>
</div>
</div>
) )
} }

View File

@ -1,56 +1,91 @@
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
ul, /* List styles */
ul,
ol { ol {
padding: 0 1rem; padding: 0 1rem;
} margin: 1.25rem 1rem 1.25rem 0.4rem;
h1, li p {
h2, margin-top: 0.25em;
h3, margin-bottom: 0.25em;
h4,
h5,
h6 {
line-height: 1.1;
}
code {
background-color: rgba(#616161, 0.1);
color: #616161;
}
pre {
background: #0D0D0D;
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
} }
} }
img { /* Heading styles */
max-width: 100%; h1,
height: auto; h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
}
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code {
background-color: var(--purple-light);
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}
pre {
background: var(--black);
border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
} }
blockquote { blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem; padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
} }
hr { hr {
border: none; border: none;
border-top: 2px solid rgba(#0D0D0D, 0.1); border-top: 1px solid var(--gray-2);
margin: 2rem 0; margin: 2rem 0;
} }
} }

View File

@ -14,154 +14,152 @@ const MenuBar = () => {
} }
return ( return (
<> <div className="control-group">
<div> <div>
<button <label>
onClick={() => setUseInputRules(prev => !prev)} <input
style={{ type="checkbox"
color: useInputRules ? 'white' : 'inherit', checked={useInputRules}
backgroundColor: useInputRules ? 'black' : 'transparent', onChange={() => setUseInputRules(prev => !prev)}
}} />
> Apply input rules
Apply Input Rules </label>
</button> <label>
<button <input
onClick={() => setUsePasteRules(prev => !prev)} type="checkbox"
style={{ checked={usePasteRules}
color: usePasteRules ? 'white' : 'inherit', onChange={() => setUsePasteRules(prev => !prev)}
backgroundColor: usePasteRules ? 'black' : 'transparent', />
}} Apply paste rules
> </label>
Apply Paste Rules
</button>
</div> </div>
<br /> <div className="button-group">
<button
onClick={() => {
editor
.chain()
.insertContent('-', {
applyInputRules: useInputRules,
applyPasteRules: usePasteRules,
})
.focus()
.run()
}}
data-test-1
>
Insert "-"
</button>
<button
onClick={() => {
editor
.chain()
.insertContent(' ', {
applyInputRules: useInputRules,
applyPasteRules: usePasteRules,
})
.focus()
.run()
}}
data-test-2
>
Insert " "
</button>
<button
onClick={() => {
editor
.chain()
.insertContent('A', {
applyInputRules: useInputRules,
applyPasteRules: usePasteRules,
})
.focus()
.run()
}}
data-test-3
>
Insert "A"
</button>
<button <br />
onClick={() => {
editor
.chain()
.insertContent('-', {
applyInputRules: useInputRules,
applyPasteRules: usePasteRules,
})
.focus()
.run()
}}
data-test-1
>
Insert "-"
</button>
<button
onClick={() => {
editor
.chain()
.insertContent(' ', {
applyInputRules: useInputRules,
applyPasteRules: usePasteRules,
})
.focus()
.run()
}}
data-test-2
>
Insert " "
</button>
<button
onClick={() => {
editor
.chain()
.insertContent('A', {
applyInputRules: useInputRules,
applyPasteRules: usePasteRules,
})
.focus()
.run()
}}
data-test-3
>
Insert "A"
</button>
<br /> <button
onClick={() => {
<button editor
onClick={() => { .chain()
editor .insertContent('*this is', {
.chain() applyInputRules: useInputRules,
.insertContent('*this is', { applyPasteRules: usePasteRules,
applyInputRules: useInputRules, })
applyPasteRules: usePasteRules, .focus()
}) .run()
.focus() }}
.run() data-test-4
}} >
data-test-4 Insert "*this is"
> </button>
Insert "*this is" <button
</button> onClick={() => {
<button editor
onClick={() => { .chain()
editor .insertContent(' a test*', {
.chain() applyInputRules: useInputRules,
.insertContent(' a test*', { applyPasteRules: usePasteRules,
applyInputRules: useInputRules, })
applyPasteRules: usePasteRules, .focus()
}) .run()
.focus() }}
.run() data-test-5
}} >
data-test-5 Insert " a test*"
> </button>
Insert " a test*" <button
</button> onClick={() => {
<button editor
onClick={() => { .chain()
editor .insertContent(' a test*, whooho', {
.chain() applyInputRules: useInputRules,
.insertContent(' a test*, whooho', { applyPasteRules: usePasteRules,
applyInputRules: useInputRules, })
applyPasteRules: usePasteRules, .focus()
}) .run()
.focus() }}
.run() data-test-6
}} >
data-test-6 Insert " a test*, whooho."
> </button>
Insert " a test*, whooho." <button
</button> onClick={() => {
<button editor
onClick={() => { .chain()
editor .insertContent('*this is a test*, whooho.', {
.chain() applyInputRules: useInputRules,
.insertContent('*this is a test*, whooho.', { applyPasteRules: usePasteRules,
applyInputRules: useInputRules, })
applyPasteRules: usePasteRules, .focus()
}) .run()
.focus() }}
.run() data-test-7
}} >
data-test-7 Insert "*this is a test*, whooho."
> </button>
Insert "*this is a test*, whooho." <button
</button> onClick={() => {
<button editor
onClick={() => { .chain()
editor .insertContent('*this is a test*', {
.chain() applyInputRules: useInputRules,
.insertContent('*this is a test*', { applyPasteRules: usePasteRules,
applyInputRules: useInputRules, })
applyPasteRules: usePasteRules, .focus()
}) .run()
.focus() }}
.run() data-test-8
}} >
data-test-8 Insert "*this is a test*"
> </button>
Insert "*this is a test*" </div>
</button> </div>
</>
) )
} }

View File

@ -1,56 +1,91 @@
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
ul, /* List styles */
ul,
ol { ol {
padding: 0 1rem; padding: 0 1rem;
} margin: 1.25rem 1rem 1.25rem 0.4rem;
h1, li p {
h2, margin-top: 0.25em;
h3, margin-bottom: 0.25em;
h4,
h5,
h6 {
line-height: 1.1;
}
code {
background-color: rgba(#616161, 0.1);
color: #616161;
}
pre {
background: #0D0D0D;
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
} }
} }
img { /* Heading styles */
max-width: 100%; h1,
height: auto; h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
}
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code {
background-color: var(--purple-light);
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}
pre {
background: var(--black);
border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
} }
blockquote { blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem; padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
} }
hr { hr {
border: none; border: none;
border-top: 2px solid rgba(#0D0D0D, 0.1); border-top: 1px solid var(--gray-2);
margin: 2rem 0; margin: 2rem 0;
} }
} }

View File

@ -50,22 +50,26 @@ export default () => {
} }
return ( return (
<div> <>
<button <div className="control-group">
onClick={setLink} <div className="button-group">
className={editor.isActive('link') ? 'is-active' : ''} <button
data-testid="setLink" onClick={setLink}
> className={editor.isActive('link') ? 'is-active' : ''}
setLink data-testid="setLink"
</button> >
<button Set link
onClick={() => editor.chain().focus().unsetLink().run()} </button>
disabled={!editor.isActive('link')} <button
data-testid="unsetLink" onClick={() => editor.chain().focus().unsetLink().run()}
> disabled={!editor.isActive('link')}
unsetLink data-testid="unsetLink"
</button> >
Unset link
</button>
</div>
</div>
<EditorContent editor={editor} /> <EditorContent editor={editor} />
</div> </>
) )
} }

View File

@ -1,54 +1,101 @@
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
ul, /* List styles */
ul,
ol { ol {
padding: 0 1rem; padding: 0 1rem;
} margin: 1.25rem 1rem 1.25rem 0.4rem;
h1, li p {
h2, margin-top: 0.25em;
h3, margin-bottom: 0.25em;
h4,
h5,
h6 {
line-height: 1.1;
}
code {
background-color: rgba(#616161, 0.1);
color: #616161;
}
pre {
background: #0D0D0D;
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
} }
} }
img { /* Heading styles */
max-width: 100%; h1,
height: auto; h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
} }
hr { h1,
margin: 1rem 0; h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code {
background-color: var(--purple-light);
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}
pre {
background: var(--black);
border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
} }
blockquote { blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem; padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1); }
hr {
border: none;
border-top: 1px solid var(--gray-2);
margin: 2rem 0;
}
/* Link styles */
a {
color: var(--purple);
cursor: pointer;
&:hover {
color: var(--purple-contrast);
}
} }
} }

View File

@ -12,116 +12,118 @@ const MenuBar = ({ editor }) => {
} }
return ( return (
<> <div className="control-group">
<button <div className="button-group">
onClick={() => editor.chain().focus().toggleBold().run()} <button
className={editor.isActive('bold') ? 'is-active' : ''} onClick={() => editor.chain().focus().toggleBold().run()}
> className={editor.isActive('bold') ? 'is-active' : ''}
bold >
</button> Bold
<button </button>
onClick={() => editor.chain().focus().toggleItalic().run()} <button
className={editor.isActive('italic') ? 'is-active' : ''} onClick={() => editor.chain().focus().toggleItalic().run()}
> className={editor.isActive('italic') ? 'is-active' : ''}
italic >
</button> Italic
<button </button>
onClick={() => editor.chain().focus().toggleStrike().run()} <button
className={editor.isActive('strike') ? 'is-active' : ''} onClick={() => editor.chain().focus().toggleStrike().run()}
> className={editor.isActive('strike') ? 'is-active' : ''}
strike >
</button> Strike
<button </button>
onClick={() => editor.chain().focus().toggleCode().run()} <button
className={editor.isActive('code') ? 'is-active' : ''} onClick={() => editor.chain().focus().toggleCode().run()}
> className={editor.isActive('code') ? 'is-active' : ''}
code >
</button> Code
<button onClick={() => editor.chain().focus().unsetAllMarks().run()}> </button>
clear marks <button onClick={() => editor.chain().focus().unsetAllMarks().run()}>
</button> Clear marks
<button onClick={() => editor.chain().focus().clearNodes().run()}> </button>
clear nodes <button onClick={() => editor.chain().focus().clearNodes().run()}>
</button> Clear nodes
<button </button>
onClick={() => editor.chain().focus().setParagraph().run()} <button
className={editor.isActive('paragraph') ? 'is-active' : ''} onClick={() => editor.chain().focus().setParagraph().run()}
> className={editor.isActive('paragraph') ? 'is-active' : ''}
paragraph >
</button> Paragraph
<button </button>
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()} <button
className={editor.isActive('heading', { level: 1 }) ? 'is-active' : ''} onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
> className={editor.isActive('heading', { level: 1 }) ? 'is-active' : ''}
h1 >
</button> H1
<button </button>
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()} <button
className={editor.isActive('heading', { level: 2 }) ? 'is-active' : ''} onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
> className={editor.isActive('heading', { level: 2 }) ? 'is-active' : ''}
h2 >
</button> H2
<button </button>
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()} <button
className={editor.isActive('heading', { level: 3 }) ? 'is-active' : ''} onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
> className={editor.isActive('heading', { level: 3 }) ? 'is-active' : ''}
h3 >
</button> H3
<button </button>
onClick={() => editor.chain().focus().toggleHeading({ level: 4 }).run()} <button
className={editor.isActive('heading', { level: 4 }) ? 'is-active' : ''} onClick={() => editor.chain().focus().toggleHeading({ level: 4 }).run()}
> className={editor.isActive('heading', { level: 4 }) ? 'is-active' : ''}
h4 >
</button> H4
<button </button>
onClick={() => editor.chain().focus().toggleHeading({ level: 5 }).run()} <button
className={editor.isActive('heading', { level: 5 }) ? 'is-active' : ''} onClick={() => editor.chain().focus().toggleHeading({ level: 5 }).run()}
> className={editor.isActive('heading', { level: 5 }) ? 'is-active' : ''}
h5 >
</button> H5
<button </button>
onClick={() => editor.chain().focus().toggleHeading({ level: 6 }).run()} <button
className={editor.isActive('heading', { level: 6 }) ? 'is-active' : ''} onClick={() => editor.chain().focus().toggleHeading({ level: 6 }).run()}
> className={editor.isActive('heading', { level: 6 }) ? 'is-active' : ''}
h6 >
</button> H6
<button </button>
onClick={() => editor.chain().focus().toggleBulletList().run()} <button
className={editor.isActive('bulletList') ? 'is-active' : ''} onClick={() => editor.chain().focus().toggleBulletList().run()}
> className={editor.isActive('bulletList') ? 'is-active' : ''}
bullet list >
</button> Bullet list
<button </button>
onClick={() => editor.chain().focus().toggleOrderedList().run()} <button
className={editor.isActive('orderedList') ? 'is-active' : ''} onClick={() => editor.chain().focus().toggleOrderedList().run()}
> className={editor.isActive('orderedList') ? 'is-active' : ''}
ordered list >
</button> Ordered list
<button </button>
onClick={() => editor.chain().focus().toggleCodeBlock().run()} <button
className={editor.isActive('codeBlock') ? 'is-active' : ''} onClick={() => editor.chain().focus().toggleCodeBlock().run()}
> className={editor.isActive('codeBlock') ? 'is-active' : ''}
code block >
</button> Code block
<button </button>
onClick={() => editor.chain().focus().toggleBlockquote().run()} <button
className={editor.isActive('blockquote') ? 'is-active' : ''} onClick={() => editor.chain().focus().toggleBlockquote().run()}
> className={editor.isActive('blockquote') ? 'is-active' : ''}
blockquote >
</button> Blockquote
<button onClick={() => editor.chain().focus().setHorizontalRule().run()}> </button>
horizontal rule <button onClick={() => editor.chain().focus().setHorizontalRule().run()}>
</button> Horizontal rule
<button onClick={() => editor.chain().focus().setHardBreak().run()}> </button>
hard break <button onClick={() => editor.chain().focus().setHardBreak().run()}>
</button> Hard break
<button onClick={() => editor.chain().focus().undo().run()}> </button>
undo <button onClick={() => editor.chain().focus().undo().run()}>
</button> Undo
<button onClick={() => editor.chain().focus().redo().run()}> </button>
redo <button onClick={() => editor.chain().focus().redo().run()}>
</button> Redo
</> </button>
</div>
</div>
) )
} }
@ -139,9 +141,9 @@ export default () => {
}) })
return ( return (
<div> <>
<MenuBar editor={editor} /> <MenuBar editor={editor} />
<EditorContent editor={editor} /> <EditorContent editor={editor} />
</div> </>
) )
} }

View File

@ -1,54 +1,91 @@
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
ul, /* List styles */
ul,
ol { ol {
padding: 0 1rem; padding: 0 1rem;
} margin: 1.25rem 1rem 1.25rem 0.4rem;
h1, li p {
h2, margin-top: 0.25em;
h3, margin-bottom: 0.25em;
h4,
h5,
h6 {
line-height: 1.1;
}
code {
background-color: rgba(#616161, 0.1);
color: #616161;
}
pre {
background: #0D0D0D;
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
} }
} }
img { /* Heading styles */
max-width: 100%; h1,
height: auto; h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
} }
hr { h1,
margin: 1rem 0; h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code {
background-color: var(--purple-light);
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}
pre {
background: var(--black);
border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
} }
blockquote { blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem; padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1); }
hr {
border: none;
border-top: 1px solid var(--gray-2);
margin: 2rem 0;
} }
} }

View File

@ -1,70 +1,74 @@
<template> <template>
<div v-if="editor"> <div v-if="editor" class="container">
<button @click="editor.chain().focus().toggleBold().run()" :class="{ 'is-active': editor.isActive('bold') }"> <div class="control-group">
bold <div class="button-group">
</button> <button @click="editor.chain().focus().toggleBold().run()" :class="{ 'is-active': editor.isActive('bold') }">
<button @click="editor.chain().focus().toggleItalic().run()" :class="{ 'is-active': editor.isActive('italic') }"> Bold
italic </button>
</button> <button @click="editor.chain().focus().toggleItalic().run()" :class="{ 'is-active': editor.isActive('italic') }">
<button @click="editor.chain().focus().toggleStrike().run()" :class="{ 'is-active': editor.isActive('strike') }"> Italic
strike </button>
</button> <button @click="editor.chain().focus().toggleStrike().run()" :class="{ 'is-active': editor.isActive('strike') }">
<button @click="editor.chain().focus().toggleCode().run()" :class="{ 'is-active': editor.isActive('code') }"> Strike
code </button>
</button> <button @click="editor.chain().focus().toggleCode().run()" :class="{ 'is-active': editor.isActive('code') }">
<button @click="editor.chain().focus().unsetAllMarks().run()"> Code
clear marks </button>
</button> <button @click="editor.chain().focus().unsetAllMarks().run()">
<button @click="editor.chain().focus().clearNodes().run()"> Clear marks
clear nodes </button>
</button> <button @click="editor.chain().focus().clearNodes().run()">
<button @click="editor.chain().focus().setParagraph().run()" :class="{ 'is-active': editor.isActive('paragraph') }"> Clear nodes
paragraph </button>
</button> <button @click="editor.chain().focus().setParagraph().run()" :class="{ 'is-active': editor.isActive('paragraph') }">
<button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"> Paragraph
h1 </button>
</button> <button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 1 }) }">
<button @click="editor.chain().focus().toggleHeading({ level: 2 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"> H1
h2 </button>
</button> <button @click="editor.chain().focus().toggleHeading({ level: 2 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 2 }) }">
<button @click="editor.chain().focus().toggleHeading({ level: 3 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"> H2
h3 </button>
</button> <button @click="editor.chain().focus().toggleHeading({ level: 3 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 3 }) }">
<button @click="editor.chain().focus().toggleHeading({ level: 4 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 4 }) }"> H3
h4 </button>
</button> <button @click="editor.chain().focus().toggleHeading({ level: 4 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 4 }) }">
<button @click="editor.chain().focus().toggleHeading({ level: 5 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 5 }) }"> H4
h5 </button>
</button> <button @click="editor.chain().focus().toggleHeading({ level: 5 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 5 }) }">
<button @click="editor.chain().focus().toggleHeading({ level: 6 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 6 }) }"> H5
h6 </button>
</button> <button @click="editor.chain().focus().toggleHeading({ level: 6 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 6 }) }">
<button @click="editor.chain().focus().toggleBulletList().run()" :class="{ 'is-active': editor.isActive('bulletList') }"> H6
bullet list </button>
</button> <button @click="editor.chain().focus().toggleBulletList().run()" :class="{ 'is-active': editor.isActive('bulletList') }">
<button @click="editor.chain().focus().toggleOrderedList().run()" :class="{ 'is-active': editor.isActive('orderedList') }"> Bullet list
ordered list </button>
</button> <button @click="editor.chain().focus().toggleOrderedList().run()" :class="{ 'is-active': editor.isActive('orderedList') }">
<button @click="editor.chain().focus().toggleCodeBlock().run()" :class="{ 'is-active': editor.isActive('codeBlock') }"> Ordered list
code block </button>
</button> <button @click="editor.chain().focus().toggleCodeBlock().run()" :class="{ 'is-active': editor.isActive('codeBlock') }">
<button @click="editor.chain().focus().toggleBlockquote().run()" :class="{ 'is-active': editor.isActive('blockquote') }"> Code block
blockquote </button>
</button> <button @click="editor.chain().focus().toggleBlockquote().run()" :class="{ 'is-active': editor.isActive('blockquote') }">
<button @click="editor.chain().focus().setHorizontalRule().run()"> Blockquote
horizontal rule </button>
</button> <button @click="editor.chain().focus().setHorizontalRule().run()">
<button @click="editor.chain().focus().setHardBreak().run()"> Horizontal rule
hard break </button>
</button> <button @click="editor.chain().focus().setHardBreak().run()">
<button @click="editor.chain().focus().undo().run()"> Hard break
undo </button>
</button> <button @click="editor.chain().focus().undo().run()">
<button @click="editor.chain().focus().redo().run()"> Undo
redo </button>
</button> <button @click="editor.chain().focus().redo().run()">
Redo
</button>
</div>
</div>
<editor-content :editor="editor" />
</div> </div>
<editor-content :editor="editor" />
</template> </template>
<script> <script>
@ -107,15 +111,23 @@ export default {
<style lang="scss"> <style lang="scss">
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
/* List styles */
ul, ul,
ol { ol {
padding: 0 1rem; padding: 0 1rem;
margin: 1.25rem 1rem 1.25rem 0.4rem;
li p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
} }
/* Heading styles */
h1, h1,
h2, h2,
h3, h3,
@ -123,40 +135,69 @@ export default {
h5, h5,
h6 { h6 {
line-height: 1.1; line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
} }
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code { code {
background-color: rgba(#616161, 0.1); background-color: var(--purple-light);
color: #616161; border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
} }
pre { pre {
background: #0D0D0D; background: var(--black);
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem; border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code { code {
color: inherit;
padding: 0;
background: none; background: none;
color: inherit;
font-size: 0.8rem; font-size: 0.8rem;
padding: 0;
} }
} }
img { blockquote {
max-width: 100%; border-left: 3px solid var(--gray-3);
height: auto; margin: 1.5rem 0;
padding-left: 1rem;
} }
hr { hr {
margin: 1rem 0; border: none;
} border-top: 1px solid var(--gray-2);
margin: 2rem 0;
blockquote {
padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
} }
} }
</style> </style>

View File

@ -17,109 +17,109 @@ const MenuBar = ({ editor }) => {
onClick={() => editor.chain().focus().toggleBold().run()} onClick={() => editor.chain().focus().toggleBold().run()}
className={editor.isActive('bold') ? 'is-active' : ''} className={editor.isActive('bold') ? 'is-active' : ''}
> >
bold Bold
</button> </button>
<button <button
onClick={() => editor.chain().focus().toggleItalic().run()} onClick={() => editor.chain().focus().toggleItalic().run()}
className={editor.isActive('italic') ? 'is-active' : ''} className={editor.isActive('italic') ? 'is-active' : ''}
> >
italic Italic
</button> </button>
<button <button
onClick={() => editor.chain().focus().toggleStrike().run()} onClick={() => editor.chain().focus().toggleStrike().run()}
className={editor.isActive('strike') ? 'is-active' : ''} className={editor.isActive('strike') ? 'is-active' : ''}
> >
strike Strike
</button> </button>
<button <button
onClick={() => editor.chain().focus().toggleCode().run()} onClick={() => editor.chain().focus().toggleCode().run()}
className={editor.isActive('code') ? 'is-active' : ''} className={editor.isActive('code') ? 'is-active' : ''}
> >
code Code
</button> </button>
<button onClick={() => editor.chain().focus().unsetAllMarks().run()}> <button onClick={() => editor.chain().focus().unsetAllMarks().run()}>
clear marks Clear marks
</button> </button>
<button onClick={() => editor.chain().focus().clearNodes().run()}> <button onClick={() => editor.chain().focus().clearNodes().run()}>
clear nodes Clear nodes
</button> </button>
<button <button
onClick={() => editor.chain().focus().setParagraph().run()} onClick={() => editor.chain().focus().setParagraph().run()}
className={editor.isActive('paragraph') ? 'is-active' : ''} className={editor.isActive('paragraph') ? 'is-active' : ''}
> >
paragraph Paragraph
</button> </button>
<button <button
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()} onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
className={editor.isActive('heading', { level: 1 }) ? 'is-active' : ''} className={editor.isActive('heading', { level: 1 }) ? 'is-active' : ''}
> >
h1 H1
</button> </button>
<button <button
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()} onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
className={editor.isActive('heading', { level: 2 }) ? 'is-active' : ''} className={editor.isActive('heading', { level: 2 }) ? 'is-active' : ''}
> >
h2 H2
</button> </button>
<button <button
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()} onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
className={editor.isActive('heading', { level: 3 }) ? 'is-active' : ''} className={editor.isActive('heading', { level: 3 }) ? 'is-active' : ''}
> >
h3 H3
</button> </button>
<button <button
onClick={() => editor.chain().focus().toggleHeading({ level: 4 }).run()} onClick={() => editor.chain().focus().toggleHeading({ level: 4 }).run()}
className={editor.isActive('heading', { level: 4 }) ? 'is-active' : ''} className={editor.isActive('heading', { level: 4 }) ? 'is-active' : ''}
> >
h4 H4
</button> </button>
<button <button
onClick={() => editor.chain().focus().toggleHeading({ level: 5 }).run()} onClick={() => editor.chain().focus().toggleHeading({ level: 5 }).run()}
className={editor.isActive('heading', { level: 5 }) ? 'is-active' : ''} className={editor.isActive('heading', { level: 5 }) ? 'is-active' : ''}
> >
h5 H5
</button> </button>
<button <button
onClick={() => editor.chain().focus().toggleHeading({ level: 6 }).run()} onClick={() => editor.chain().focus().toggleHeading({ level: 6 }).run()}
className={editor.isActive('heading', { level: 6 }) ? 'is-active' : ''} className={editor.isActive('heading', { level: 6 }) ? 'is-active' : ''}
> >
h6 H6
</button> </button>
<button <button
onClick={() => editor.chain().focus().toggleBulletList().run()} onClick={() => editor.chain().focus().toggleBulletList().run()}
className={editor.isActive('bulletList') ? 'is-active' : ''} className={editor.isActive('bulletList') ? 'is-active' : ''}
> >
bullet list Bullet list
</button> </button>
<button <button
onClick={() => editor.chain().focus().toggleOrderedList().run()} onClick={() => editor.chain().focus().toggleOrderedList().run()}
className={editor.isActive('orderedList') ? 'is-active' : ''} className={editor.isActive('orderedList') ? 'is-active' : ''}
> >
ordered list Ordered list
</button> </button>
<button <button
onClick={() => editor.chain().focus().toggleCodeBlock().run()} onClick={() => editor.chain().focus().toggleCodeBlock().run()}
className={editor.isActive('codeBlock') ? 'is-active' : ''} className={editor.isActive('codeBlock') ? 'is-active' : ''}
> >
code block Code block
</button> </button>
<button <button
onClick={() => editor.chain().focus().toggleBlockquote().run()} onClick={() => editor.chain().focus().toggleBlockquote().run()}
className={editor.isActive('blockquote') ? 'is-active' : ''} className={editor.isActive('blockquote') ? 'is-active' : ''}
> >
blockquote Blockquote
</button> </button>
<button onClick={() => editor.chain().focus().setHorizontalRule().run()}> <button onClick={() => editor.chain().focus().setHorizontalRule().run()}>
horizontal rule Horizontal rule
</button> </button>
<button onClick={() => editor.chain().focus().setHardBreak().run()}> <button onClick={() => editor.chain().focus().setHardBreak().run()}>
hard break Hard break
</button> </button>
<button onClick={() => editor.chain().focus().undo().run()}> <button onClick={() => editor.chain().focus().undo().run()}>
undo Undo
</button> </button>
<button onClick={() => editor.chain().focus().redo().run()}> <button onClick={() => editor.chain().focus().redo().run()}>
redo Redo
</button> </button>
</div> </div>
) )
@ -135,7 +135,7 @@ export default () => {
Hi there, Hi there,
</h2> </h2>
<p> <p>
this is a <em>basic</em> example of <strong>tiptap</strong>. Sure, there are all kind of basic text styles youd probably expect from a text editor. But wait until you see the lists: this is a <em>basic</em> example of <strong>Tiptap</strong>. Sure, there are all kind of basic text styles youd probably expect from a text editor. But wait until you see the lists:
</p> </p>
<ul> <ul>
<li> <li>
@ -163,9 +163,9 @@ export default () => {
}) })
return ( return (
<div> <>
<MenuBar editor={editor} /> <MenuBar editor={editor} />
<EditorContent editor={editor} /> <EditorContent editor={editor} />
</div> </>
) )
} }

View File

@ -1,56 +1,91 @@
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
ul, /* List styles */
ul,
ol { ol {
padding: 0 1rem; padding: 0 1rem;
} margin: 1.25rem 1rem 1.25rem 0.4rem;
h1, li p {
h2, margin-top: 0.25em;
h3, margin-bottom: 0.25em;
h4,
h5,
h6 {
line-height: 1.1;
}
code {
background-color: rgba(#616161, 0.1);
color: #616161;
}
pre {
background: #0D0D0D;
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
} }
} }
img { /* Heading styles */
max-width: 100%; h1,
height: auto; h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
}
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code {
background-color: var(--purple-light);
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}
pre {
background: var(--black);
border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
} }
blockquote { blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem; padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
} }
hr { hr {
border: none; border: none;
border-top: 2px solid rgba(#0D0D0D, 0.1); border-top: 1px solid var(--gray-2);
margin: 2rem 0; margin: 2rem 0;
} }
} }

View File

@ -1,67 +1,67 @@
<template> <template>
<div v-if="editor" class="toolbar" :class="styles.toolbar"> <div v-if="editor" class="toolbar" :class="styles.toolbar">
<button @click="editor.chain().focus().toggleBold().run()" :class="{ 'is-active': editor.isActive('bold') }"> <button @click="editor.chain().focus().toggleBold().run()" :class="{ 'is-active': editor.isActive('bold') }">
bold Bold
</button> </button>
<button @click="editor.chain().focus().toggleItalic().run()" :class="{ 'is-active': editor.isActive('italic') }"> <button @click="editor.chain().focus().toggleItalic().run()" :class="{ 'is-active': editor.isActive('italic') }">
italic Italic
</button> </button>
<button @click="editor.chain().focus().toggleStrike().run()" :class="{ 'is-active': editor.isActive('strike') }"> <button @click="editor.chain().focus().toggleStrike().run()" :class="{ 'is-active': editor.isActive('strike') }">
strike Strike
</button> </button>
<button @click="editor.chain().focus().toggleCode().run()" :class="{ 'is-active': editor.isActive('code') }"> <button @click="editor.chain().focus().toggleCode().run()" :class="{ 'is-active': editor.isActive('code') }">
code Code
</button> </button>
<button @click="editor.chain().focus().unsetAllMarks().run()"> <button @click="editor.chain().focus().unsetAllMarks().run()">
clear marks Clear marks
</button> </button>
<button @click="editor.chain().focus().clearNodes().run()"> <button @click="editor.chain().focus().clearNodes().run()">
clear nodes Clear nodes
</button> </button>
<button @click="editor.chain().focus().setParagraph().run()" :class="{ 'is-active': editor.isActive('paragraph') }"> <button @click="editor.chain().focus().setParagraph().run()" :class="{ 'is-active': editor.isActive('paragraph') }">
paragraph Paragraph
</button> </button>
<button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"> <button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 1 }) }">
h1 H1
</button> </button>
<button @click="editor.chain().focus().toggleHeading({ level: 2 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"> <button @click="editor.chain().focus().toggleHeading({ level: 2 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 2 }) }">
h2 H2
</button> </button>
<button @click="editor.chain().focus().toggleHeading({ level: 3 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"> <button @click="editor.chain().focus().toggleHeading({ level: 3 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 3 }) }">
h3 H3
</button> </button>
<button @click="editor.chain().focus().toggleHeading({ level: 4 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 4 }) }"> <button @click="editor.chain().focus().toggleHeading({ level: 4 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 4 }) }">
h4 H4
</button> </button>
<button @click="editor.chain().focus().toggleHeading({ level: 5 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 5 }) }"> <button @click="editor.chain().focus().toggleHeading({ level: 5 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 5 }) }">
h5 H5
</button> </button>
<button @click="editor.chain().focus().toggleHeading({ level: 6 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 6 }) }"> <button @click="editor.chain().focus().toggleHeading({ level: 6 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 6 }) }">
h6 H6
</button> </button>
<button @click="editor.chain().focus().toggleBulletList().run()" :class="{ 'is-active': editor.isActive('bulletList') }"> <button @click="editor.chain().focus().toggleBulletList().run()" :class="{ 'is-active': editor.isActive('bulletList') }">
bullet list Bullet list
</button> </button>
<button @click="editor.chain().focus().toggleOrderedList().run()" :class="{ 'is-active': editor.isActive('orderedList') }"> <button @click="editor.chain().focus().toggleOrderedList().run()" :class="{ 'is-active': editor.isActive('orderedList') }">
ordered list Ordered list
</button> </button>
<button @click="editor.chain().focus().toggleCodeBlock().run()" :class="{ 'is-active': editor.isActive('codeBlock') }"> <button @click="editor.chain().focus().toggleCodeBlock().run()" :class="{ 'is-active': editor.isActive('codeBlock') }">
code block Code block
</button> </button>
<button @click="editor.chain().focus().toggleBlockquote().run()" :class="{ 'is-active': editor.isActive('blockquote') }"> <button @click="editor.chain().focus().toggleBlockquote().run()" :class="{ 'is-active': editor.isActive('blockquote') }">
blockquote Blockquote
</button> </button>
<button @click="editor.chain().focus().setHorizontalRule().run()"> <button @click="editor.chain().focus().setHorizontalRule().run()">
horizontal rule Horizontal rule
</button> </button>
<button @click="editor.chain().focus().setHardBreak().run()"> <button @click="editor.chain().focus().setHardBreak().run()">
hard break Hard break
</button> </button>
<button @click="editor.chain().focus().undo().run()"> <button @click="editor.chain().focus().undo().run()">
undo Undo
</button> </button>
<button @click="editor.chain().focus().redo().run()"> <button @click="editor.chain().focus().redo().run()">
redo Redo
</button> </button>
</div> </div>
<editor-content :editor="editor" /> <editor-content :editor="editor" />
@ -95,7 +95,7 @@ export default {
This is a red headline This is a red headline
</h1> </h1>
<p> <p>
this is a <em>basic</em> example of <strong>tiptap</strong>. Sure, there are all kind of basic text styles youd probably expect from a text editor. But wait until you see the lists: this is a <em>basic</em> example of <strong>Tiptap</strong>. Sure, there are all kind of basic text styles youd probably expect from a text editor. But wait until you see the lists:
</p> </p>
<ul> <ul>
<li> <li>
@ -132,15 +132,23 @@ export default {
<style lang="scss"> <style lang="scss">
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
/* List styles */
ul, ul,
ol { ol {
padding: 0 1rem; padding: 0 1rem;
margin: 1.25rem 1rem 1.25rem 0.4rem;
li p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
} }
/* Heading styles */
h1, h1,
h2, h2,
h3, h3,
@ -148,41 +156,68 @@ export default {
h5, h5,
h6 { h6 {
line-height: 1.1; line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
} }
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code { code {
background-color: rgba(#616161, 0.1); background-color: var(--purple-light);
color: #616161; border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
} }
pre { pre {
background: #0D0D0D; background: var(--black);
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem; border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code { code {
color: inherit;
padding: 0;
background: none; background: none;
color: inherit;
font-size: 0.8rem; font-size: 0.8rem;
padding: 0;
} }
} }
img {
max-width: 100%;
height: auto;
}
blockquote { blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem; padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
} }
hr { hr {
border: none; border: none;
border-top: 2px solid rgba(#0D0D0D, 0.1); border-top: 1px solid var(--gray-2);
margin: 2rem 0; margin: 2rem 0;
} }
} }

View File

@ -1,9 +1,13 @@
.code-block { .tiptap {
position: relative; .code-block {
position: relative;
select {
position: absolute; select {
right: 0.5rem; position: absolute;
top: 0.5rem; background-color: var(--white);
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="Black" d="M7 10l5 5 5-5z"/></svg>');
right: 0.5rem;
top: 0.5rem;
}
} }
} }

View File

@ -30,9 +30,13 @@ const MenuBar = ({ editor }) => {
} }
return ( return (
<button onClick={() => editor.chain().focus().toggleCodeBlock().run()} className={editor.isActive('codeBlock') ? 'is-active' : ''}> <div className="control-group">
code block <div className="button-group">
</button> <button onClick={() => editor.chain().focus().toggleCodeBlock().run()} className={editor.isActive('codeBlock') ? 'is-active' : ''}>
Toggle code block
</button>
</div>
</div>
) )
} }
@ -72,9 +76,9 @@ export default () => {
}) })
return ( return (
<div> <>
<MenuBar editor={editor} /> <MenuBar editor={editor} />
<EditorContent editor={editor} /> <EditorContent editor={editor} />
</div> </>
) )
} }

View File

@ -1,23 +1,25 @@
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
pre { pre {
background: #0d0d0d; background: var(--black);
color: #fff;
font-family: "JetBrainsMono", monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem; border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code { code {
color: inherit;
padding: 0;
background: none; background: none;
color: inherit;
font-size: 0.8rem; font-size: 0.8rem;
padding: 0;
} }
/* Code styling */
.hljs-comment, .hljs-comment,
.hljs-quote { .hljs-quote {
color: #616161; color: #616161;

View File

@ -46,13 +46,17 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
.code-block { .tiptap {
position: relative; .code-block {
position: relative;
select { select {
position: absolute; position: absolute;
top: 0.5rem; background-color: var(--white);
right: 0.5rem; background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="Black" d="M7 10l5 5 5-5z"/></svg>');
right: 0.5rem;
top: 0.5rem;
}
} }
} }
</style> </style>

View File

@ -1,8 +1,12 @@
<template> <template>
<div v-if="editor"> <div v-if="editor" class="container">
<button @click="editor.chain().focus().toggleCodeBlock().run()" :class="{ 'is-active': editor.isActive('codeBlock') }"> <div class="control-group">
code block <div class="button-">
</button> <button @click="editor.chain().focus().toggleCodeBlock().run()" :class="{ 'is-active': editor.isActive('codeBlock') }">
Toggle code block
</button>
</div>
</div>
</div> </div>
<editor-content :editor="editor" /> <editor-content :editor="editor" />
</template> </template>
@ -88,24 +92,26 @@ export default {
<style lang="scss"> <style lang="scss">
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
pre { pre {
background: #0D0D0D; background: var(--black);
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem; border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code { code {
color: inherit;
padding: 0;
background: none; background: none;
color: inherit;
font-size: 0.8rem; font-size: 0.8rem;
padding: 0;
} }
/* Code styling */
.hljs-comment, .hljs-comment,
.hljs-quote { .hljs-quote {
color: #616161; color: #616161;
@ -121,7 +127,7 @@ export default {
.hljs-name, .hljs-name,
.hljs-selector-id, .hljs-selector-id,
.hljs-selector-class { .hljs-selector-class {
color: #F98181; color: #f98181;
} }
.hljs-number, .hljs-number,
@ -131,23 +137,23 @@ export default {
.hljs-literal, .hljs-literal,
.hljs-type, .hljs-type,
.hljs-params { .hljs-params {
color: #FBBC88; color: #fbbc88;
} }
.hljs-string, .hljs-string,
.hljs-symbol, .hljs-symbol,
.hljs-bullet { .hljs-bullet {
color: #B9F18D; color: #b9f18d;
} }
.hljs-title, .hljs-title,
.hljs-section { .hljs-section {
color: #FAF594; color: #faf594;
} }
.hljs-keyword, .hljs-keyword,
.hljs-selector-tag { .hljs-selector-tag {
color: #70CFF8; color: #70cff8;
} }
.hljs-emphasis { .hljs-emphasis {

View File

@ -59,25 +59,25 @@ export default ({ editor }) => {
}, },
{ {
icon: 'list-unordered', icon: 'list-unordered',
title: 'Bullet List', title: 'Bullet list',
action: () => editor.chain().focus().toggleBulletList().run(), action: () => editor.chain().focus().toggleBulletList().run(),
isActive: () => editor.isActive('bulletList'), isActive: () => editor.isActive('bulletList'),
}, },
{ {
icon: 'list-ordered', icon: 'list-ordered',
title: 'Ordered List', title: 'Ordered list',
action: () => editor.chain().focus().toggleOrderedList().run(), action: () => editor.chain().focus().toggleOrderedList().run(),
isActive: () => editor.isActive('orderedList'), isActive: () => editor.isActive('orderedList'),
}, },
{ {
icon: 'list-check-2', icon: 'list-check-2',
title: 'Task List', title: 'Task list',
action: () => editor.chain().focus().toggleTaskList().run(), action: () => editor.chain().focus().toggleTaskList().run(),
isActive: () => editor.isActive('taskList'), isActive: () => editor.isActive('taskList'),
}, },
{ {
icon: 'code-box-line', icon: 'code-box-line',
title: 'Code Block', title: 'Code block',
action: () => editor.chain().focus().toggleCodeBlock().run(), action: () => editor.chain().focus().toggleCodeBlock().run(),
isActive: () => editor.isActive('codeBlock'), isActive: () => editor.isActive('codeBlock'),
}, },
@ -92,7 +92,7 @@ export default ({ editor }) => {
}, },
{ {
icon: 'separator', icon: 'separator',
title: 'Horizontal Rule', title: 'Horizontal rule',
action: () => editor.chain().focus().setHorizontalRule().run(), action: () => editor.chain().focus().setHorizontalRule().run(),
}, },
{ {
@ -100,12 +100,12 @@ export default ({ editor }) => {
}, },
{ {
icon: 'text-wrap', icon: 'text-wrap',
title: 'Hard Break', title: 'Hard break',
action: () => editor.chain().focus().setHardBreak().run(), action: () => editor.chain().focus().setHardBreak().run(),
}, },
{ {
icon: 'format-clear', icon: 'format-clear',
title: 'Clear Format', title: 'Clear format',
action: () => editor.chain().focus().clearNodes().unsetAllMarks() action: () => editor.chain().focus().clearNodes().unsetAllMarks()
.run(), .run(),
}, },

View File

@ -78,25 +78,25 @@ export default {
}, },
{ {
icon: 'list-unordered', icon: 'list-unordered',
title: 'Bullet List', title: 'Bullet list',
action: () => this.editor.chain().focus().toggleBulletList().run(), action: () => this.editor.chain().focus().toggleBulletList().run(),
isActive: () => this.editor.isActive('bulletList'), isActive: () => this.editor.isActive('bulletList'),
}, },
{ {
icon: 'list-ordered', icon: 'list-ordered',
title: 'Ordered List', title: 'Ordered list',
action: () => this.editor.chain().focus().toggleOrderedList().run(), action: () => this.editor.chain().focus().toggleOrderedList().run(),
isActive: () => this.editor.isActive('orderedList'), isActive: () => this.editor.isActive('orderedList'),
}, },
{ {
icon: 'list-check-2', icon: 'list-check-2',
title: 'Task List', title: 'Task list',
action: () => this.editor.chain().focus().toggleTaskList().run(), action: () => this.editor.chain().focus().toggleTaskList().run(),
isActive: () => this.editor.isActive('taskList'), isActive: () => this.editor.isActive('taskList'),
}, },
{ {
icon: 'code-box-line', icon: 'code-box-line',
title: 'Code Block', title: 'Code block',
action: () => this.editor.chain().focus().toggleCodeBlock().run(), action: () => this.editor.chain().focus().toggleCodeBlock().run(),
isActive: () => this.editor.isActive('codeBlock'), isActive: () => this.editor.isActive('codeBlock'),
}, },
@ -111,7 +111,7 @@ export default {
}, },
{ {
icon: 'separator', icon: 'separator',
title: 'Horizontal Rule', title: 'Horizontal rule',
action: () => this.editor.chain().focus().setHorizontalRule().run(), action: () => this.editor.chain().focus().setHorizontalRule().run(),
}, },
{ {
@ -119,12 +119,12 @@ export default {
}, },
{ {
icon: 'text-wrap', icon: 'text-wrap',
title: 'Hard Break', title: 'Hard break',
action: () => this.editor.chain().focus().setHardBreak().run(), action: () => this.editor.chain().focus().setHardBreak().run(),
}, },
{ {
icon: 'format-clear', icon: 'format-clear',
title: 'Clear Format', title: 'Clear format',
action: () => this.editor.chain() action: () => this.editor.chain()
.focus() .focus()
.clearNodes() .clearNodes()

View File

@ -54,11 +54,11 @@ export const MentionList = forwardRef((props, ref) => {
})) }))
return ( return (
<div className="items"> <div className="dropdown-menu">
{props.items.length {props.items.length
? props.items.map((item, index) => ( ? props.items.map((item, index) => (
<button <button
className={`item ${index === selectedIndex ? 'is-selected' : ''}`} className={index === selectedIndex ? 'is-selected' : ''}
key={index} key={index}
onClick={() => selectItem(index)} onClick={() => selectItem(index)}
> >

View File

@ -1,28 +1,31 @@
.items { /* Dropdown menu */
padding: 0.2rem; .dropdown-menu {
background: var(--white);
border: 1px solid var(--gray-1);
border-radius: 0.7rem;
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
gap: 0.1rem;
overflow: auto;
padding: 0.4rem;
position: relative; position: relative;
border-radius: 0.5rem;
background: #FFF;
color: rgba(0, 0, 0, 0.8);
overflow: hidden;
font-size: 0.9rem;
box-shadow:
0 0 0 1px rgba(0, 0, 0, 0.05),
0px 10px 20px rgba(0, 0, 0, 0.1),
;
}
.item { button {
display: block; align-items: center;
margin: 0; background-color: transparent;
width: 100%; display: flex;
text-align: left; gap: 0.25rem;
background: transparent; text-align: left;
border-radius: 0.4rem; width: 100%;
border: 1px solid transparent;
padding: 0.2rem 0.4rem; &:hover,
&:hover.is-selected {
background-color: var(--gray-3);
}
&.is-selected { &.is-selected {
border-color: #000; background-color: var(--gray-2);
}
} }
} }

View File

@ -40,7 +40,7 @@ export default () => {
: 0 : 0
return ( return (
<div> <>
<EditorContent editor={editor} /> <EditorContent editor={editor} />
{editor {editor
&& <div className={`character-count ${editor.storage.characterCount.characters() === limit ? 'character-count--warning' : ''}`}> && <div className={`character-count ${editor.storage.characterCount.characters() === limit ? 'character-count--warning' : ''}`}>
@ -48,7 +48,6 @@ export default () => {
height="20" height="20"
width="20" width="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
className="character-count__graph"
> >
<circle <circle
r="10" r="10"
@ -74,11 +73,9 @@ export default () => {
/> />
</svg> </svg>
<div className="character-count__text"> {editor.storage.characterCount.characters()} / {limit} characters
{editor.storage.characterCount.characters()}/{limit} characters
</div>
</div> </div>
} }
</div> </>
) )
} }

View File

@ -4,33 +4,33 @@ context('/src/Examples/Community/React/', () => {
}) })
it('should count the characters correctly', () => { it('should count the characters correctly', () => {
// check if count text is "44/280 characters" // check if count text is "44 / 280 characters"
cy.get('.character-count__text').should('have.text', '44/280 characters') cy.get('.character-count').should('contain', '44 / 280 characters')
// type in .tiptap // type in .tiptap
cy.get('.tiptap').type(' Hello World') cy.get('.tiptap').type(' Hello World')
cy.get('.character-count__text').should('have.text', '56/280 characters') cy.get('.character-count').should('contain', '56 / 280 characters')
// remove content from .tiptap and enter text // remove content from .tiptap and enter text
cy.get('.tiptap').type('{selectall}{backspace}Hello World') cy.get('.tiptap').type('{selectall}{backspace}Hello World')
cy.get('.character-count__text').should('have.text', '11/280 characters') cy.get('.character-count').should('contain', '11 / 280 characters')
}) })
it('should mention a user', () => { it('should mention a user', () => {
cy.get('.tiptap').type('{selectall}{backspace}@') cy.get('.tiptap').type('{selectall}{backspace}@')
// check if the mention autocomplete is visible // check if the mention autocomplete is visible
cy.get('.tippy-content .items').should('be.visible') cy.get('.tippy-content .dropdown-menu').should('be.visible')
// select the first user // select the first user
cy.get('.tippy-content .items .item').first().then($el => { cy.get('.tippy-content .dropdown-menu button').first().then($el => {
const name = $el.text() const name = $el.text()
$el.click() $el.click()
// check if the user is mentioned // check if the user is mentioned
cy.get('.tiptap').should('have.text', `@${name} `) cy.get('.tiptap').should('have.text', `@${name} `)
cy.get('.character-count__text').should('have.text', '2/280 characters') cy.get('.character-count').should('contain', '2 / 280 characters')
}) })
}) })

View File

@ -1,41 +1,33 @@
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
h1, .mention {
h2, background-color: var(--purple-light);
h3, border-radius: 0.4rem;
h4, box-decoration-break: clone;
h5, color: var(--purple);
h6 { padding: 0.1rem 0.3rem;
line-height: 1.1;
} }
} }
.mention { /* Character count */
border: 1px solid #000;
border-radius: 0.4rem;
padding: 0.1rem 0.3rem;
box-decoration-break: clone;
}
.character-count { .character-count {
margin-top: 1rem;
display: flex;
align-items: center; align-items: center;
color: #68CEF8; color: var(--gray-5);
display: flex;
font-size: 0.75rem;
gap: .5rem;
margin: 1.5rem;
&--warning { svg {
color: #FB5151; color: var(--purple);
} }
&__graph { &--warning,
margin-right: 0.5rem; &--warning svg {
} color: var(--red);
&__text {
color: #868e96;
} }
} }

View File

@ -1,8 +1,7 @@
<template> <template>
<div class="items"> <div class="dropdown-menu">
<template v-if="items.length"> <template v-if="items.length">
<button <button
class="item"
:class="{ 'is-selected': index === selectedIndex }" :class="{ 'is-selected': index === selectedIndex }"
v-for="(item, index) in items" v-for="(item, index) in items"
:key="index" :key="index"
@ -87,32 +86,35 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
.items { /* Dropdown menu */
padding: 0.2rem; .dropdown-menu {
background: var(--white);
border: 1px solid var(--gray-1);
border-radius: 0.7rem;
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
gap: 0.1rem;
overflow: auto;
padding: 0.4rem;
position: relative; position: relative;
border-radius: 0.5rem;
background: #FFF;
color: rgba(0, 0, 0, 0.8);
overflow: hidden;
font-size: 0.9rem;
box-shadow:
0 0 0 1px rgba(0, 0, 0, 0.05),
0px 10px 20px rgba(0, 0, 0, 0.1),
;
}
.item { button {
display: block; align-items: center;
margin: 0; background-color: transparent;
width: 100%; display: flex;
text-align: left; gap: 0.25rem;
background: transparent; text-align: left;
border-radius: 0.4rem; width: 100%;
border: 1px solid transparent;
padding: 0.2rem 0.4rem;
&.is-selected { &:hover,
border-color: #000; &:hover.is-selected {
background-color: var(--gray-3);
}
&.is-selected {
background-color: var(--gray-2);
}
} }
} }
</style> </style>

View File

@ -4,33 +4,33 @@ context('/src/Examples/Community/Vue/', () => {
}) })
it('should count the characters correctly', () => { it('should count the characters correctly', () => {
// check if count text is "44/280 characters" // check if count text is "44 / 280 characters"
cy.get('.character-count__text').should('have.text', '44/280 characters') cy.get('.character-count').should('contain', '44 / 280 characters')
// type in .tiptap // type in .tiptap
cy.get('.tiptap').type(' Hello World') cy.get('.tiptap').type(' Hello World')
cy.get('.character-count__text').should('have.text', '56/280 characters') cy.get('.character-count').should('contain', '56 / 280 characters')
// remove content from .tiptap and enter text // remove content from .tiptap and enter text
cy.get('.tiptap').type('{selectall}{backspace}Hello World') cy.get('.tiptap').type('{selectall}{backspace}Hello World')
cy.get('.character-count__text').should('have.text', '11/280 characters') cy.get('.character-count').should('contain', '11 / 280 characters')
}) })
it('should mention a user', () => { it('should mention a user', () => {
cy.get('.tiptap').type('{selectall}{backspace}@') cy.get('.tiptap').type('{selectall}{backspace}@')
// check if the mention autocomplete is visible // check if the mention autocomplete is visible
cy.get('.tippy-content .items').should('be.visible') cy.get('.tippy-content .dropdown-menu').should('be.visible')
// select the first user // select the first user
cy.get('.tippy-content .items .item').first().then($el => { cy.get('.tippy-content .dropdown-menu button').first().then($el => {
const name = $el.text() const name = $el.text()
$el.click() $el.click()
// check if the user is mentioned // check if the user is mentioned
cy.get('.tiptap').should('have.text', `@${name} `) cy.get('.tiptap').should('have.text', `@${name} `)
cy.get('.character-count__text').should('have.text', '2/280 characters') cy.get('.character-count').should('contain', '2 / 280 characters')
}) })
}) })

View File

@ -6,7 +6,6 @@
height="20" height="20"
width="20" width="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
class="character-count__graph"
> >
<circle <circle
r="10" r="10"
@ -32,7 +31,7 @@
/> />
</svg> </svg>
<div class="character-count__text">{{ editor.storage.characterCount.characters() }}/{{ limit }} characters</div> {{ editor.storage.characterCount.characters() }} / {{ limit }} characters
</div> </div>
</template> </template>
@ -97,43 +96,35 @@ export default {
<style lang="scss"> <style lang="scss">
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
h1, .mention {
h2, background-color: var(--purple-light);
h3, border-radius: 0.4rem;
h4, box-decoration-break: clone;
h5, color: var(--purple);
h6 { padding: 0.1rem 0.3rem;
line-height: 1.1;
} }
} }
.mention { /* Character count */
border: 1px solid #000;
border-radius: 0.4rem;
padding: 0.1rem 0.3rem;
box-decoration-break: clone;
}
.character-count { .character-count {
margin-top: 1rem;
display: flex;
align-items: center; align-items: center;
color: #68CEF8; color: var(--gray-5);
display: flex;
font-size: 0.75rem;
gap: .5rem;
margin: 1.5rem;
&--warning { svg {
color: #FB5151; color: var(--purple);
} }
&__graph { &--warning,
margin-right: 0.5rem; &--warning svg {
} color: var(--red);
&__text {
color: #868e96;
} }
} }
</style> </style>

View File

@ -1,24 +1,109 @@
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
}
/* List styles */
ul,
ol {
padding: 0 1rem;
margin: 1.25rem 1rem 1.25rem 0.4rem;
li p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
}
/* Heading styles */
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
}
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code {
background-color: var(--purple-light);
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}
pre {
background: var(--black);
border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
}
blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem;
}
hr {
border: none;
border-top: 1px solid var(--gray-2);
margin: 2rem 0;
}
/* Placeholder (at the top) */
/* p.is-editor-empty:first-child::before {
color: var(--gray-4);
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}*/
/* Placeholder (on every new line) */
.is-empty::before {
color: var(--gray-4);
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
} }
} }
/* Placeholder (at the top) */
/*.tiptap p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;
color: #ced4da;
pointer-events: none;
height: 0;
}*/
/* Placeholder (on every new line) */
.tiptap .is-empty::before {
content: attr(data-placeholder);
float: left;
color: #ced4da;
pointer-events: none;
height: 0;
}

View File

@ -60,26 +60,111 @@ export default {
<style lang="scss"> <style lang="scss">
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
}
/* List styles */
ul,
ol {
padding: 0 1rem;
margin: 1.25rem 1rem 1.25rem 0.4rem;
li p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
}
/* Heading styles */
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
}
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code {
background-color: var(--purple-light);
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}
pre {
background: var(--black);
border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
}
blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem;
}
hr {
border: none;
border-top: 1px solid var(--gray-2);
margin: 2rem 0;
}
/* Placeholder (at the top) */
/* p.is-editor-empty:first-child::before {
color: var(--gray-4);
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}*/
/* Placeholder (on every new line) */
.is-empty::before {
color: var(--gray-4);
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
} }
} }
/* Placeholder (at the top) */
/*.tiptap p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;
color: #ced4da;
pointer-events: none;
height: 0;
}*/
/* Placeholder (on every new line) */
.tiptap .is-empty::before {
content: attr(data-placeholder);
float: left;
color: #ced4da;
pointer-events: none;
height: 0;
}
</style> </style>

View File

@ -1,24 +1,91 @@
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
}
/* List styles */
ul,
ol {
padding: 0 1rem;
margin: 1.25rem 1rem 1.25rem 0.4rem;
li p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
}
/* Heading styles */
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
}
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code {
background-color: var(--purple-light);
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}
pre {
background: var(--black);
border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
}
blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem;
}
hr {
border: none;
border-top: 1px solid var(--gray-2);
margin: 2rem 0;
} }
} }
/* Placeholder (at the top) */
/*.tiptap p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;
color: #ced4da;
pointer-events: none;
height: 0;
}*/
/* Placeholder (on every new line) */
.tiptap .is-empty::before {
content: attr(data-placeholder);
float: left;
color: #ced4da;
pointer-events: none;
height: 0;
}

View File

@ -15,168 +15,170 @@ const MenuBar = () => {
} }
return ( return (
<> <div className="control-group">
<button <div className="button-group">
onClick={() => editor.chain().focus().toggleBold().run()} <button
disabled={ onClick={() => editor.chain().focus().toggleBold().run()}
!editor.can() disabled={
.chain() !editor.can()
.focus() .chain()
.toggleBold() .focus()
.run() .toggleBold()
} .run()
className={editor.isActive('bold') ? 'is-active' : ''} }
> className={editor.isActive('bold') ? 'is-active' : ''}
bold >
</button> Bold
<button </button>
onClick={() => editor.chain().focus().toggleItalic().run()} <button
disabled={ onClick={() => editor.chain().focus().toggleItalic().run()}
!editor.can() disabled={
.chain() !editor.can()
.focus() .chain()
.toggleItalic() .focus()
.run() .toggleItalic()
} .run()
className={editor.isActive('italic') ? 'is-active' : ''} }
> className={editor.isActive('italic') ? 'is-active' : ''}
italic >
</button> Italic
<button </button>
onClick={() => editor.chain().focus().toggleStrike().run()} <button
disabled={ onClick={() => editor.chain().focus().toggleStrike().run()}
!editor.can() disabled={
.chain() !editor.can()
.focus() .chain()
.toggleStrike() .focus()
.run() .toggleStrike()
} .run()
className={editor.isActive('strike') ? 'is-active' : ''} }
> className={editor.isActive('strike') ? 'is-active' : ''}
strike >
</button> Strike
<button </button>
onClick={() => editor.chain().focus().toggleCode().run()} <button
disabled={ onClick={() => editor.chain().focus().toggleCode().run()}
!editor.can() disabled={
.chain() !editor.can()
.focus() .chain()
.toggleCode() .focus()
.run() .toggleCode()
} .run()
className={editor.isActive('code') ? 'is-active' : ''} }
> className={editor.isActive('code') ? 'is-active' : ''}
code >
</button> Code
<button onClick={() => editor.chain().focus().unsetAllMarks().run()}> </button>
clear marks <button onClick={() => editor.chain().focus().unsetAllMarks().run()}>
</button> Clear marks
<button onClick={() => editor.chain().focus().clearNodes().run()}> </button>
clear nodes <button onClick={() => editor.chain().focus().clearNodes().run()}>
</button> Clear nodes
<button </button>
onClick={() => editor.chain().focus().setParagraph().run()} <button
className={editor.isActive('paragraph') ? 'is-active' : ''} onClick={() => editor.chain().focus().setParagraph().run()}
> className={editor.isActive('paragraph') ? 'is-active' : ''}
paragraph >
</button> Paragraph
<button </button>
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()} <button
className={editor.isActive('heading', { level: 1 }) ? 'is-active' : ''} onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
> className={editor.isActive('heading', { level: 1 }) ? 'is-active' : ''}
h1 >
</button> H1
<button </button>
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()} <button
className={editor.isActive('heading', { level: 2 }) ? 'is-active' : ''} onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
> className={editor.isActive('heading', { level: 2 }) ? 'is-active' : ''}
h2 >
</button> H2
<button </button>
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()} <button
className={editor.isActive('heading', { level: 3 }) ? 'is-active' : ''} onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
> className={editor.isActive('heading', { level: 3 }) ? 'is-active' : ''}
h3 >
</button> H3
<button </button>
onClick={() => editor.chain().focus().toggleHeading({ level: 4 }).run()} <button
className={editor.isActive('heading', { level: 4 }) ? 'is-active' : ''} onClick={() => editor.chain().focus().toggleHeading({ level: 4 }).run()}
> className={editor.isActive('heading', { level: 4 }) ? 'is-active' : ''}
h4 >
</button> H4
<button </button>
onClick={() => editor.chain().focus().toggleHeading({ level: 5 }).run()} <button
className={editor.isActive('heading', { level: 5 }) ? 'is-active' : ''} onClick={() => editor.chain().focus().toggleHeading({ level: 5 }).run()}
> className={editor.isActive('heading', { level: 5 }) ? 'is-active' : ''}
h5 >
</button> H5
<button </button>
onClick={() => editor.chain().focus().toggleHeading({ level: 6 }).run()} <button
className={editor.isActive('heading', { level: 6 }) ? 'is-active' : ''} onClick={() => editor.chain().focus().toggleHeading({ level: 6 }).run()}
> className={editor.isActive('heading', { level: 6 }) ? 'is-active' : ''}
h6 >
</button> H6
<button </button>
onClick={() => editor.chain().focus().toggleBulletList().run()} <button
className={editor.isActive('bulletList') ? 'is-active' : ''} onClick={() => editor.chain().focus().toggleBulletList().run()}
> className={editor.isActive('bulletList') ? 'is-active' : ''}
bullet list >
</button> Bullet list
<button </button>
onClick={() => editor.chain().focus().toggleOrderedList().run()} <button
className={editor.isActive('orderedList') ? 'is-active' : ''} onClick={() => editor.chain().focus().toggleOrderedList().run()}
> className={editor.isActive('orderedList') ? 'is-active' : ''}
ordered list >
</button> Ordered list
<button </button>
onClick={() => editor.chain().focus().toggleCodeBlock().run()} <button
className={editor.isActive('codeBlock') ? 'is-active' : ''} onClick={() => editor.chain().focus().toggleCodeBlock().run()}
> className={editor.isActive('codeBlock') ? 'is-active' : ''}
code block >
</button> Code block
<button </button>
onClick={() => editor.chain().focus().toggleBlockquote().run()} <button
className={editor.isActive('blockquote') ? 'is-active' : ''} onClick={() => editor.chain().focus().toggleBlockquote().run()}
> className={editor.isActive('blockquote') ? 'is-active' : ''}
blockquote >
</button> Blockquote
<button onClick={() => editor.chain().focus().setHorizontalRule().run()}> </button>
horizontal rule <button onClick={() => editor.chain().focus().setHorizontalRule().run()}>
</button> Horizontal rule
<button onClick={() => editor.chain().focus().setHardBreak().run()}> </button>
hard break <button onClick={() => editor.chain().focus().setHardBreak().run()}>
</button> Hard break
<button </button>
onClick={() => editor.chain().focus().undo().run()} <button
disabled={ onClick={() => editor.chain().focus().undo().run()}
!editor.can() disabled={
.chain() !editor.can()
.focus() .chain()
.undo() .focus()
.run() .undo()
} .run()
> }
undo >
</button> Undo
<button </button>
onClick={() => editor.chain().focus().redo().run()} <button
disabled={ onClick={() => editor.chain().focus().redo().run()}
!editor.can() disabled={
.chain() !editor.can()
.focus() .chain()
.redo() .focus()
.run() .redo()
} .run()
> }
redo >
</button> Redo
<button </button>
onClick={() => editor.chain().focus().setColor('#958DF1').run()} <button
className={editor.isActive('textStyle', { color: '#958DF1' }) ? 'is-active' : ''} onClick={() => editor.chain().focus().setColor('#958DF1').run()}
> className={editor.isActive('textStyle', { color: '#958DF1' }) ? 'is-active' : ''}
purple >
</button> Purple
</> </button>
</div>
</div>
) )
} }
@ -200,7 +202,7 @@ const content = `
Hi there, Hi there,
</h2> </h2>
<p> <p>
this is a <em>basic</em> example of <strong>tiptap</strong>. Sure, there are all kind of basic text styles youd probably expect from a text editor. But wait until you see the lists: this is a <em>basic</em> example of <strong>Tiptap</strong>. Sure, there are all kind of basic text styles youd probably expect from a text editor. But wait until you see the lists:
</p> </p>
<ul> <ul>
<li> <li>
@ -214,7 +216,7 @@ const content = `
Isnt that great? And all of that is editable. But wait, theres more. Lets try a code block: Isnt that great? And all of that is editable. But wait, theres more. Lets try a code block:
</p> </p>
<pre><code class="language-css">body { <pre><code class="language-css">body {
display: none; display: none;
}</code></pre> }</code></pre>
<p> <p>
I know, I know, this is impressive. Its only the tip of the iceberg though. Give it a try and click a little bit around. Dont forget to check the other examples too. I know, I know, this is impressive. Its only the tip of the iceberg though. Give it a try and click a little bit around. Dont forget to check the other examples too.

View File

@ -21,41 +21,41 @@ context('/src/Examples/Default/React/', () => {
}) })
const buttonMarks = [ const buttonMarks = [
{ label: 'bold', tag: 'strong' }, { label: 'Bold', tag: 'strong' },
{ label: 'italic', tag: 'em' }, { label: 'Italic', tag: 'em' },
{ label: 'strike', tag: 's' }, { label: 'Strike', tag: 's' },
] ]
buttonMarks.forEach(m => { buttonMarks.forEach(m => {
it(`should disable ${m.label} when the code tag is enabled for cursor`, () => { it(`should disable ${m.label} when the code tag is enabled for cursor`, () => {
cy.get('.tiptap').type('{selectall}Hello world') cy.get('.tiptap').type('{selectall}Hello world')
cy.get('button').contains('code').click() cy.get('button').contains('Code').click()
cy.get('button').contains(m.label).should('be.disabled') cy.get('button').contains(m.label).should('be.disabled')
}) })
it(`should enable ${m.label} when the code tag is disabled for cursor`, () => { it(`should enable ${m.label} when the code tag is disabled for cursor`, () => {
cy.get('.tiptap').type('{selectall}Hello world') cy.get('.tiptap').type('{selectall}Hello world')
cy.get('button').contains('code').click() cy.get('button').contains('Code').click()
cy.get('button').contains('code').click() cy.get('button').contains('Code').click()
cy.get('button').contains(m.label).should('not.be.disabled') cy.get('button').contains(m.label).should('not.be.disabled')
}) })
it(`should disable ${m.label} when the code tag is enabled for selection`, () => { it(`should disable ${m.label} when the code tag is enabled for selection`, () => {
cy.get('.tiptap').type('{selectall}Hello world{selectall}') cy.get('.tiptap').type('{selectall}Hello world{selectall}')
cy.get('button').contains('code').click() cy.get('button').contains('Code').click()
cy.get('button').contains(m.label).should('be.disabled') cy.get('button').contains(m.label).should('be.disabled')
}) })
it(`should enable ${m.label} when the code tag is disabled for selection`, () => { it(`should enable ${m.label} when the code tag is disabled for selection`, () => {
cy.get('.tiptap').type('{selectall}Hello world{selectall}') cy.get('.tiptap').type('{selectall}Hello world{selectall}')
cy.get('button').contains('code').click() cy.get('button').contains('Code').click()
cy.get('button').contains('code').click() cy.get('button').contains('Code').click()
cy.get('button').contains(m.label).should('not.be.disabled') cy.get('button').contains(m.label).should('not.be.disabled')
}) })
it(`should apply ${m.label} when the button is pressed`, () => { it(`should apply ${m.label} when the button is pressed`, () => {
cy.get('.tiptap').type('{selectall}Hello world') cy.get('.tiptap').type('{selectall}Hello world')
cy.get('button').contains('paragraph').click() cy.get('button').contains('Paragraph').click()
cy.get('.tiptap').type('{selectall}') cy.get('.tiptap').type('{selectall}')
cy.get('button').contains(m.label).click() cy.get('button').contains(m.label).click()
cy.get(`.tiptap ${m.tag}`).should('exist').should('have.text', 'Hello world') cy.get(`.tiptap ${m.tag}`).should('exist').should('have.text', 'Hello world')
@ -64,40 +64,40 @@ context('/src/Examples/Default/React/', () => {
it('should clear marks when the button is pressed', () => { it('should clear marks when the button is pressed', () => {
cy.get('.tiptap').type('{selectall}Hello world') cy.get('.tiptap').type('{selectall}Hello world')
cy.get('button').contains('paragraph').click() cy.get('button').contains('Paragraph').click()
cy.get('.tiptap').type('{selectall}') cy.get('.tiptap').type('{selectall}')
cy.get('button').contains('bold').click() cy.get('button').contains('Bold').click()
cy.get('.tiptap strong').should('exist').should('have.text', 'Hello world') cy.get('.tiptap strong').should('exist').should('have.text', 'Hello world')
cy.get('button').contains('clear marks').click() cy.get('button').contains('Clear marks').click()
cy.get('.tiptap strong').should('not.exist') cy.get('.tiptap strong').should('not.exist')
}) })
it('should clear nodes when the button is pressed', () => { it('should clear nodes when the button is pressed', () => {
cy.get('.tiptap').type('{selectall}Hello world') cy.get('.tiptap').type('{selectall}Hello world')
cy.get('button').contains('bullet list').click() cy.get('button').contains('Bullet list').click()
cy.get('.tiptap ul').should('exist').should('have.text', 'Hello world') cy.get('.tiptap ul').should('exist').should('have.text', 'Hello world')
cy.get('.tiptap').type('{enter}A second item{enter}A third item{selectall}') cy.get('.tiptap').type('{enter}A second item{enter}A third item{selectall}')
cy.get('button').contains('clear nodes').click() cy.get('button').contains('Clear nodes').click()
cy.get('.tiptap ul').should('not.exist') cy.get('.tiptap ul').should('not.exist')
cy.get('.tiptap p').should('have.length', 3) cy.get('.tiptap p').should('have.length', 3)
}) })
const buttonNodes = [ const buttonNodes = [
{ label: 'h1', tag: 'h1' }, { label: 'H1', tag: 'h1' },
{ label: 'h2', tag: 'h2' }, { label: 'H2', tag: 'h2' },
{ label: 'h3', tag: 'h3' }, { label: 'H3', tag: 'h3' },
{ label: 'h4', tag: 'h4' }, { label: 'H4', tag: 'h4' },
{ label: 'h5', tag: 'h5' }, { label: 'H5', tag: 'h5' },
{ label: 'h6', tag: 'h6' }, { label: 'H6', tag: 'h6' },
{ label: 'bullet list', tag: 'ul' }, { label: 'Bullet list', tag: 'ul' },
{ label: 'ordered list', tag: 'ol' }, { label: 'Ordered list', tag: 'ol' },
{ label: 'code block', tag: 'pre code' }, { label: 'Code block', tag: 'pre code' },
{ label: 'blockquote', tag: 'blockquote' }, { label: 'Blockquote', tag: 'blockquote' },
] ]
buttonNodes.forEach(n => { buttonNodes.forEach(n => {
it(`should set ${n.label} when the button is pressed`, () => { it(`should set ${n.label} when the button is pressed`, () => {
cy.get('button').contains('paragraph').click() cy.get('button').contains('Paragraph').click()
cy.get('.tiptap').type('{selectall}Hello world{selectall}') cy.get('.tiptap').type('{selectall}Hello world{selectall}')
cy.get('button').contains(n.label).click() cy.get('button').contains(n.label).click()
@ -109,35 +109,35 @@ context('/src/Examples/Default/React/', () => {
it('should add a hr when on the same line as a node', () => { it('should add a hr when on the same line as a node', () => {
cy.get('.tiptap').type('{rightArrow}') cy.get('.tiptap').type('{rightArrow}')
cy.get('button').contains('horizontal rule').click() cy.get('button').contains('Horizontal rule').click()
cy.get('.tiptap hr').should('exist') cy.get('.tiptap hr').should('exist')
cy.get('.tiptap h1').should('exist') cy.get('.tiptap h1').should('exist')
}) })
it('should add a hr when on a new line', () => { it('should add a hr when on a new line', () => {
cy.get('.tiptap').type('{rightArrow}{enter}') cy.get('.tiptap').type('{rightArrow}{enter}')
cy.get('button').contains('horizontal rule').click() cy.get('button').contains('Horizontal rule').click()
cy.get('.tiptap hr').should('exist') cy.get('.tiptap hr').should('exist')
cy.get('.tiptap h1').should('exist') cy.get('.tiptap h1').should('exist')
}) })
it('should add a br', () => { it('should add a br', () => {
cy.get('.tiptap').type('{rightArrow}') cy.get('.tiptap').type('{rightArrow}')
cy.get('button').contains('hard break').click() cy.get('button').contains('Hard break').click()
cy.get('.tiptap h1 br').should('exist') cy.get('.tiptap h1 br').should('exist')
}) })
it('should undo', () => { it('should undo', () => {
cy.get('.tiptap').type('{selectall}{backspace}') cy.get('.tiptap').type('{selectall}{backspace}')
cy.get('button').contains('undo').click() cy.get('button').contains('Undo').click()
cy.get('.tiptap').should('contain', 'Hello world') cy.get('.tiptap').should('contain', 'Hello world')
}) })
it('should redo', () => { it('should redo', () => {
cy.get('.tiptap').type('{selectall}{backspace}') cy.get('.tiptap').type('{selectall}{backspace}')
cy.get('button').contains('undo').click() cy.get('button').contains('Undo').click()
cy.get('.tiptap').should('contain', 'Hello world') cy.get('.tiptap').should('contain', 'Hello world')
cy.get('button').contains('redo').click() cy.get('button').contains('Redo').click()
cy.get('.tiptap').should('not.contain', 'Hello world') cy.get('.tiptap').should('not.contain', 'Hello world')
}) })
}) })

View File

@ -1,56 +1,91 @@
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
ul, /* List styles */
ul,
ol { ol {
padding: 0 1rem; padding: 0 1rem;
} margin: 1.25rem 1rem 1.25rem 0.4rem;
h1, li p {
h2, margin-top: 0.25em;
h3, margin-bottom: 0.25em;
h4,
h5,
h6 {
line-height: 1.1;
}
code {
background-color: rgba(#616161, 0.1);
color: #616161;
}
pre {
background: #0D0D0D;
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
} }
} }
img { /* Heading styles */
max-width: 100%; h1,
height: auto; h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
}
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code {
background-color: var(--purple-light);
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}
pre {
background: var(--black);
border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
} }
blockquote { blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem; padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
} }
hr { hr {
border: none; border: none;
border-top: 2px solid rgba(#0D0D0D, 0.1); border-top: 1px solid var(--gray-2);
margin: 2rem 0; margin: 2rem 0;
} }
} }

View File

@ -21,15 +21,15 @@ context('/src/Examples/Default/React/', () => {
}) })
const buttonMarks = [ const buttonMarks = [
{ label: 'bold', tag: 'strong' }, { label: 'Bold', tag: 'strong' },
{ label: 'italic', tag: 'em' }, { label: 'Italic', tag: 'em' },
{ label: 'strike', tag: 's' }, { label: 'Strike', tag: 's' },
] ]
buttonMarks.forEach(m => { buttonMarks.forEach(m => {
it(`should apply ${m.label} when the button is pressed`, () => { it(`should apply ${m.label} when the button is pressed`, () => {
cy.get('.tiptap').type('{selectall}Hello world') cy.get('.tiptap').type('{selectall}Hello world')
cy.get('button').contains('paragraph').click() cy.get('button').contains('Paragraph').click()
cy.get('.tiptap').type('{selectall}') cy.get('.tiptap').type('{selectall}')
cy.get('button').contains(m.label).click() cy.get('button').contains(m.label).click()
cy.get(`.tiptap ${m.tag}`).should('exist').should('have.text', 'Hello world') cy.get(`.tiptap ${m.tag}`).should('exist').should('have.text', 'Hello world')
@ -38,40 +38,40 @@ context('/src/Examples/Default/React/', () => {
it('should clear marks when the button is pressed', () => { it('should clear marks when the button is pressed', () => {
cy.get('.tiptap').type('{selectall}Hello world') cy.get('.tiptap').type('{selectall}Hello world')
cy.get('button').contains('paragraph').click() cy.get('button').contains('Paragraph').click()
cy.get('.tiptap').type('{selectall}') cy.get('.tiptap').type('{selectall}')
cy.get('button').contains('bold').click() cy.get('button').contains('Bold').click()
cy.get('.tiptap strong').should('exist').should('have.text', 'Hello world') cy.get('.tiptap strong').should('exist').should('have.text', 'Hello world')
cy.get('button').contains('clear marks').click() cy.get('button').contains('Clear marks').click()
cy.get('.tiptap strong').should('not.exist') cy.get('.tiptap strong').should('not.exist')
}) })
it('should clear nodes when the button is pressed', () => { it('should clear nodes when the button is pressed', () => {
cy.get('.tiptap').type('{selectall}Hello world') cy.get('.tiptap').type('{selectall}Hello world')
cy.get('button').contains('bullet list').click() cy.get('button').contains('Bullet list').click()
cy.get('.tiptap ul').should('exist').should('have.text', 'Hello world') cy.get('.tiptap ul').should('exist').should('have.text', 'Hello world')
cy.get('.tiptap').type('{enter}A second item{enter}A third item{selectall}') cy.get('.tiptap').type('{enter}A second item{enter}A third item{selectall}')
cy.get('button').contains('clear nodes').click() cy.get('button').contains('Clear nodes').click()
cy.get('.tiptap ul').should('not.exist') cy.get('.tiptap ul').should('not.exist')
cy.get('.tiptap p').should('have.length', 3) cy.get('.tiptap p').should('have.length', 3)
}) })
const buttonNodes = [ const buttonNodes = [
{ label: 'h1', tag: 'h1' }, { label: 'H1', tag: 'h1' },
{ label: 'h2', tag: 'h2' }, { label: 'H2', tag: 'h2' },
{ label: 'h3', tag: 'h3' }, { label: 'H3', tag: 'h3' },
{ label: 'h4', tag: 'h4' }, { label: 'H4', tag: 'h4' },
{ label: 'h5', tag: 'h5' }, { label: 'H5', tag: 'h5' },
{ label: 'h6', tag: 'h6' }, { label: 'H6', tag: 'h6' },
{ label: 'bullet list', tag: 'ul' }, { label: 'Bullet list', tag: 'ul' },
{ label: 'ordered list', tag: 'ol' }, { label: 'Ordered list', tag: 'ol' },
{ label: 'code block', tag: 'pre code' }, { label: 'Code block', tag: 'pre code' },
{ label: 'blockquote', tag: 'blockquote' }, { label: 'Blockquote', tag: 'blockquote' },
] ]
buttonNodes.forEach(n => { buttonNodes.forEach(n => {
it(`should set ${n.label} when the button is pressed`, () => { it(`should set ${n.label} when the button is pressed`, () => {
cy.get('button').contains('paragraph').click() cy.get('button').contains('Paragraph').click()
cy.get('.tiptap').type('{selectall}Hello world{selectall}') cy.get('.tiptap').type('{selectall}Hello world{selectall}')
cy.get('button').contains(n.label).click() cy.get('button').contains(n.label).click()
@ -83,35 +83,35 @@ context('/src/Examples/Default/React/', () => {
it('should add a hr when on the same line as a node', () => { it('should add a hr when on the same line as a node', () => {
cy.get('.tiptap').type('{rightArrow}') cy.get('.tiptap').type('{rightArrow}')
cy.get('button').contains('horizontal rule').click() cy.get('button').contains('Horizontal rule').click()
cy.get('.tiptap hr').should('exist') cy.get('.tiptap hr').should('exist')
cy.get('.tiptap h1').should('exist') cy.get('.tiptap h1').should('exist')
}) })
it('should add a hr when on a new line', () => { it('should add a hr when on a new line', () => {
cy.get('.tiptap').type('{rightArrow}{enter}') cy.get('.tiptap').type('{rightArrow}{enter}')
cy.get('button').contains('horizontal rule').click() cy.get('button').contains('Horizontal rule').click()
cy.get('.tiptap hr').should('exist') cy.get('.tiptap hr').should('exist')
cy.get('.tiptap h1').should('exist') cy.get('.tiptap h1').should('exist')
}) })
it('should add a br', () => { it('should add a br', () => {
cy.get('.tiptap').type('{rightArrow}') cy.get('.tiptap').type('{rightArrow}')
cy.get('button').contains('hard break').click() cy.get('button').contains('Hard break').click()
cy.get('.tiptap h1 br').should('exist') cy.get('.tiptap h1 br').should('exist')
}) })
it('should undo', () => { it('should undo', () => {
cy.get('.tiptap').type('{selectall}{backspace}') cy.get('.tiptap').type('{selectall}{backspace}')
cy.get('button').contains('undo').click() cy.get('button').contains('Undo').click()
cy.get('.tiptap').should('contain', 'Hello world') cy.get('.tiptap').should('contain', 'Hello world')
}) })
it('should redo', () => { it('should redo', () => {
cy.get('.tiptap').type('{selectall}{backspace}') cy.get('.tiptap').type('{selectall}{backspace}')
cy.get('button').contains('undo').click() cy.get('button').contains('Undo').click()
cy.get('.tiptap').should('contain', 'Hello world') cy.get('.tiptap').should('contain', 'Hello world')
cy.get('button').contains('redo').click() cy.get('button').contains('Redo').click()
cy.get('.tiptap').should('not.contain', 'Hello world') cy.get('.tiptap').should('not.contain', 'Hello world')
}) })
}) })

View File

@ -1,6 +1,9 @@
<script> <script>
import "./styles.scss"; import "./styles.scss";
import { Color } from '@tiptap/extension-color'
import ListItem from '@tiptap/extension-list-item'
import TextStyle from '@tiptap/extension-text-style'
import StarterKit from "@tiptap/starter-kit"; import StarterKit from "@tiptap/starter-kit";
import { Editor } from "@tiptap/core"; import { Editor } from "@tiptap/core";
import { onMount } from "svelte"; import { onMount } from "svelte";
@ -11,13 +14,17 @@
onMount(() => { onMount(() => {
editor = new Editor({ editor = new Editor({
element: element, element: element,
extensions: [StarterKit], extensions: [
Color.configure({ types: [TextStyle.name, ListItem.name] }),
TextStyle.configure({ types: [ListItem.name] }),
StarterKit,
],
content: ` content: `
<h2> <h2>
Hi there, Hi there,
</h2> </h2>
<p> <p>
this is a <em>basic</em> example of <strong>tiptap</strong>. Sure, there are all kind of basic text styles youd probably expect from a text editor. But wait until you see the lists: this is a <em>basic</em> example of <strong>Tiptap</strong>. Sure, there are all kind of basic text styles youd probably expect from a text editor. But wait until you see the lists:
</p> </p>
<ul> <ul>
<li> <li>
@ -31,8 +38,8 @@
Isnt that great? And all of that is editable. But wait, theres more. Lets try a code block: Isnt that great? And all of that is editable. But wait, theres more. Lets try a code block:
</p> </p>
<pre><code class="language-css">body { <pre><code class="language-css">body {
display: none; display: none;
}</code></pre> }</code></pre>
<p> <p>
I know, I know, this is impressive. Its only the tip of the iceberg though. Give it a try and click a little bit around. Dont forget to check the other examples too. I know, I know, this is impressive. Its only the tip of the iceberg though. Give it a try and click a little bit around. Dont forget to check the other examples too.
</p> </p>
@ -51,119 +58,125 @@
</script> </script>
{#if editor} {#if editor}
<div> <div class="control-group">
<div> <div class="button-group">
<button <button
on:click={() => console.log && editor.chain().focus().toggleBold().run()} on:click={() => console.log && editor.chain().focus().toggleBold().run()}
disabled={!editor.can().chain().focus().toggleBold().run()} disabled={!editor.can().chain().focus().toggleBold().run()}
class={editor.isActive("bold") ? "is-active" : ""} class={editor.isActive("bold") ? "is-active" : ""}
> >
bold Bold
</button> </button>
<button <button
on:click={() => editor.chain().focus().toggleItalic().run()} on:click={() => editor.chain().focus().toggleItalic().run()}
disabled={!editor.can().chain().focus().toggleItalic().run()} disabled={!editor.can().chain().focus().toggleItalic().run()}
class={editor.isActive("italic") ? "is-active" : ""} class={editor.isActive("italic") ? "is-active" : ""}
> >
italic Italic
</button> </button>
<button <button
on:click={() => editor.chain().focus().toggleStrike().run()} on:click={() => editor.chain().focus().toggleStrike().run()}
disabled={!editor.can().chain().focus().toggleStrike().run()} disabled={!editor.can().chain().focus().toggleStrike().run()}
class={editor.isActive("strike") ? "is-active" : ""} class={editor.isActive("strike") ? "is-active" : ""}
> >
strike Strike
</button> </button>
<button <button
on:click={() => editor.chain().focus().toggleCode().run()} on:click={() => editor.chain().focus().toggleCode().run()}
disabled={!editor.can().chain().focus().toggleCode().run()} disabled={!editor.can().chain().focus().toggleCode().run()}
class={editor.isActive("code") ? "is-active" : ""} class={editor.isActive("code") ? "is-active" : ""}
> >
code Code
</button> </button>
<button on:click={() => editor.chain().focus().unsetAllMarks().run()}> clear marks </button> <button on:click={() => editor.chain().focus().unsetAllMarks().run()}>Clear marks</button>
<button on:click={() => editor.chain().focus().clearNodes().run()}> clear nodes </button> <button on:click={() => editor.chain().focus().clearNodes().run()}>Clear nodes</button>
<button <button
on:click={() => editor.chain().focus().setParagraph().run()} on:click={() => editor.chain().focus().setParagraph().run()}
class={editor.isActive("paragraph") ? "is-active" : ""} class={editor.isActive("paragraph") ? "is-active" : ""}
> >
paragraph Paragraph
</button> </button>
<button <button
on:click={() => editor.chain().focus().toggleHeading({ level: 1 }).run()} on:click={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
class={editor.isActive("heading", { level: 1 }) ? "is-active" : ""} class={editor.isActive("heading", { level: 1 }) ? "is-active" : ""}
> >
h1 H1
</button> </button>
<button <button
on:click={() => editor.chain().focus().toggleHeading({ level: 2 }).run()} on:click={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
class={editor.isActive("heading", { level: 2 }) ? "is-active" : ""} class={editor.isActive("heading", { level: 2 }) ? "is-active" : ""}
> >
h2 H2
</button> </button>
<button <button
on:click={() => editor.chain().focus().toggleHeading({ level: 3 }).run()} on:click={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
class={editor.isActive("heading", { level: 3 }) ? "is-active" : ""} class={editor.isActive("heading", { level: 3 }) ? "is-active" : ""}
> >
h3 H3
</button> </button>
<button <button
on:click={() => editor.chain().focus().toggleHeading({ level: 4 }).run()} on:click={() => editor.chain().focus().toggleHeading({ level: 4 }).run()}
class={editor.isActive("heading", { level: 4 }) ? "is-active" : ""} class={editor.isActive("heading", { level: 4 }) ? "is-active" : ""}
> >
h4 H4
</button> </button>
<button <button
on:click={() => editor.chain().focus().toggleHeading({ level: 5 }).run()} on:click={() => editor.chain().focus().toggleHeading({ level: 5 }).run()}
class={editor.isActive("heading", { level: 5 }) ? "is-active" : ""} class={editor.isActive("heading", { level: 5 }) ? "is-active" : ""}
> >
h5 H5
</button> </button>
<button <button
on:click={() => editor.chain().focus().toggleHeading({ level: 6 }).run()} on:click={() => editor.chain().focus().toggleHeading({ level: 6 }).run()}
class={editor.isActive("heading", { level: 6 }) ? "is-active" : ""} class={editor.isActive("heading", { level: 6 }) ? "is-active" : ""}
> >
h6 H6
</button> </button>
<button <button
on:click={() => editor.chain().focus().toggleBulletList().run()} on:click={() => editor.chain().focus().toggleBulletList().run()}
class={editor.isActive("bulletList") ? "is-active" : ""} class={editor.isActive("bulletList") ? "is-active" : ""}
> >
bullet list Bullet list
</button> </button>
<button <button
on:click={() => editor.chain().focus().toggleOrderedList().run()} on:click={() => editor.chain().focus().toggleOrderedList().run()}
class={editor.isActive("orderedList") ? "is-active" : ""} class={editor.isActive("orderedList") ? "is-active" : ""}
> >
ordered list Ordered list
</button> </button>
<button <button
on:click={() => editor.chain().focus().toggleCodeBlock().run()} on:click={() => editor.chain().focus().toggleCodeBlock().run()}
class={editor.isActive("codeBlock") ? "is-active" : ""} class={editor.isActive("codeBlock") ? "is-active" : ""}
> >
code block Code block
</button> </button>
<button <button
on:click={() => editor.chain().focus().toggleBlockquote().run()} on:click={() => editor.chain().focus().toggleBlockquote().run()}
class={editor.isActive("blockquote") ? "is-active" : ""} class={editor.isActive("blockquote") ? "is-active" : ""}
> >
blockquote Blockquote
</button> </button>
<button on:click={() => editor.chain().focus().setHorizontalRule().run()}> <button on:click={() => editor.chain().focus().setHorizontalRule().run()}>
horizontal rule Horizontal rule
</button> </button>
<button on:click={() => editor.chain().focus().setHardBreak().run()}> hard break </button> <button on:click={() => editor.chain().focus().setHardBreak().run()}>Hard break</button>
<button <button
on:click={() => editor.chain().focus().undo().run()} on:click={() => editor.chain().focus().undo().run()}
disabled={!editor.can().chain().focus().undo().run()} disabled={!editor.can().chain().focus().undo().run()}
> >
undo Undo
</button> </button>
<button <button
on:click={() => editor.chain().focus().redo().run()} on:click={() => editor.chain().focus().redo().run()}
disabled={!editor.can().chain().focus().redo().run()} disabled={!editor.can().chain().focus().redo().run()}
> >
redo Redo
</button>
<button
on:click={() => editor.chain().focus().setColor('#958DF1').run()}
class={editor.isActive('textStyle', { color: '#958DF1' }) ? 'is-active' : ''}
>
Purple
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,56 +1,91 @@
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
ul, /* List styles */
ul,
ol { ol {
padding: 0 1rem; padding: 0 1rem;
} margin: 1.25rem 1rem 1.25rem 0.4rem;
h1, li p {
h2, margin-top: 0.25em;
h3, margin-bottom: 0.25em;
h4,
h5,
h6 {
line-height: 1.1;
}
code {
background-color: rgba(#616161, 0.1);
color: #616161;
}
pre {
background: #0D0D0D;
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
} }
} }
img { /* Heading styles */
max-width: 100%; h1,
height: auto; h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
}
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code {
background-color: var(--purple-light);
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}
pre {
background: var(--black);
border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
} }
blockquote { blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem; padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
} }
hr { hr {
border: none; border: none;
border-top: 2px solid rgba(#0D0D0D, 0.1); border-top: 1px solid var(--gray-2);
margin: 2rem 0; margin: 2rem 0;
} }
} }

View File

@ -21,15 +21,15 @@ context('/src/Examples/Default/Vue/', () => {
}) })
const buttonMarks = [ const buttonMarks = [
{ label: 'bold', tag: 'strong' }, { label: 'Bold', tag: 'strong' },
{ label: 'italic', tag: 'em' }, { label: 'Italic', tag: 'em' },
{ label: 'strike', tag: 's' }, { label: 'Strike', tag: 's' },
] ]
buttonMarks.forEach(m => { buttonMarks.forEach(m => {
it(`should apply ${m.label} when the button is pressed`, () => { it(`should apply ${m.label} when the button is pressed`, () => {
cy.get('.tiptap').type('{selectall}Hello world') cy.get('.tiptap').type('{selectall}Hello world')
cy.get('button').contains('paragraph').click() cy.get('button').contains('Paragraph').click()
cy.get('.tiptap').type('{selectall}') cy.get('.tiptap').type('{selectall}')
cy.get('button').contains(m.label).click() cy.get('button').contains(m.label).click()
cy.get(`.tiptap ${m.tag}`).should('exist').should('have.text', 'Hello world') cy.get(`.tiptap ${m.tag}`).should('exist').should('have.text', 'Hello world')
@ -38,40 +38,40 @@ context('/src/Examples/Default/Vue/', () => {
it('should clear marks when the button is pressed', () => { it('should clear marks when the button is pressed', () => {
cy.get('.tiptap').type('{selectall}Hello world') cy.get('.tiptap').type('{selectall}Hello world')
cy.get('button').contains('paragraph').click() cy.get('button').contains('Paragraph').click()
cy.get('.tiptap').type('{selectall}') cy.get('.tiptap').type('{selectall}')
cy.get('button').contains('bold').click() cy.get('button').contains('Bold').click()
cy.get('.tiptap strong').should('exist').should('have.text', 'Hello world') cy.get('.tiptap strong').should('exist').should('have.text', 'Hello world')
cy.get('button').contains('clear marks').click() cy.get('button').contains('Clear marks').click()
cy.get('.tiptap strong').should('not.exist') cy.get('.tiptap strong').should('not.exist')
}) })
it('should clear nodes when the button is pressed', () => { it('should clear nodes when the button is pressed', () => {
cy.get('.tiptap').type('{selectall}Hello world') cy.get('.tiptap').type('{selectall}Hello world')
cy.get('button').contains('bullet list').click() cy.get('button').contains('Bullet list').click()
cy.get('.tiptap ul').should('exist').should('have.text', 'Hello world') cy.get('.tiptap ul').should('exist').should('have.text', 'Hello world')
cy.get('.tiptap').type('{enter}A second item{enter}A third item{selectall}') cy.get('.tiptap').type('{enter}A second item{enter}A third item{selectall}')
cy.get('button').contains('clear nodes').click() cy.get('button').contains('Clear nodes').click()
cy.get('.tiptap ul').should('not.exist') cy.get('.tiptap ul').should('not.exist')
cy.get('.tiptap p').should('have.length', 3) cy.get('.tiptap p').should('have.length', 3)
}) })
const buttonNodes = [ const buttonNodes = [
{ label: 'h1', tag: 'h1' }, { label: 'H1', tag: 'h1' },
{ label: 'h2', tag: 'h2' }, { label: 'H2', tag: 'h2' },
{ label: 'h3', tag: 'h3' }, { label: 'H3', tag: 'h3' },
{ label: 'h4', tag: 'h4' }, { label: 'H4', tag: 'h4' },
{ label: 'h5', tag: 'h5' }, { label: 'H5', tag: 'h5' },
{ label: 'h6', tag: 'h6' }, { label: 'H6', tag: 'h6' },
{ label: 'bullet list', tag: 'ul' }, { label: 'Bullet list', tag: 'ul' },
{ label: 'ordered list', tag: 'ol' }, { label: 'Ordered list', tag: 'ol' },
{ label: 'code block', tag: 'pre code' }, { label: 'Code block', tag: 'pre code' },
{ label: 'blockquote', tag: 'blockquote' }, { label: 'Blockquote', tag: 'blockquote' },
] ]
buttonNodes.forEach(n => { buttonNodes.forEach(n => {
it(`should set ${n.label} when the button is pressed`, () => { it(`should set ${n.label} when the button is pressed`, () => {
cy.get('button').contains('paragraph').click() cy.get('button').contains('Paragraph').click()
cy.get('.tiptap').type('{selectall}Hello world{selectall}') cy.get('.tiptap').type('{selectall}Hello world{selectall}')
cy.get('button').contains(n.label).click() cy.get('button').contains(n.label).click()
@ -83,35 +83,35 @@ context('/src/Examples/Default/Vue/', () => {
it('should add a hr when on the same line as a node', () => { it('should add a hr when on the same line as a node', () => {
cy.get('.tiptap').type('{rightArrow}') cy.get('.tiptap').type('{rightArrow}')
cy.get('button').contains('horizontal rule').click() cy.get('button').contains('Horizontal rule').click()
cy.get('.tiptap hr').should('exist') cy.get('.tiptap hr').should('exist')
cy.get('.tiptap h1').should('exist') cy.get('.tiptap h1').should('exist')
}) })
it('should add a hr when on a new line', () => { it('should add a hr when on a new line', () => {
cy.get('.tiptap').type('{rightArrow}{enter}') cy.get('.tiptap').type('{rightArrow}{enter}')
cy.get('button').contains('horizontal rule').click() cy.get('button').contains('Horizontal rule').click()
cy.get('.tiptap hr').should('exist') cy.get('.tiptap hr').should('exist')
cy.get('.tiptap h1').should('exist') cy.get('.tiptap h1').should('exist')
}) })
it('should add a br', () => { it('should add a br', () => {
cy.get('.tiptap').type('{rightArrow}') cy.get('.tiptap').type('{rightArrow}')
cy.get('button').contains('hard break').click() cy.get('button').contains('Hard break').click()
cy.get('.tiptap h1 br').should('exist') cy.get('.tiptap h1 br').should('exist')
}) })
it('should undo', () => { it('should undo', () => {
cy.get('.tiptap').type('{selectall}{backspace}') cy.get('.tiptap').type('{selectall}{backspace}')
cy.get('button').contains('undo').click() cy.get('button').contains('Undo').click()
cy.get('.tiptap').should('contain', 'Hello world') cy.get('.tiptap').should('contain', 'Hello world')
}) })
it('should redo', () => { it('should redo', () => {
cy.get('.tiptap').type('{selectall}{backspace}') cy.get('.tiptap').type('{selectall}{backspace}')
cy.get('button').contains('undo').click() cy.get('button').contains('Undo').click()
cy.get('.tiptap').should('contain', 'Hello world') cy.get('.tiptap').should('contain', 'Hello world')
cy.get('button').contains('redo').click() cy.get('button').contains('Redo').click()
cy.get('.tiptap').should('not.contain', 'Hello world') cy.get('.tiptap').should('not.contain', 'Hello world')
}) })
}) })

View File

@ -1,73 +1,83 @@
<template> <template>
<div v-if="editor"> <div v-if="editor" class="container">
<button @click="editor.chain().focus().toggleBold().run()" :disabled="!editor.can().chain().focus().toggleBold().run()" :class="{ 'is-active': editor.isActive('bold') }"> <div class="control-group">
bold <div class="button-group">
</button> <button @click="editor.chain().focus().toggleBold().run()" :disabled="!editor.can().chain().focus().toggleBold().run()" :class="{ 'is-active': editor.isActive('bold') }">
<button @click="editor.chain().focus().toggleItalic().run()" :disabled="!editor.can().chain().focus().toggleItalic().run()" :class="{ 'is-active': editor.isActive('italic') }"> Bold
italic </button>
</button> <button @click="editor.chain().focus().toggleItalic().run()" :disabled="!editor.can().chain().focus().toggleItalic().run()" :class="{ 'is-active': editor.isActive('italic') }">
<button @click="editor.chain().focus().toggleStrike().run()" :disabled="!editor.can().chain().focus().toggleStrike().run()" :class="{ 'is-active': editor.isActive('strike') }"> Italic
strike </button>
</button> <button @click="editor.chain().focus().toggleStrike().run()" :disabled="!editor.can().chain().focus().toggleStrike().run()" :class="{ 'is-active': editor.isActive('strike') }">
<button @click="editor.chain().focus().toggleCode().run()" :disabled="!editor.can().chain().focus().toggleCode().run()" :class="{ 'is-active': editor.isActive('code') }"> Strike
code </button>
</button> <button @click="editor.chain().focus().toggleCode().run()" :disabled="!editor.can().chain().focus().toggleCode().run()" :class="{ 'is-active': editor.isActive('code') }">
<button @click="editor.chain().focus().unsetAllMarks().run()"> Code
clear marks </button>
</button> <button @click="editor.chain().focus().unsetAllMarks().run()">
<button @click="editor.chain().focus().clearNodes().run()"> Clear marks
clear nodes </button>
</button> <button @click="editor.chain().focus().clearNodes().run()">
<button @click="editor.chain().focus().setParagraph().run()" :class="{ 'is-active': editor.isActive('paragraph') }"> Clear nodes
paragraph </button>
</button> <button @click="editor.chain().focus().setParagraph().run()" :class="{ 'is-active': editor.isActive('paragraph') }">
<button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"> Paragraph
h1 </button>
</button> <button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 1 }) }">
<button @click="editor.chain().focus().toggleHeading({ level: 2 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"> H1
h2 </button>
</button> <button @click="editor.chain().focus().toggleHeading({ level: 2 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 2 }) }">
<button @click="editor.chain().focus().toggleHeading({ level: 3 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"> H2
h3 </button>
</button> <button @click="editor.chain().focus().toggleHeading({ level: 3 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 3 }) }">
<button @click="editor.chain().focus().toggleHeading({ level: 4 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 4 }) }"> H3
h4 </button>
</button> <button @click="editor.chain().focus().toggleHeading({ level: 4 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 4 }) }">
<button @click="editor.chain().focus().toggleHeading({ level: 5 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 5 }) }"> H4
h5 </button>
</button> <button @click="editor.chain().focus().toggleHeading({ level: 5 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 5 }) }">
<button @click="editor.chain().focus().toggleHeading({ level: 6 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 6 }) }"> H5
h6 </button>
</button> <button @click="editor.chain().focus().toggleHeading({ level: 6 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 6 }) }">
<button @click="editor.chain().focus().toggleBulletList().run()" :class="{ 'is-active': editor.isActive('bulletList') }"> H6
bullet list </button>
</button> <button @click="editor.chain().focus().toggleBulletList().run()" :class="{ 'is-active': editor.isActive('bulletList') }">
<button @click="editor.chain().focus().toggleOrderedList().run()" :class="{ 'is-active': editor.isActive('orderedList') }"> Bullet list
ordered list </button>
</button> <button @click="editor.chain().focus().toggleOrderedList().run()" :class="{ 'is-active': editor.isActive('orderedList') }">
<button @click="editor.chain().focus().toggleCodeBlock().run()" :class="{ 'is-active': editor.isActive('codeBlock') }"> Ordered list
code block </button>
</button> <button @click="editor.chain().focus().toggleCodeBlock().run()" :class="{ 'is-active': editor.isActive('codeBlock') }">
<button @click="editor.chain().focus().toggleBlockquote().run()" :class="{ 'is-active': editor.isActive('blockquote') }"> Code block
blockquote </button>
</button> <button @click="editor.chain().focus().toggleBlockquote().run()" :class="{ 'is-active': editor.isActive('blockquote') }">
<button @click="editor.chain().focus().setHorizontalRule().run()"> Blockquote
horizontal rule </button>
</button> <button @click="editor.chain().focus().setHorizontalRule().run()">
<button @click="editor.chain().focus().setHardBreak().run()"> Horizontal rule
hard break </button>
</button> <button @click="editor.chain().focus().setHardBreak().run()">
<button @click="editor.chain().focus().undo().run()" :disabled="!editor.can().chain().focus().undo().run()"> Hard break
undo </button>
</button> <button @click="editor.chain().focus().undo().run()" :disabled="!editor.can().chain().focus().undo().run()">
<button @click="editor.chain().focus().redo().run()" :disabled="!editor.can().chain().focus().redo().run()"> Undo
redo </button>
</button> <button @click="editor.chain().focus().redo().run()" :disabled="!editor.can().chain().focus().redo().run()">
Redo
</button>
<button @click="editor.chain().focus().setColor('#958DF1').run()" :class="{ 'is-active': editor.isActive('textStyle', { color: '#958DF1' }) }">
Purple
</button>
</div>
</div>
<editor-content :editor="editor" />
</div> </div>
<editor-content :editor="editor" />
</template> </template>
<script> <script>
import { Color } from '@tiptap/extension-color'
import ListItem from '@tiptap/extension-list-item'
import TextStyle from '@tiptap/extension-text-style'
import StarterKit from '@tiptap/starter-kit' import StarterKit from '@tiptap/starter-kit'
import { Editor, EditorContent } from '@tiptap/vue-3' import { Editor, EditorContent } from '@tiptap/vue-3'
@ -85,6 +95,8 @@ export default {
mounted() { mounted() {
this.editor = new Editor({ this.editor = new Editor({
extensions: [ extensions: [
Color.configure({ types: [TextStyle.name, ListItem.name] }),
TextStyle.configure({ types: [ListItem.name] }),
StarterKit, StarterKit,
], ],
content: ` content: `
@ -92,7 +104,7 @@ export default {
Hi there, Hi there,
</h2> </h2>
<p> <p>
this is a <em>basic</em> example of <strong>tiptap</strong>. Sure, there are all kind of basic text styles youd probably expect from a text editor. But wait until you see the lists: this is a <em>basic</em> example of <strong>Tiptap</strong>. Sure, there are all kind of basic text styles youd probably expect from a text editor. But wait until you see the lists:
</p> </p>
<ul> <ul>
<li> <li>
@ -129,15 +141,23 @@ export default {
<style lang="scss"> <style lang="scss">
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
/* List styles */
ul, ul,
ol { ol {
padding: 0 1rem; padding: 0 1rem;
margin: 1.25rem 1rem 1.25rem 0.4rem;
li p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
} }
/* Heading styles */
h1, h1,
h2, h2,
h3, h3,
@ -145,41 +165,68 @@ export default {
h5, h5,
h6 { h6 {
line-height: 1.1; line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
} }
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code { code {
background-color: rgba(#616161, 0.1); background-color: var(--purple-light);
color: #616161; border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
} }
pre { pre {
background: #0D0D0D; background: var(--black);
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem; border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code { code {
color: inherit;
padding: 0;
background: none; background: none;
color: inherit;
font-size: 0.8rem; font-size: 0.8rem;
padding: 0;
} }
} }
img {
max-width: 100%;
height: auto;
}
blockquote { blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem; padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
} }
hr { hr {
border: none; border: none;
border-top: 2px solid rgba(#0D0D0D, 0.1); border-top: 1px solid var(--gray-2);
margin: 2rem 0; margin: 2rem 0;
} }
} }

View File

@ -1,27 +1,31 @@
<template> <template>
<node-view-wrapper class="draw"> <node-view-wrapper class="draw">
<input type="color" v-model="color"> <div class="control-group">
<input <div class="button-group">
type="number" <input type="color" v-model="color">
min="1" <input
max="10" type="number"
v-model="size" min="1"
> max="10"
<button @click="clear"> v-model="size"
clear >
</button> <button @click="clear">
<svg viewBox="0 0 500 250" ref="canvas"> Clear
<template v-for="item in node.attrs.lines"> </button>
<path </div>
v-if="item.id !== id" <svg viewBox="0 0 500 250" ref="canvas">
:key="item.id" <template v-for="item in node.attrs.lines">
:d="item.path" <path
:id="`id-${item.id}`" v-if="item.id !== id"
:stroke="item.color" :key="item.id"
:stroke-width="item.size" :d="item.path"
/> :id="`id-${item.id}`"
</template> :stroke="item.color"
</svg> :stroke-width="item.size"
/>
</template>
</svg>
</div>
</node-view-wrapper> </node-view-wrapper>
</template> </template>

View File

@ -12,44 +12,46 @@ const MenuBar = ({ editor }) => {
} }
return ( return (
<> <div className="control-group">
<button onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()} className={editor.isActive('heading', { level: 1 }) ? 'is-active' : ''}> <div className="button-group">
h1 <button onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()} className={editor.isActive('heading', { level: 1 }) ? 'is-active' : ''}>
</button> H1
<button onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()} className={editor.isActive('heading', { level: 2 }) ? 'is-active' : ''}> </button>
h2 <button onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()} className={editor.isActive('heading', { level: 2 }) ? 'is-active' : ''}>
</button> H2
<button onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()} className={editor.isActive('heading', { level: 3 }) ? 'is-active' : ''}> </button>
h3 <button onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()} className={editor.isActive('heading', { level: 3 }) ? 'is-active' : ''}>
</button> H3
<button onClick={() => editor.chain().focus().setParagraph().run()} className={editor.isActive('paragraph') ? 'is-active' : ''}> </button>
paragraph <button onClick={() => editor.chain().focus().setParagraph().run()} className={editor.isActive('paragraph') ? 'is-active' : ''}>
</button> Paragraph
<button onClick={() => editor.chain().focus().toggleBold().run()} className={editor.isActive('bold') ? 'is-active' : ''}> </button>
bold <button onClick={() => editor.chain().focus().toggleBold().run()} className={editor.isActive('bold') ? 'is-active' : ''}>
</button> Bold
<button onClick={() => editor.chain().focus().toggleItalic().run()} className={editor.isActive('italic') ? 'is-active' : ''}> </button>
italic <button onClick={() => editor.chain().focus().toggleItalic().run()} className={editor.isActive('italic') ? 'is-active' : ''}>
</button> Italic
<button onClick={() => editor.chain().focus().toggleStrike().run()} className={editor.isActive('strike') ? 'is-active' : ''}> </button>
strike <button onClick={() => editor.chain().focus().toggleStrike().run()} className={editor.isActive('strike') ? 'is-active' : ''}>
</button> Strike
<button onClick={() => editor.chain().focus().toggleHighlight().run()} className={editor.isActive('highlight') ? 'is-active' : ''}> </button>
highlight <button onClick={() => editor.chain().focus().toggleHighlight().run()} className={editor.isActive('highlight') ? 'is-active' : ''}>
</button> Highlight
<button onClick={() => editor.chain().focus().setTextAlign('left').run()} className={editor.isActive({ textAlign: 'left' }) ? 'is-active' : ''}> </button>
left <button onClick={() => editor.chain().focus().setTextAlign('left').run()} className={editor.isActive({ textAlign: 'left' }) ? 'is-active' : ''}>
</button> Left
<button onClick={() => editor.chain().focus().setTextAlign('center').run()} className={editor.isActive({ textAlign: 'center' }) ? 'is-active' : ''}> </button>
center <button onClick={() => editor.chain().focus().setTextAlign('center').run()} className={editor.isActive({ textAlign: 'center' }) ? 'is-active' : ''}>
</button> Center
<button onClick={() => editor.chain().focus().setTextAlign('right').run()} className={editor.isActive({ textAlign: 'right' }) ? 'is-active' : ''}> </button>
right <button onClick={() => editor.chain().focus().setTextAlign('right').run()} className={editor.isActive({ textAlign: 'right' }) ? 'is-active' : ''}>
</button> Right
<button onClick={() => editor.chain().focus().setTextAlign('justify').run()} className={editor.isActive({ textAlign: 'justify' }) ? 'is-active' : ''}> </button>
justify <button onClick={() => editor.chain().focus().setTextAlign('justify').run()} className={editor.isActive({ textAlign: 'justify' }) ? 'is-active' : ''}>
</button> Justify
</> </button>
</div>
</div>
) )
} }
@ -91,9 +93,9 @@ export default () => {
}) })
return ( return (
<div> <>
<MenuBar editor={editor} /> <MenuBar editor={editor} />
<EditorContent editor={editor} /> <EditorContent editor={editor} />
</div> </>
) )
} }

View File

@ -8,7 +8,7 @@ context('/src/Examples/Formatting/React/', () => {
}) })
const marks = [ const marks = [
{ label: 'highlight', mark: 'mark' }, { label: 'Highlight', mark: 'mark' },
] ]
marks.forEach(m => { marks.forEach(m => {
@ -20,10 +20,10 @@ context('/src/Examples/Formatting/React/', () => {
}) })
const alignments = [ const alignments = [
{ label: 'left', alignment: 'left' }, { label: 'Left', alignment: 'left' },
{ label: 'center', alignment: 'center' }, { label: 'Center', alignment: 'center' },
{ label: 'right', alignment: 'right' }, { label: 'Right', alignment: 'right' },
{ label: 'justify', alignment: 'justify' }, { label: 'Justify', alignment: 'justify' },
] ]
alignments.forEach(a => { alignments.forEach(a => {

View File

@ -1,62 +1,98 @@
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
margin-top: 1rem; :first-child {
margin-top: 0;
> * + * {
margin-top: 0.75em;
} }
ul, /* List styles */
ul,
ol { ol {
padding: 0 1rem; padding: 0 1rem;
margin: 1.25rem 1rem 1.25rem 0.4rem;
li p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
} }
h1, /* Heading styles */
h2, h1,
h3, h2,
h4, h3,
h5, h4,
h5,
h6 { h6 {
line-height: 1.1; line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
} }
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code { code {
background-color: rgba(#616161, 0.1); background-color: var(--purple-light);
color: #616161; border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
} }
pre { pre {
background: #0D0D0D; background: var(--black);
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem; border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code { code {
color: inherit;
padding: 0;
background: none; background: none;
color: inherit;
font-size: 0.8rem; font-size: 0.8rem;
padding: 0;
} }
} }
mark { mark {
background-color: #FAF594; background-color: #FAF594;
} border-radius: 0.4rem;
box-decoration-break: clone;
img { padding: 0.1rem 0.3rem;
max-width: 100%;
height: auto;
} }
blockquote { blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem; padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
} }
hr { hr {
border: none; border: none;
border-top: 2px solid rgba(#0D0D0D, 0.1); border-top: 1px solid var(--gray-2);
margin: 2rem 0; margin: 2rem 0;
} }
} }

View File

@ -8,7 +8,7 @@ context('/src/Examples/Formatting/Vue/', () => {
}) })
const marks = [ const marks = [
{ label: 'highlight', mark: 'mark' }, { label: 'Highlight', mark: 'mark' },
] ]
marks.forEach(m => { marks.forEach(m => {
@ -20,10 +20,10 @@ context('/src/Examples/Formatting/Vue/', () => {
}) })
const alignments = [ const alignments = [
{ label: 'left', alignment: 'left' }, { label: 'Left', alignment: 'left' },
{ label: 'center', alignment: 'center' }, { label: 'Center', alignment: 'center' },
{ label: 'right', alignment: 'right' }, { label: 'Right', alignment: 'right' },
{ label: 'justify', alignment: 'justify' }, { label: 'Justify', alignment: 'justify' },
] ]
alignments.forEach(a => { alignments.forEach(a => {

View File

@ -1,43 +1,47 @@
<template> <template>
<div v-if="editor"> <div v-if="editor" class="container">
<button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"> <div class="control-group">
h1 <div class="button-group">
</button> <button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 1 }) }">
<button @click="editor.chain().focus().toggleHeading({ level: 2 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"> H1
h2 </button>
</button> <button @click="editor.chain().focus().toggleHeading({ level: 2 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 2 }) }">
<button @click="editor.chain().focus().toggleHeading({ level: 3 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"> H2
h3 </button>
</button> <button @click="editor.chain().focus().toggleHeading({ level: 3 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 3 }) }">
<button @click="editor.chain().focus().setParagraph().run()" :class="{ 'is-active': editor.isActive('paragraph') }"> H3
paragraph </button>
</button> <button @click="editor.chain().focus().setParagraph().run()" :class="{ 'is-active': editor.isActive('paragraph') }">
<button @click="editor.chain().focus().toggleBold().run()" :class="{ 'is-active': editor.isActive('bold') }"> Paragraph
bold </button>
</button> <button @click="editor.chain().focus().toggleBold().run()" :class="{ 'is-active': editor.isActive('bold') }">
<button @click="editor.chain().focus().toggleItalic().run()" :class="{ 'is-active': editor.isActive('italic') }"> Bold
italic </button>
</button> <button @click="editor.chain().focus().toggleItalic().run()" :class="{ 'is-active': editor.isActive('italic') }">
<button @click="editor.chain().focus().toggleStrike().run()" :class="{ 'is-active': editor.isActive('strike') }"> Italic
strike </button>
</button> <button @click="editor.chain().focus().toggleStrike().run()" :class="{ 'is-active': editor.isActive('strike') }">
<button @click="editor.chain().focus().toggleHighlight().run()" :class="{ 'is-active': editor.isActive('highlight') }"> Strike
highlight </button>
</button> <button @click="editor.chain().focus().toggleHighlight().run()" :class="{ 'is-active': editor.isActive('highlight') }">
<button @click="editor.chain().focus().setTextAlign('left').run()" :class="{ 'is-active': editor.isActive({ textAlign: 'left' }) }"> Highlight
left </button>
</button> <button @click="editor.chain().focus().setTextAlign('left').run()" :class="{ 'is-active': editor.isActive({ textAlign: 'left' }) }">
<button @click="editor.chain().focus().setTextAlign('center').run()" :class="{ 'is-active': editor.isActive({ textAlign: 'center' }) }"> Left
center </button>
</button> <button @click="editor.chain().focus().setTextAlign('center').run()" :class="{ 'is-active': editor.isActive({ textAlign: 'center' }) }">
<button @click="editor.chain().focus().setTextAlign('right').run()" :class="{ 'is-active': editor.isActive({ textAlign: 'right' }) }"> Center
right </button>
</button> <button @click="editor.chain().focus().setTextAlign('right').run()" :class="{ 'is-active': editor.isActive({ textAlign: 'right' }) }">
<button @click="editor.chain().focus().setTextAlign('justify').run()" :class="{ 'is-active': editor.isActive({ textAlign: 'justify' }) }"> Right
justify </button>
</button> <button @click="editor.chain().focus().setTextAlign('justify').run()" :class="{ 'is-active': editor.isActive({ textAlign: 'justify' }) }">
Justify
</button>
</div>
</div>
<editor-content :editor="editor" />
</div> </div>
<editor-content :editor="editor" />
</template> </template>
<script> <script>
@ -104,17 +108,23 @@ export default {
<style lang="scss"> <style lang="scss">
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
margin-top: 1rem; :first-child {
margin-top: 0;
> * + * {
margin-top: 0.75em;
} }
/* List styles */
ul, ul,
ol { ol {
padding: 0 1rem; padding: 0 1rem;
margin: 1.25rem 1rem 1.25rem 0.4rem;
li p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
} }
/* Heading styles */
h1, h1,
h2, h2,
h3, h3,
@ -122,45 +132,75 @@ export default {
h5, h5,
h6 { h6 {
line-height: 1.1; line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
} }
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code { code {
background-color: rgba(#616161, 0.1); background-color: var(--purple-light);
color: #616161; border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
} }
pre { pre {
background: #0D0D0D; background: var(--black);
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem; border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code { code {
color: inherit;
padding: 0;
background: none; background: none;
color: inherit;
font-size: 0.8rem; font-size: 0.8rem;
padding: 0;
} }
} }
mark { mark {
background-color: #FAF594; background-color: #FAF594;
} border-radius: 0.4rem;
box-decoration-break: clone;
img { padding: 0.1rem 0.3rem;
max-width: 100%;
height: auto;
} }
blockquote { blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem; padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
} }
hr { hr {
border: none; border: none;
border-top: 2px solid rgba(#0D0D0D, 0.1); border-top: 1px solid var(--gray-2);
margin: 2rem 0; margin: 2rem 0;
} }
} }

View File

@ -37,9 +37,13 @@ export default () => {
} }
return ( return (
<div> <>
<button onClick={addImage}>add image from URL</button> <div className="control-group">
<div className="button-group">
<button onClick={addImage}>Add image from URL</button>
</div>
</div>
<EditorContent editor={editor} /> <EditorContent editor={editor} />
</div> </>
) )
} }

View File

@ -19,7 +19,7 @@ context('/src/Examples/Images/React/', () => {
cy.stub(win, 'prompt').returns('https://placehold.co/400x400') cy.stub(win, 'prompt').returns('https://placehold.co/400x400')
cy.wait(1000) cy.wait(1000)
cy.get('button').contains('add image from URL').click({ force: false }) cy.get('button').contains('Add image from URL').click({ force: false })
cy.wait(1000) cy.wait(1000)
cy.get('.tiptap img').should('have.length', 3) cy.get('.tiptap img').should('have.length', 3)
}) })

View File

@ -1,15 +1,17 @@
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
img { img {
max-width: 100%; display: block;
height: auto; height: auto;
margin: 1.5rem 0;
max-width: 100%;
&.ProseMirror-selectednode { &.ProseMirror-selectednode {
outline: 3px solid #68CEF8; outline: 3px solid var(--purple);
} }
} }
} }

View File

@ -20,7 +20,7 @@ context('/src/Examples/Images/Vue/', () => {
cy.stub(win, 'prompt').returns('https://placehold.co/400x400') cy.stub(win, 'prompt').returns('https://placehold.co/400x400')
cy.wait(1000) cy.wait(1000)
cy.get('button').contains('add image from URL').click({ force: false }) cy.get('button').contains('Add image from URL').click({ force: false })
cy.wait(1000) cy.wait(1000)
cy.get('.tiptap img').should('have.length', 3) cy.get('.tiptap img').should('have.length', 3)
}) })

View File

@ -1,8 +1,12 @@
<template> <template>
<div v-if="editor"> <div v-if="editor" class="container">
<button @click="addImage">add image from URL</button> <div class="control-group">
<div class="button-group">
<button @click="addImage">Add image from URL</button>
</div>
</div>
<editor-content :editor="editor" />
</div> </div>
<editor-content :editor="editor" />
</template> </template>
<script> <script>
@ -60,16 +64,18 @@ export default {
<style lang="scss"> <style lang="scss">
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
img { img {
max-width: 100%; display: block;
height: auto; height: auto;
margin: 1.5rem 0;
max-width: 100%;
&.ProseMirror-selectednode { &.ProseMirror-selectednode {
outline: 3px solid #68CEF8; outline: 3px solid var(--purple);
} }
} }
} }

View File

@ -10,7 +10,7 @@ export default props => {
return ( return (
<NodeViewWrapper className="react-component"> <NodeViewWrapper className="react-component">
<span className="label">React Component</span> <label>React Component</label>
<div className="content"> <div className="content">
<button onClick={increase}> <button onClick={increase}>

View File

@ -1,33 +1,116 @@
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
}
/* List styles */
.react-component { ul,
background: #FAF594; ol {
border: 3px solid #0D0D0D; padding: 0 1rem;
border-radius: 0.5rem; margin: 1.25rem 1rem 1.25rem 0.4rem;
margin: 1rem 0;
position: relative; li p {
margin-top: 0.25em;
.label { margin-bottom: 0.25em;
margin-left: 1rem; }
background-color: #0D0D0D; }
font-size: 0.6rem;
letter-spacing: 1px; /* Heading styles */
font-weight: bold; h1,
text-transform: uppercase; h2,
color: #fff; h3,
position: absolute; h4,
top: 0; h5,
padding: 0.25rem 0.75rem; h6 {
border-radius: 0 0 0.5rem 0.5rem; line-height: 1.1;
} margin-top: 2.5rem;
text-wrap: pretty;
.content { }
margin-top: 1.5rem;
padding: 1rem; h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code {
background-color: var(--purple-light);
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}
pre {
background: var(--black);
border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
}
blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem;
}
hr {
border: none;
border-top: 1px solid var(--gray-2);
margin: 2rem 0;
}
/* React component */
.react-component {
background-color: var(--purple-light);
border: 2px solid var(--purple);
border-radius: 0.5rem;
margin: 2rem 0;
position: relative;
label {
background-color: var(--purple);
border-radius: 0 0 0.5rem 0;
color: var(--white);
font-size: 0.75rem;
font-weight: bold;
padding: 0.25rem 0.5rem;
position: absolute;
top: 0;
}
.content {
margin-top: 1.5rem;
padding: 1rem;
}
} }
} }

View File

@ -1,6 +1,6 @@
<template> <template>
<node-view-wrapper class="vue-component"> <node-view-wrapper class="vue-component">
<span class="label">Vue Component</span> <label>Vue Component</label>
<div class="content"> <div class="content">
<button @click="increase">This button has been clicked {{ node.attrs.count }} times.</button> <button @click="increase">This button has been clicked {{ node.attrs.count }} times.</button>
@ -29,30 +29,30 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
.vue-component { .tiptap {
background: #FAF594; /* Vue component */
border: 3px solid #0D0D0D; .vue-component {
border-radius: 0.5rem; background-color: var(--purple-light);
margin: 1rem 0; border: 2px solid var(--purple);
position: relative; border-radius: 0.5rem;
} margin: 2rem 0;
position: relative;
.label { label {
margin-left: 1rem; background-color: var(--purple);
background-color: #0D0D0D; border-radius: 0 0 0.5rem 0;
font-size: 0.6rem; color: var(--white);
letter-spacing: 1px; font-size: 0.75rem;
font-weight: bold; font-weight: bold;
text-transform: uppercase; padding: 0.25rem 0.5rem;
color: #fff; position: absolute;
position: absolute; top: 0;
top: 0; }
padding: 0.25rem 0.75rem;
border-radius: 0 0 0.5rem 0.5rem;
}
.content { .content {
margin-top: 1.5rem; margin-top: 1.5rem;
padding: 1rem; padding: 1rem;
}
}
} }
</style> </style>

View File

@ -46,8 +46,93 @@ export default {
<style lang="scss"> <style lang="scss">
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
}
/* List styles */
ul,
ol {
padding: 0 1rem;
margin: 1.25rem 1rem 1.25rem 0.4rem;
li p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
}
/* Heading styles */
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
}
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code {
background-color: var(--purple-light);
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}
pre {
background: var(--black);
border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
}
blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem;
}
hr {
border: none;
border-top: 1px solid var(--gray-2);
margin: 2rem 0;
} }
} }
</style> </style>

View File

@ -3,10 +3,10 @@ import React from 'react'
export default () => { export default () => {
return ( return (
<NodeViewWrapper className="react-component-with-content"> <NodeViewWrapper className="react-component">
<span className="label" contentEditable={false}>React Component</span> <label contentEditable={false}>React Component</label>
<NodeViewContent className="content" /> <NodeViewContent className="content is-editable" />
</NodeViewWrapper> </NodeViewWrapper>
) )
} }

View File

@ -11,12 +11,12 @@ context('/src/Examples/InteractivityComponentContent/React/', () => {
}) })
it('should render a custom node', () => { it('should render a custom node', () => {
cy.get('.ProseMirror .react-component-with-content') cy.get('.ProseMirror .react-component')
.should('have.length', 1) .should('have.length', 1)
}) })
it('should allow text editing inside component', () => { it('should allow text editing inside component', () => {
cy.get('.ProseMirror .react-component-with-content .content div') cy.get('.ProseMirror .react-component .content div')
.invoke('attr', 'contentEditable', true) .invoke('attr', 'contentEditable', true)
.invoke('text', '') .invoke('text', '')
.type('Hello World!') .type('Hello World!')
@ -24,24 +24,24 @@ context('/src/Examples/InteractivityComponentContent/React/', () => {
}) })
it('should allow text editing inside component with markdown text', () => { it('should allow text editing inside component with markdown text', () => {
cy.get('.ProseMirror .react-component-with-content .content div') cy.get('.ProseMirror .react-component .content div')
.invoke('attr', 'contentEditable', true) .invoke('attr', 'contentEditable', true)
.invoke('text', '') .invoke('text', '')
.type('Hello World! This is **bold**.') .type('Hello World! This is **bold**.')
.should('have.text', 'Hello World! This is bold.') .should('have.text', 'Hello World! This is bold.')
cy.get('.ProseMirror .react-component-with-content .content strong') cy.get('.ProseMirror .react-component .content strong')
.should('exist') .should('exist')
}) })
it('should remove node via selectall', () => { it('should remove node via selectall', () => {
cy.get('.ProseMirror .react-component-with-content') cy.get('.ProseMirror .react-component')
.should('have.length', 1) .should('have.length', 1)
cy.get('.ProseMirror') cy.get('.ProseMirror')
.type('{selectall}{backspace}') .type('{selectall}{backspace}')
cy.get('.ProseMirror .react-component-with-content') cy.get('.ProseMirror .react-component')
.should('have.length', 0) .should('have.length', 0)
}) })
}) })

View File

@ -1,36 +1,123 @@
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
}
}
.react-component-with-content {
background: #FAF594;
border: 3px solid #0D0D0D;
border-radius: 0.5rem;
margin: 1rem 0;
position: relative;
.label {
margin-left: 1rem;
background-color: #0D0D0D;
font-size: 0.6rem;
letter-spacing: 1px;
font-weight: bold;
text-transform: uppercase;
color: #fff;
position: absolute;
top: 0;
padding: 0.25rem 0.75rem;
border-radius: 0 0 0.5rem 0.5rem;
} }
.content { /* List styles */
margin: 2.5rem 1rem 1rem; ul,
padding: 0.5rem; ol {
border: 2px dashed #0D0D0D20; padding: 0 1rem;
margin: 1.25rem 1rem 1.25rem 0.4rem;
li p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
}
/* Heading styles */
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
}
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code {
background-color: var(--purple-light);
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}
pre {
background: var(--black);
border-radius: 0.5rem; border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
}
blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem;
}
hr {
border: none;
border-top: 1px solid var(--gray-2);
margin: 2rem 0;
}
/* React component */
.react-component {
background-color: var(--purple-light);
border: 2px solid var(--purple);
border-radius: 0.5rem;
margin: 2rem 0;
position: relative;
label {
background-color: var(--purple);
border-radius: 0 0 0.5rem 0;
color: var(--white);
font-size: 0.75rem;
font-weight: bold;
padding: 0.25rem 0.5rem;
position: absolute;
top: 0;
}
.content {
margin-top: 1.5rem;
padding: 1rem;
&.is-editable {
border: 2px dashed var(--gray-3);
border-radius: 0.5rem;
margin: 2.5rem 1rem 1rem;
padding: 0.5rem;
}
}
} }
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<node-view-wrapper class="vue-component"> <node-view-wrapper class="vue-component">
<span class="label" contenteditable="false">Vue Component</span> <label contenteditable="false">Vue Component</label>
<node-view-content class="content" /> <node-view-content class="content is-editable" />
</node-view-wrapper> </node-view-wrapper>
</template> </template>
@ -19,32 +19,37 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
.vue-component { .tiptap {
background: #FAF594; /* Vue component */
border: 3px solid #0D0D0D; .vue-component {
border-radius: 0.5rem; background-color: var(--purple-light);
margin: 1rem 0; border: 2px solid var(--purple);
position: relative; border-radius: 0.5rem;
} margin: 2rem 0;
position: relative;
.label { label {
margin-left: 1rem; background-color: var(--purple);
background-color: #0D0D0D; border-radius: 0 0 0.5rem 0;
font-size: 0.6rem; color: var(--white);
letter-spacing: 1px; font-size: 0.75rem;
font-weight: bold; font-weight: bold;
text-transform: uppercase; padding: 0.25rem 0.5rem;
color: #fff; position: absolute;
position: absolute; top: 0;
top: 0; }
padding: 0.25rem 0.75rem;
border-radius: 0 0 0.5rem 0.5rem;
}
.content { .content {
margin: 2.5rem 1rem 1rem; margin-top: 1.5rem;
padding: 0.5rem; padding: 1rem;
border: 2px dashed #0D0D0D20;
border-radius: 0.5rem; &.is-editable {
border: 2px dashed var(--gray-3);
border-radius: 0.5rem;
margin: 2.5rem 1rem 1rem;
padding: 0.5rem;
}
}
}
} }
</style> </style>

View File

@ -48,8 +48,93 @@ export default {
<style lang="scss"> <style lang="scss">
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
}
/* List styles */
ul,
ol {
padding: 0 1rem;
margin: 1.25rem 1rem 1.25rem 0.4rem;
li p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
}
/* Heading styles */
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
}
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code {
background-color: var(--purple-light);
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}
pre {
background: var(--black);
border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
}
blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem;
}
hr {
border: none;
border-top: 1px solid var(--gray-2);
margin: 2rem 0;
} }
} }
</style> </style>

View File

@ -21,7 +21,7 @@ export default () => {
To test that, start a new line and type <code>#</code> followed by a space to get a heading. Try <code>#</code>, <code>##</code>, <code>###</code>, <code>####</code>, <code>#####</code>, <code>######</code> for different levels. To test that, start a new line and type <code>#</code> followed by a space to get a heading. Try <code>#</code>, <code>##</code>, <code>###</code>, <code>####</code>, <code>#####</code>, <code>######</code> for different levels.
</p> </p>
<p> <p>
Those conventions are called input rules in tiptap. Some of them are enabled by default. Try <code>></code> for blockquotes, <code>*</code>, <code>-</code> or <code>+</code> for bullet lists, or <code>\`foobar\`</code> to highlight code, <code>~~tildes~~</code> to strike text, or <code>==equal signs==</code> to highlight text. Those conventions are called input rules in Tiptap. Some of them are enabled by default. Try <code>></code> for blockquotes, <code>*</code>, <code>-</code> or <code>+</code> for bullet lists, or <code>\`foobar\`</code> to highlight code, <code>~~tildes~~</code> to strike text, or <code>==equal signs==</code> to highlight text.
</p> </p>
<p> <p>
You can overwrite existing input rules or add your own to nodes, marks and extensions. You can overwrite existing input rules or add your own to nodes, marks and extensions.

View File

@ -1,53 +1,98 @@
/* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
ul, /* List styles */
ul,
ol { ol {
padding: 0 1rem; padding: 0 1rem;
} margin: 1.25rem 1rem 1.25rem 0.4rem;
h1, li p {
h2, margin-top: 0.25em;
h3, margin-bottom: 0.25em;
h4,
h5,
h6 {
line-height: 1.1;
}
code {
background-color: rgba(#616161, 0.1);
color: #616161;
}
pre {
background: #0D0D0D;
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
} }
} }
img { /* Heading styles */
max-width: 100%; h1,
height: auto; h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
} }
hr { h1,
margin: 1rem 0; h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code {
background-color: var(--purple-light);
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}
pre {
background: var(--black);
border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
}
mark {
background-color: #FAF594;
border-radius: 0.4rem;
box-decoration-break: clone;
padding: 0.1rem 0.3rem;
} }
blockquote { blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem; padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1); }
hr {
border: none;
border-top: 1px solid var(--gray-2);
margin: 2rem 0;
} }
} }

View File

@ -34,7 +34,7 @@ export default {
To test that, start a new line and type <code>#</code> followed by a space to get a heading. Try <code>#</code>, <code>##</code>, <code>###</code>, <code>####</code>, <code>#####</code>, <code>######</code> for different levels. To test that, start a new line and type <code>#</code> followed by a space to get a heading. Try <code>#</code>, <code>##</code>, <code>###</code>, <code>####</code>, <code>#####</code>, <code>######</code> for different levels.
</p> </p>
<p> <p>
Those conventions are called input rules in tiptap. Some of them are enabled by default. Try <code>></code> for blockquotes, <code>*</code>, <code>-</code> or <code>+</code> for bullet lists, or <code>\`foobar\`</code> to highlight code, <code>~~tildes~~</code> to strike text, or <code>==equal signs==</code> to highlight text. Those conventions are called input rules in Tiptap. Some of them are enabled by default. Try <code>></code> for blockquotes, <code>*</code>, <code>-</code> or <code>+</code> for bullet lists, or <code>\`foobar\`</code> to highlight code, <code>~~tildes~~</code> to strike text, or <code>==equal signs==</code> to highlight text.
</p> </p>
<p> <p>
You can overwrite existing input rules or add your own to nodes, marks and extensions. You can overwrite existing input rules or add your own to nodes, marks and extensions.
@ -55,15 +55,23 @@ export default {
<style lang="scss"> <style lang="scss">
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
/* List styles */
ul, ul,
ol { ol {
padding: 0 1rem; padding: 0 1rem;
margin: 1.25rem 1rem 1.25rem 0.4rem;
li p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
} }
/* Heading styles */
h1, h1,
h2, h2,
h3, h3,
@ -71,40 +79,76 @@ export default {
h5, h5,
h6 { h6 {
line-height: 1.1; line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
} }
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code { code {
background-color: rgba(#616161, 0.1); background-color: var(--purple-light);
color: #616161; border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
} }
pre { pre {
background: #0D0D0D; background: var(--black);
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem; border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code { code {
color: inherit;
padding: 0;
background: none; background: none;
color: inherit;
font-size: 0.8rem; font-size: 0.8rem;
padding: 0;
} }
} }
img { mark {
max-width: 100%; background-color: #FAF594;
height: auto; border-radius: 0.4rem;
} box-decoration-break: clone;
padding: 0.1rem 0.3rem;
hr {
margin: 1rem 0;
} }
blockquote { blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem; padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1); }
hr {
border: none;
border-top: 1px solid var(--gray-2);
margin: 2rem 0;
} }
} }
</style> </style>

View File

@ -64,7 +64,7 @@ export default () => {
onClick={() => editor.chain().focus().toggleBulletList().run()} onClick={() => editor.chain().focus().toggleBulletList().run()}
className={editor.isActive('bulletList') ? 'is-active' : ''} className={editor.isActive('bulletList') ? 'is-active' : ''}
> >
Bullet List Bullet list
</button> </button>
</FloatingMenu>} </FloatingMenu>}

View File

@ -1,53 +1,144 @@
/* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
ul, /* List styles */
ul,
ol { ol {
padding: 0 1rem; padding: 0 1rem;
margin: 1.25rem 1rem 1.25rem 0.4rem;
li p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
}
/* Heading styles */
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
}
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code {
background-color: var(--purple-light);
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}
pre {
background: var(--black);
border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
}
blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem;
}
hr {
border: none;
border-top: 1px solid var(--gray-2);
margin: 2rem 0;
} }
} }
/* Bubble menu */
.bubble-menu { .bubble-menu {
background-color: var(--white);
border: 1px solid var(--gray-1);
border-radius: 0.7rem;
box-shadow: var(--shadow);
display: flex; display: flex;
background-color: #0D0D0D;
padding: 0.2rem; padding: 0.2rem;
border-radius: 0.5rem;
button { button {
border: none; background-color: unset;
background: none;
color: #FFF; &:hover {
font-size: 0.85rem; background-color: var(--gray-3);
font-weight: 500; }
padding: 0 0.2rem;
opacity: 0.6;
&:hover,
&.is-active { &.is-active {
opacity: 1; background-color: var(--purple);
&:hover {
background-color: var(--purple-contrast);
}
} }
} }
} }
/* Floating menu */
.floating-menu { .floating-menu {
display: flex; display: flex;
background-color: #0D0D0D10; background-color: var(--gray-3);
padding: 0.2rem; padding: 0.1rem;
border-radius: 0.5rem; border-radius: 0.5rem;
button { button {
border: none; background-color: unset;
background: none; padding: 0.275rem 0.425rem;
font-size: 0.85rem; border-radius: 0.3rem;
font-weight: 500;
padding: 0 0.2rem; &:hover {
opacity: 0.6; background-color: var(--gray-3);
}
&:hover,
&.is-active { &.is-active {
opacity: 1; background-color: var(--white);
color: var(--purple);
&:hover {
color: var(--purple-contrast);
}
} }
} }
} }

View File

@ -28,7 +28,7 @@
H2 H2
</button> </button>
<button @click="editor.chain().focus().toggleBulletList().run()" :class="{ 'is-active': editor.isActive('bulletList') }"> <button @click="editor.chain().focus().toggleBulletList().run()" :class="{ 'is-active': editor.isActive('bulletList') }">
Bullet List Bullet list
</button> </button>
</floating-menu> </floating-menu>
</div> </div>
@ -83,60 +83,145 @@ export default {
<style lang="scss"> <style lang="scss">
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
/* List styles */
ul, ul,
ol { ol {
padding: 0 1rem; padding: 0 1rem;
margin: 1.25rem 1rem 1.25rem 0.4rem;
li p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
}
/* Heading styles */
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
}
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code {
background-color: var(--purple-light);
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}
pre {
background: var(--black);
border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
} }
blockquote { blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem; padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1); }
hr {
border: none;
border-top: 1px solid var(--gray-2);
margin: 2rem 0;
} }
} }
/* Bubble menu */
.bubble-menu { .bubble-menu {
background-color: var(--white);
border: 1px solid var(--gray-1);
border-radius: 0.7rem;
box-shadow: var(--shadow);
display: flex; display: flex;
background-color: #0D0D0D;
padding: 0.2rem; padding: 0.2rem;
border-radius: 0.5rem;
button { button {
border: none; background-color: unset;
background: none;
color: #FFF; &:hover {
font-size: 0.85rem; background-color: var(--gray-3);
font-weight: 500; }
padding: 0 0.2rem;
opacity: 0.6;
&:hover,
&.is-active { &.is-active {
opacity: 1; background-color: var(--purple);
&:hover {
background-color: var(--purple-contrast);
}
} }
} }
} }
/* Floating menu */
.floating-menu { .floating-menu {
display: flex; display: flex;
background-color: #0D0D0D10; background-color: var(--gray-3);
padding: 0.2rem; padding: 0.1rem;
border-radius: 0.5rem; border-radius: 0.5rem;
button { button {
border: none; background-color: unset;
background: none; padding: 0.275rem 0.425rem;
font-size: 0.85rem; border-radius: 0.3rem;
font-weight: 500;
padding: 0 0.2rem; &:hover {
opacity: 0.6; background-color: var(--gray-3);
}
&:hover,
&.is-active { &.is-active {
opacity: 1; background-color: var(--white);
color: var(--purple);
&:hover {
color: var(--purple-contrast);
}
} }
} }
} }

View File

@ -15,7 +15,7 @@ export default () => {
], ],
content: ` content: `
<p> <p>
This is a radically reduced version of tiptap. It has support for a document, with paragraphs and text. Thats it. Its probably too much for real minimalists though. This is a radically reduced version of Tiptap. It has support for a document, with paragraphs and text. Thats it. Its probably too much for real minimalists though.
</p> </p>
<p> <p>
The paragraph extension is not really required, but you need at least one node. Sure, that node can be something different. The paragraph extension is not really required, but you need at least one node. Sure, that node can be something different.

View File

@ -1,6 +1,6 @@
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
} }

View File

@ -28,7 +28,7 @@ export default {
], ],
content: ` content: `
<p> <p>
This is a radically reduced version of tiptap. It has support for a document, with paragraphs and text. Thats it. Its probably too much for real minimalists though. This is a radically reduced version of Tiptap. It has support for a document, with paragraphs and text. Thats it. Its probably too much for real minimalists though.
</p> </p>
<p> <p>
The paragraph extension is not really required, but you need at least one node. Sure, that node can be something different. The paragraph extension is not really required, but you need at least one node. Sure, that node can be something different.
@ -46,8 +46,8 @@ export default {
<style lang="scss"> <style lang="scss">
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
} }
</style> </style>

View File

@ -18,7 +18,7 @@ export default () => {
<p> <p>
This is a <strong>simple</strong> paragraph. This is a <strong>simple</strong> paragraph.
</p> </p>
<img src="https://unsplash.it/200/200" alt="A 200x200 square thumbnail from unsplash." /> <img src="https://placehold.co/200x200" alt="A 200x200 square thumbnail from placehold.co." />
<p> <p>
Here is another paragraph inside this document. Here is another paragraph inside this document.
</p> </p>
@ -72,11 +72,11 @@ export default () => {
<blockquote> <blockquote>
<p>Here we have another paragraph inside a blockquote.</p> <p>Here we have another paragraph inside a blockquote.</p>
<blockquote> <blockquote>
<img src="https://unsplash.it/260/200" alt="A 260x200 landscape thumbnail from unsplash." /> <img src="https://placehold.co/260x200" alt="A 260x200 landscape thumbnail from placehold.co." />
<img src="https://unsplash.it/100/200" alt="A 100x200 portrait thumbnail from unsplash." /> <img src="https://placehold.co/100x200" alt="A 100x200 portrait thumbnail from placehold.co." />
</blockquote> </blockquote>
</blockquote> </blockquote>
<img src="https://unsplash.it/260/200" alt="A 260x200 landscape thumbnail from unsplash." /> <img src="https://placehold.co/260x200" alt="A 260x200 landscape thumbnail from placehold.co." />
`, `,
}) })
@ -160,7 +160,7 @@ export default () => {
}, [editor]) }, [editor])
const findSquaredImage = useCallback(() => { const findSquaredImage = useCallback(() => {
const nodePosition = editor.$doc.querySelector('image', { src: 'https://unsplash.it/200/200' }) const nodePosition = editor.$doc.querySelector('image', { src: 'https://placehold.co/200x200' })
if (!nodePosition) { if (!nodePosition) {
setFoundNodes(null) setFoundNodes(null)
@ -171,7 +171,7 @@ export default () => {
}, [editor]) }, [editor])
const findLandscapeImage = useCallback(() => { const findLandscapeImage = useCallback(() => {
const nodePosition = editor.$doc.querySelector('image', { src: 'https://unsplash.it/260/200' }) const nodePosition = editor.$doc.querySelector('image', { src: 'https://placehold.co/260x200' })
if (!nodePosition) { if (!nodePosition) {
setFoundNodes(null) setFoundNodes(null)
@ -182,7 +182,7 @@ export default () => {
}, [editor]) }, [editor])
const findAllLandscapeImages = useCallback(() => { const findAllLandscapeImages = useCallback(() => {
const nodePosition = editor.$doc.querySelectorAll('image', { src: 'https://unsplash.it/260/200' }) const nodePosition = editor.$doc.querySelectorAll('image', { src: 'https://placehold.co/260x200' })
if (!nodePosition) { if (!nodePosition) {
setFoundNodes(null) setFoundNodes(null)
@ -193,7 +193,7 @@ export default () => {
}, [editor]) }, [editor])
const findFirstLandscapeImageWithAllQuery = useCallback(() => { const findFirstLandscapeImageWithAllQuery = useCallback(() => {
const nodePosition = editor.$doc.querySelectorAll('image', { src: 'https://unsplash.it/260/200' }, true) const nodePosition = editor.$doc.querySelectorAll('image', { src: 'https://placehold.co/260x200' }, true)
if (!nodePosition) { if (!nodePosition) {
setFoundNodes(null) setFoundNodes(null)
@ -204,7 +204,7 @@ export default () => {
}, [editor]) }, [editor])
const findPortraitImageInBlockquote = useCallback(() => { const findPortraitImageInBlockquote = useCallback(() => {
const nodePosition = editor.$doc.querySelector('image', { src: 'https://unsplash.it/100/200' }) const nodePosition = editor.$doc.querySelector('image', { src: 'https://placehold.co/100x200' })
if (!nodePosition) { if (!nodePosition) {
setFoundNodes(null) setFoundNodes(null)
@ -259,33 +259,35 @@ export default () => {
}, [editor]) }, [editor])
return ( return (
<div> <>
<div> <div className="control-group">
<button data-testid="find-paragraphs" onClick={findParagraphs}>Find paragraphs</button> <div className="button-group">
<button data-testid="find-listitems" onClick={findListItems}>Find list items</button> <button data-testid="find-paragraphs" onClick={findParagraphs}>Find paragraphs</button>
<button data-testid="find-bulletlists" onClick={findBulletList}>Find bullet lists</button> <button data-testid="find-listitems" onClick={findListItems}>Find list items</button>
<button data-testid="find-orderedlists" onClick={findOrderedList}>Find ordered lists</button> <button data-testid="find-bulletlists" onClick={findBulletList}>Find bullet lists</button>
<button data-testid="find-blockquotes" onClick={findBlockquote}>Find blockquotes</button> <button data-testid="find-orderedlists" onClick={findOrderedList}>Find ordered lists</button>
<button data-testid="find-images" onClick={findImages}>Find images</button> <button data-testid="find-blockquotes" onClick={findBlockquote}>Find blockquotes</button>
</div> <button data-testid="find-images" onClick={findImages}>Find images</button>
<div> </div>
<button data-testid="find-first-blockquote" onClick={findFirstBlockquote}>Find first blockquote</button> <div className="button-group">
<button data-testid="find-squared-image" onClick={findSquaredImage}>Find squared image</button> <button data-testid="find-first-blockquote" onClick={findFirstBlockquote}>Find first blockquote</button>
<button data-testid="find-landscape-image" onClick={findLandscapeImage}>Find landscape image</button> <button data-testid="find-squared-image" onClick={findSquaredImage}>Find squared image</button>
<button data-testid="find-all-landscape-images" onClick={findAllLandscapeImages}>Find all landscape images</button> <button data-testid="find-landscape-image" onClick={findLandscapeImage}>Find landscape image</button>
<button data-testid="find-first-landscape-image-with-all-query" onClick={findFirstLandscapeImageWithAllQuery}>Find first landscape image with all query</button> <button data-testid="find-all-landscape-images" onClick={findAllLandscapeImages}>Find all landscape images</button>
<button data-testid="find-portrait-image-inside-blockquote" onClick={findPortraitImageInBlockquote}>Find portrait image in blockquote</button> <button data-testid="find-first-landscape-image-with-all-query" onClick={findFirstLandscapeImageWithAllQuery}>Find first landscape image with all query</button>
</div> <button data-testid="find-portrait-image-inside-blockquote" onClick={findPortraitImageInBlockquote}>Find portrait image in blockquote</button>
<div> </div>
<button data-testid="find-first-node" onClick={findFirstNode}>Find first node</button> <div className="button-group">
<button data-testid="find-last-node" onClick={findLastNode}>Find last node</button> <button data-testid="find-first-node" onClick={findFirstNode}>Find first node</button>
<button data-testid="find-last-node-of-first-bullet-list" onClick={findLastNodeOfFirstBulletList}>Find last node of first bullet list</button> <button data-testid="find-last-node" onClick={findLastNode}>Find last node</button>
<button data-testid="find-nonexistent-node" onClick={findNonexistentNode}>Find nonexistent node</button> <button data-testid="find-last-node-of-first-bullet-list" onClick={findLastNodeOfFirstBulletList}>Find last node of first bullet list</button>
<button data-testid="find-nonexistent-node" onClick={findNonexistentNode}>Find nonexistent node</button>
</div>
</div> </div>
<EditorContent editor={editor} /> <EditorContent editor={editor} />
{foundNodes ? <div data-testid="found-nodes">{foundNodes.map(n => ( {foundNodes ? <div className="output-group" data-testid="found-nodes">{foundNodes.map(n => (
<div data-testid="found-node" key={n.pos}>{mapNodePosToString(n)}</div> <div data-testid="found-node" key={n.pos}>{mapNodePosToString(n)}</div>
))}</div> : ''} ))}</div> : ''}
</div> </>
) )
} }

View File

@ -66,7 +66,7 @@ context('/src/Examples/NodePos/React/', () => {
cy.get('button[data-testid="find-squared-image"]').click() cy.get('button[data-testid="find-squared-image"]').click()
cy.get('div[data-testid="found-nodes"]').should('exist') cy.get('div[data-testid="found-nodes"]').should('exist')
cy.get('div[data-testid="found-node"]').should('have.length', 1) cy.get('div[data-testid="found-node"]').should('have.length', 1)
cy.get('div[data-testid="found-node"]').should('contain', 'https://unsplash.it/200/200') cy.get('div[data-testid="found-node"]').should('contain', 'https://placehold.co/200x200')
}) })
}) })
@ -75,7 +75,7 @@ context('/src/Examples/NodePos/React/', () => {
cy.get('button[data-testid="find-landscape-image"]').click() cy.get('button[data-testid="find-landscape-image"]').click()
cy.get('div[data-testid="found-nodes"]').should('exist') cy.get('div[data-testid="found-nodes"]').should('exist')
cy.get('div[data-testid="found-node"]').should('have.length', 1) cy.get('div[data-testid="found-node"]').should('have.length', 1)
cy.get('div[data-testid="found-node"]').should('contain', 'https://unsplash.it/260/200') cy.get('div[data-testid="found-node"]').should('contain', 'https://placehold.co/260x200')
}) })
}) })
@ -84,8 +84,8 @@ context('/src/Examples/NodePos/React/', () => {
cy.get('button[data-testid="find-all-landscape-images"]').click() cy.get('button[data-testid="find-all-landscape-images"]').click()
cy.get('div[data-testid="found-nodes"]').should('exist') cy.get('div[data-testid="found-nodes"]').should('exist')
cy.get('div[data-testid="found-node"]').should('have.length', 2) cy.get('div[data-testid="found-node"]').should('have.length', 2)
cy.get('div[data-testid="found-node"]').eq(0).should('contain', 'https://unsplash.it/260/200') cy.get('div[data-testid="found-node"]').eq(0).should('contain', 'https://placehold.co/260x200')
cy.get('div[data-testid="found-node"]').eq(1).should('contain', 'https://unsplash.it/260/200') cy.get('div[data-testid="found-node"]').eq(1).should('contain', 'https://placehold.co/260x200')
}) })
}) })
@ -94,7 +94,7 @@ context('/src/Examples/NodePos/React/', () => {
cy.get('button[data-testid="find-first-landscape-image-with-all-query"]').click() cy.get('button[data-testid="find-first-landscape-image-with-all-query"]').click()
cy.get('div[data-testid="found-nodes"]').should('exist') cy.get('div[data-testid="found-nodes"]').should('exist')
cy.get('div[data-testid="found-node"]').should('have.length', 1) cy.get('div[data-testid="found-node"]').should('have.length', 1)
cy.get('div[data-testid="found-node"]').should('contain', 'https://unsplash.it/260/200') cy.get('div[data-testid="found-node"]').should('contain', 'https://placehold.co/260x200')
}) })
}) })
@ -103,7 +103,7 @@ context('/src/Examples/NodePos/React/', () => {
cy.get('button[data-testid="find-portrait-image-inside-blockquote"]').click() cy.get('button[data-testid="find-portrait-image-inside-blockquote"]').click()
cy.get('div[data-testid="found-nodes"]').should('exist') cy.get('div[data-testid="found-nodes"]').should('exist')
cy.get('div[data-testid="found-node"]').should('have.length', 1) cy.get('div[data-testid="found-node"]').should('have.length', 1)
cy.get('div[data-testid="found-node"]').should('contain', 'https://unsplash.it/100/200') cy.get('div[data-testid="found-node"]').should('contain', 'https://placehold.co/100x200')
}) })
}) })
}) })

View File

@ -1,15 +1,103 @@
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
h1, /* List styles */
h2, ul,
h3, ol {
h4, padding: 0 1rem;
h5, margin: 1.25rem 1rem 1.25rem 0.4rem;
li p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
}
/* Heading styles */
h1,
h2,
h3,
h4,
h5,
h6 { h6 {
line-height: 1.1; line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
}
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code {
background-color: var(--purple-light);
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}
pre {
background: var(--black);
border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
}
blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem;
}
hr {
border: none;
border-top: 1px solid var(--gray-2);
margin: 2rem 0;
}
/* Image styles */
img {
display: block;
height: auto;
margin: 1.5rem 0;
max-width: 100%;
&.ProseMirror-selectednode {
outline: 3px solid var(--purple);
}
} }
} }

View File

@ -24,7 +24,7 @@ export default () => {
], ],
content: ` content: `
<p> <p>
With the Typography extension, tiptap understands »what you mean« and adds correct characters to your text its like a typography nerd on your side. With the Typography extension, Tiptap understands »what you mean« and adds correct characters to your text its like a typography nerd on your side.
</p> </p>
<p> <p>
Try it out and type <code>(c)</code>, <code>-></code>, <code>>></code>, <code>1/2</code>, <code>!=</code>, <code>--</code> or <code>1x1</code> here: Try it out and type <code>(c)</code>, <code>-></code>, <code>>></code>, <code>1/2</code>, <code>!=</code>, <code>--</code> or <code>1x1</code> here:

View File

@ -1,38 +1,33 @@
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
} }
/* Code and preformatted text styles */
code { code {
background-color: rgba(#616161, 0.1); background-color: var(--purple-light);
color: #616161; border-radius: 0.4rem;
} color: var(--black);
} font-size: 0.85rem;
padding: 0.25em 0.3em;
/* Color swatches */ }
.color {
white-space: nowrap; /* Color swatches */
.color {
&::before { white-space: nowrap;
background-color: var(--color);
border: 1px solid rgba(128, 128, 128, 0.3); &::before {
border-radius: 2px; background-color: var(--color);
content: " "; border: 1px solid rgba(128, 128, 128, 0.3);
display: inline-block; border-radius: 2px;
height: 1em; content: " ";
margin-bottom: 0.15em; display: inline-block;
margin-right: 0.1em; height: 1em;
vertical-align: middle; margin-bottom: 0.15em;
width: 1em; margin-right: 0.1em;
vertical-align: middle;
width: 1em;
}
} }
} }

View File

@ -37,7 +37,7 @@ export default {
], ],
content: ` content: `
<p> <p>
With the Typography extension, tiptap understands »what you mean« and adds correct characters to your text its like a typography nerd on your side. With the Typography extension, Tiptap understands »what you mean« and adds correct characters to your text its like a typography nerd on your side.
</p> </p>
<p> <p>
Try it out and type <code>(c)</code>, <code>-></code>, <code>>></code>, <code>1/2</code>, <code>!=</code>, <code>--</code> or <code>1x1</code> here: Try it out and type <code>(c)</code>, <code>-></code>, <code>>></code>, <code>1/2</code>, <code>!=</code>, <code>--</code> or <code>1x1</code> here:
@ -64,40 +64,35 @@ export default {
<style lang="scss"> <style lang="scss">
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
} }
/* Code and preformatted text styles */
code { code {
background-color: rgba(#616161, 0.1); background-color: var(--purple-light);
color: #616161; border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
} }
}
/* Color swatches */ /* Color swatches */
.color { .color {
white-space: nowrap; white-space: nowrap;
&::before { &::before {
content: ' '; background-color: var(--color);
display: inline-block; border: 1px solid rgba(128, 128, 128, 0.3);
width: 1em; border-radius: 2px;
height: 1em; content: " ";
border: 1px solid rgba(128, 128, 128, 0.3); display: inline-block;
vertical-align: middle; height: 1em;
margin-right: 0.1em; margin-bottom: 0.15em;
margin-bottom: 0.15em; margin-right: 0.1em;
border-radius: 2px; vertical-align: middle;
background-color: var(--color); width: 1em;
}
} }
} }
</style> </style>

View File

@ -60,69 +60,71 @@ const MenuBar = ({ editor }) => {
} }
return ( return (
<> <div className="control-group">
<button onClick={() => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()}> <div className="button-group">
insertTable <button onClick={() => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()}>
</button> Insert table
<button onClick={() => editor.chain().focus().insertContent(tableHTML, { </button>
parseOptions: { <button onClick={() => editor.chain().focus().insertContent(tableHTML, {
preserveWhitespace: false, parseOptions: {
}, preserveWhitespace: false,
}).run()}> },
insertHTMLTable }).run()}>
</button> Insert HTML table
<button onClick={() => editor.chain().focus().addColumnBefore().run()} disabled={!editor.can().addColumnBefore()}> </button>
addColumnBefore <button onClick={() => editor.chain().focus().addColumnBefore().run()} disabled={!editor.can().addColumnBefore()}>
</button> Add column before
<button onClick={() => editor.chain().focus().addColumnAfter().run()} disabled={!editor.can().addColumnAfter()}> </button>
addColumnAfter <button onClick={() => editor.chain().focus().addColumnAfter().run()} disabled={!editor.can().addColumnAfter()}>
</button> Add column after
<button onClick={() => editor.chain().focus().deleteColumn().run()} disabled={!editor.can().deleteColumn()}> </button>
deleteColumn <button onClick={() => editor.chain().focus().deleteColumn().run()} disabled={!editor.can().deleteColumn()}>
</button> Delete column
<button onClick={() => editor.chain().focus().addRowBefore().run()} disabled={!editor.can().addRowBefore()}> </button>
addRowBefore <button onClick={() => editor.chain().focus().addRowBefore().run()} disabled={!editor.can().addRowBefore()}>
</button> Add row before
<button onClick={() => editor.chain().focus().addRowAfter().run()} disabled={!editor.can().addRowAfter()}> </button>
addRowAfter <button onClick={() => editor.chain().focus().addRowAfter().run()} disabled={!editor.can().addRowAfter()}>
</button> Add row after
<button onClick={() => editor.chain().focus().deleteRow().run()} disabled={!editor.can().deleteRow()}> </button>
deleteRow <button onClick={() => editor.chain().focus().deleteRow().run()} disabled={!editor.can().deleteRow()}>
</button> Delete row
<button onClick={() => editor.chain().focus().deleteTable().run()} disabled={!editor.can().deleteTable()}> </button>
deleteTable <button onClick={() => editor.chain().focus().deleteTable().run()} disabled={!editor.can().deleteTable()}>
</button> Delete table
<button onClick={() => editor.chain().focus().mergeCells().run()} disabled={!editor.can().mergeCells()}> </button>
mergeCells <button onClick={() => editor.chain().focus().mergeCells().run()} disabled={!editor.can().mergeCells()}>
</button> Merge cells
<button onClick={() => editor.chain().focus().splitCell().run()} disabled={!editor.can().splitCell()}> </button>
splitCell <button onClick={() => editor.chain().focus().splitCell().run()} disabled={!editor.can().splitCell()}>
</button> Split cell
<button onClick={() => editor.chain().focus().toggleHeaderColumn().run()} disabled={!editor.can().toggleHeaderColumn()}> </button>
toggleHeaderColumn <button onClick={() => editor.chain().focus().toggleHeaderColumn().run()} disabled={!editor.can().toggleHeaderColumn()}>
</button> ToggleHeaderColumn
<button onClick={() => editor.chain().focus().toggleHeaderRow().run()} disabled={!editor.can().toggleHeaderRow()}> </button>
toggleHeaderRow <button onClick={() => editor.chain().focus().toggleHeaderRow().run()} disabled={!editor.can().toggleHeaderRow()}>
</button> Toggle header row
<button onClick={() => editor.chain().focus().toggleHeaderCell().run()} disabled={!editor.can().toggleHeaderCell()}> </button>
toggleHeaderCell <button onClick={() => editor.chain().focus().toggleHeaderCell().run()} disabled={!editor.can().toggleHeaderCell()}>
</button> Toggle header cell
<button onClick={() => editor.chain().focus().mergeOrSplit().run()} disabled={!editor.can().mergeOrSplit()}> </button>
mergeOrSplit <button onClick={() => editor.chain().focus().mergeOrSplit().run()} disabled={!editor.can().mergeOrSplit()}>
</button> Merge or split
<button onClick={() => editor.chain().focus().setCellAttribute('backgroundColor', '#FAF594').run()} disabled={!editor.can().setCellAttribute('backgroundColor', '#FAF594')}> </button>
setCellAttribute <button onClick={() => editor.chain().focus().setCellAttribute('backgroundColor', '#FAF594').run()} disabled={!editor.can().setCellAttribute('backgroundColor', '#FAF594')}>
</button> Set cell attribute
<button onClick={() => editor.chain().focus().fixTables().run()} disabled={!editor.can().fixTables()}> </button>
fixTables <button onClick={() => editor.chain().focus().fixTables().run()} disabled={!editor.can().fixTables()}>
</button> Fix tables
<button onClick={() => editor.chain().focus().goToNextCell().run()} disabled={!editor.can().goToNextCell()}> </button>
goToNextCell <button onClick={() => editor.chain().focus().goToNextCell().run()} disabled={!editor.can().goToNextCell()}>
</button> Go to next cell
<button onClick={() => editor.chain().focus().goToPreviousCell().run()} disabled={!editor.can().goToPreviousCell()}> </button>
goToPreviousCell <button onClick={() => editor.chain().focus().goToPreviousCell().run()} disabled={!editor.can().goToPreviousCell()}>
</button> Go to previous cell
</> </button>
</div>
</div>
) )
} }
@ -145,9 +147,9 @@ export default () => {
Have you seen our tables? They are amazing! Have you seen our tables? They are amazing!
</h3> </h3>
<ul> <ul>
<li>tables with rows, cells and headers (optional)</li> <li>Tables with rows, cells and headers (optional)</li>
<li>support for <code>colgroup</code> and <code>rowspan</code></li> <li>Support for <code>colgroup</code> and <code>rowspan</code></li>
<li>and even resizable columns (optional)</li> <li>And even resizable columns (optional)</li>
</ul> </ul>
<p> <p>
Here is an example: Here is an example:
@ -160,20 +162,20 @@ export default () => {
</tr> </tr>
<tr> <tr>
<td>Cyndi Lauper</td> <td>Cyndi Lauper</td>
<td>singer</td> <td>Singer</td>
<td>songwriter</td> <td>Songwriter</td>
<td>actress</td> <td>Actress</td>
</tr> </tr>
<tr> <tr>
<td>Marie Curie</td> <td>Marie Curie</td>
<td>scientist</td> <td>Scientist</td>
<td>chemist</td> <td>Chemist</td>
<td>physicist</td> <td>Physicist</td>
</tr> </tr>
<tr> <tr>
<td>Indira Gandhi</td> <td>Indira Gandhi</td>
<td>prime minister</td> <td>Prime minister</td>
<td colspan="2">politician</td> <td colspan="2">Politician</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -181,9 +183,9 @@ export default () => {
}) })
return ( return (
<div> <>
<MenuBar editor={editor} /> <MenuBar editor={editor} />
<EditorContent editor={editor} /> <EditorContent editor={editor} />
</div> </>
) )
} }

View File

@ -6,7 +6,7 @@ context('/src/Examples/Tables/React/', () => {
beforeEach(() => { beforeEach(() => {
cy.get('.tiptap').then(([{ editor }]) => { cy.get('.tiptap').then(([{ editor }]) => {
editor.commands.clearContent() editor.commands.clearContent()
cy.get('button').contains('insertTable').click() cy.get('button').contains('Insert table').click()
}) })
}) })
@ -28,15 +28,15 @@ context('/src/Examples/Tables/React/', () => {
}) })
it('adds & delete columns', () => { it('adds & delete columns', () => {
cy.get('button').contains('addColumnBefore').click() cy.get('button').contains('Add column before').click()
cy.get('.tiptap table th') cy.get('.tiptap table th')
.should('have.length', 4) .should('have.length', 4)
cy.get('button').contains('addColumnAfter').click() cy.get('button').contains('Add column after').click()
cy.get('.tiptap table th') cy.get('.tiptap table th')
.should('have.length', 5) .should('have.length', 5)
cy.get('button').contains('deleteColumn') cy.get('button').contains('Delete column')
.click() .click()
.click() .click()
cy.get('.tiptap table th') cy.get('.tiptap table th')
@ -44,15 +44,15 @@ context('/src/Examples/Tables/React/', () => {
}) })
it('adds & delete rows', () => { it('adds & delete rows', () => {
cy.get('button').contains('addRowBefore').click() cy.get('button').contains('Add row before').click()
cy.get('.tiptap table tr') cy.get('.tiptap table tr')
.should('have.length', 4) .should('have.length', 4)
cy.get('button').contains('addRowAfter').click() cy.get('button').contains('Add row after').click()
cy.get('.tiptap table tr') cy.get('.tiptap table tr')
.should('have.length', 5) .should('have.length', 5)
cy.get('button').contains('deleteRow') cy.get('button').contains('Delete row')
.click() .click()
.click() .click()
cy.get('.tiptap table tr') cy.get('.tiptap table tr')
@ -60,21 +60,21 @@ context('/src/Examples/Tables/React/', () => {
}) })
it('should delete table', () => { it('should delete table', () => {
cy.get('button').contains('deleteTable').click() cy.get('button').contains('Delete table').click()
cy.get('.tiptap table').should('not.exist') cy.get('.tiptap table').should('not.exist')
}) })
it('should merge cells', () => { it('should merge cells', () => {
cy.get('.tiptap').type('{shift}{rightArrow}') cy.get('.tiptap').type('{shift}{rightArrow}')
cy.get('button').contains('mergeCells').click() cy.get('button').contains('Merge cells').click()
cy.get('.tiptap table th').should('have.length', 2) cy.get('.tiptap table th').should('have.length', 2)
}) })
it('should split cells', () => { it('should split cells', () => {
cy.get('.tiptap').type('{shift}{rightArrow}') cy.get('.tiptap').type('{shift}{rightArrow}')
cy.get('button').contains('mergeCells').click() cy.get('button').contains('Merge cells').click()
cy.get('.tiptap table th').should('have.length', 2) cy.get('.tiptap table th').should('have.length', 2)
cy.get('button').contains('splitCell').click() cy.get('button').contains('Split cell').click()
cy.get('.tiptap table th').should('have.length', 3) cy.get('.tiptap table th').should('have.length', 3)
}) })
@ -94,11 +94,11 @@ context('/src/Examples/Tables/React/', () => {
it('should merge split', () => { it('should merge split', () => {
cy.get('.tiptap').type('{shift}{rightArrow}') cy.get('.tiptap').type('{shift}{rightArrow}')
cy.get('button').contains('mergeCells').click() cy.get('button').contains('Merge cells').click()
cy.get('.tiptap th[colspan="2"]') cy.get('.tiptap th[colspan="2"]')
.should('exist') .should('exist')
cy.get('button') cy.get('button')
.contains('mergeOrSplit') .contains('Merge or split')
.click() .click()
cy.get('.tiptap th[colspan="2"]') cy.get('.tiptap th[colspan="2"]')
.should('not.exist') .should('not.exist')
@ -106,15 +106,15 @@ context('/src/Examples/Tables/React/', () => {
it('should set cell attribute', () => { it('should set cell attribute', () => {
cy.get('.tiptap').type('{downArrow}') cy.get('.tiptap').type('{downArrow}')
cy.get('button').contains('setCellAttribute').click() cy.get('button').contains('Set cell attribute').click()
cy.get('.tiptap table td[style]').should('have.attr', 'style', 'background-color: #FAF594') cy.get('.tiptap table td[style]').should('have.attr', 'style', 'background-color: #FAF594')
}) })
it('should move focus to next or prev cell', () => { it('should move focus to next or prev cell', () => {
cy.get('.tiptap').type('Column 1') cy.get('.tiptap').type('Column 1')
cy.get('button').contains('goToNextCell').click() cy.get('button').contains('Go to next cell').click()
cy.get('.tiptap').type('Column 2') cy.get('.tiptap').type('Column 2')
cy.get('button').contains('goToPreviousCell').click() cy.get('button').contains('Go to previous cell').click()
cy.get('.tiptap th').then(elements => { cy.get('.tiptap th').then(elements => {
expect(elements[0].innerText).to.equal('Column 1') expect(elements[0].innerText).to.equal('Column 1')

View File

@ -1,79 +1,110 @@
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
margin: 1rem 0; :first-child {
margin-top: 0;
> * + * {
margin-top: 0.75em;
} }
ul, /* List styles */
ul,
ol { ol {
padding: 0 1rem; padding: 0 1rem;
} margin: 1.25rem 1rem 1.25rem 0.4rem;
h1, li p {
h2, margin-top: 0.25em;
h3, margin-bottom: 0.25em;
h4,
h5,
h6 {
line-height: 1.1;
}
code {
background-color: rgba(#616161, 0.1);
color: #616161;
}
pre {
background: #0D0D0D;
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
} }
} }
img { /* Heading styles */
max-width: 100%; h1,
height: auto; h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
}
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code {
background-color: var(--purple-light);
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}
pre {
background: var(--black);
border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
} }
blockquote { blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem; padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
} }
hr { hr {
border: none; border: none;
border-top: 2px solid rgba(#0D0D0D, 0.1); border-top: 1px solid var(--gray-2);
margin: 2rem 0; margin: 2rem 0;
} }
}
/* Table-specific styling */ /* Table-specific styling */
.tiptap {
table { table {
border-collapse: collapse; border-collapse: collapse;
table-layout: fixed;
width: 100%;
margin: 0; margin: 0;
overflow: hidden; overflow: hidden;
table-layout: fixed;
width: 100%;
td, td,
th { th {
min-width: 1em; border: 1px solid var(--gray-3);
border: 2px solid #ced4da;
padding: 3px 5px;
vertical-align: top;
box-sizing: border-box; box-sizing: border-box;
min-width: 1em;
padding: 6px 8px;
position: relative; position: relative;
vertical-align: top;
> * { > * {
margin-bottom: 0; margin-bottom: 0;
@ -81,37 +112,38 @@
} }
th { th {
background-color: var(--gray-1);
font-weight: bold; font-weight: bold;
text-align: left; text-align: left;
background-color: #f1f3f5;
} }
.selectedCell:after { .selectedCell:after {
z-index: 2; background: var(--gray-2);
position: absolute;
content: ""; content: "";
left: 0; right: 0; top: 0; bottom: 0; left: 0; right: 0; top: 0; bottom: 0;
background: rgba(200, 200, 255, 0.4);
pointer-events: none; pointer-events: none;
position: absolute;
z-index: 2;
} }
.column-resize-handle { .column-resize-handle {
background-color: var(--purple);
bottom: -2px;
pointer-events: none;
position: absolute; position: absolute;
right: -2px; right: -2px;
top: 0; top: 0;
bottom: -2px;
width: 4px; width: 4px;
background-color: #adf;
pointer-events: none;
} }
} }
}
.tableWrapper { .tableWrapper {
overflow-x: auto; margin: 1.5rem 0;
} overflow-x: auto;
}
.resize-cursor { .resize-cursor {
cursor: ew-resize; cursor: ew-resize;
cursor: col-resize; cursor: col-resize;
}
} }

View File

@ -6,7 +6,7 @@ context('/src/Examples/Tables/Vue/', () => {
beforeEach(() => { beforeEach(() => {
cy.get('.tiptap').then(([{ editor }]) => { cy.get('.tiptap').then(([{ editor }]) => {
editor.commands.clearContent() editor.commands.clearContent()
cy.get('button').contains('insertTable').click() cy.get('button').contains('Insert table').click()
}) })
}) })
@ -28,15 +28,15 @@ context('/src/Examples/Tables/Vue/', () => {
}) })
it('adds & delete columns', () => { it('adds & delete columns', () => {
cy.get('button').contains('addColumnBefore').click() cy.get('button').contains('Add column before').click()
cy.get('.tiptap table th') cy.get('.tiptap table th')
.should('have.length', 4) .should('have.length', 4)
cy.get('button').contains('addColumnAfter').click() cy.get('button').contains('Add column after').click()
cy.get('.tiptap table th') cy.get('.tiptap table th')
.should('have.length', 5) .should('have.length', 5)
cy.get('button').contains('deleteColumn') cy.get('button').contains('Delete column')
.click() .click()
.click() .click()
cy.get('.tiptap table th') cy.get('.tiptap table th')
@ -44,15 +44,15 @@ context('/src/Examples/Tables/Vue/', () => {
}) })
it('adds & delete rows', () => { it('adds & delete rows', () => {
cy.get('button').contains('addRowBefore').click() cy.get('button').contains('Add row before').click()
cy.get('.tiptap table tr') cy.get('.tiptap table tr')
.should('have.length', 4) .should('have.length', 4)
cy.get('button').contains('addRowAfter').click() cy.get('button').contains('Add row after').click()
cy.get('.tiptap table tr') cy.get('.tiptap table tr')
.should('have.length', 5) .should('have.length', 5)
cy.get('button').contains('deleteRow') cy.get('button').contains('Delete row')
.click() .click()
.click() .click()
cy.get('.tiptap table tr') cy.get('.tiptap table tr')
@ -60,21 +60,21 @@ context('/src/Examples/Tables/Vue/', () => {
}) })
it('should delete table', () => { it('should delete table', () => {
cy.get('button').contains('deleteTable').click() cy.get('button').contains('Delete table').click()
cy.get('.tiptap table').should('not.exist') cy.get('.tiptap table').should('not.exist')
}) })
it('should merge cells', () => { it('should merge cells', () => {
cy.get('.tiptap').type('{shift}{rightArrow}') cy.get('.tiptap').type('{shift}{rightArrow}')
cy.get('button').contains('mergeCells').click() cy.get('button').contains('Merge cells').click()
cy.get('.tiptap table th').should('have.length', 2) cy.get('.tiptap table th').should('have.length', 2)
}) })
it('should split cells', () => { it('should split cells', () => {
cy.get('.tiptap').type('{shift}{rightArrow}') cy.get('.tiptap').type('{shift}{rightArrow}')
cy.get('button').contains('mergeCells').click() cy.get('button').contains('Merge cells').click()
cy.get('.tiptap table th').should('have.length', 2) cy.get('.tiptap table th').should('have.length', 2)
cy.get('button').contains('splitCell').click() cy.get('button').contains('Split cell').click()
cy.get('.tiptap table th').should('have.length', 3) cy.get('.tiptap table th').should('have.length', 3)
}) })
@ -94,11 +94,11 @@ context('/src/Examples/Tables/Vue/', () => {
it('should merge split', () => { it('should merge split', () => {
cy.get('.tiptap').type('{shift}{rightArrow}') cy.get('.tiptap').type('{shift}{rightArrow}')
cy.get('button').contains('mergeCells').click() cy.get('button').contains('Merge cells').click()
cy.get('.tiptap th[colspan="2"]') cy.get('.tiptap th[colspan="2"]')
.should('exist') .should('exist')
cy.get('button') cy.get('button')
.contains('mergeOrSplit') .contains('Merge or split')
.click() .click()
cy.get('.tiptap th[colspan="2"]') cy.get('.tiptap th[colspan="2"]')
.should('not.exist') .should('not.exist')
@ -106,15 +106,15 @@ context('/src/Examples/Tables/Vue/', () => {
it('should set cell attribute', () => { it('should set cell attribute', () => {
cy.get('.tiptap').type('{downArrow}') cy.get('.tiptap').type('{downArrow}')
cy.get('button').contains('setCellAttribute').click() cy.get('button').contains('Set cell attribute').click()
cy.get('.tiptap table td[style]').should('have.attr', 'style', 'background-color: #FAF594') cy.get('.tiptap table td[style]').should('have.attr', 'style', 'background-color: #FAF594')
}) })
it('should move focus to next or prev cell', () => { it('should move focus to next or prev cell', () => {
cy.get('.tiptap').type('Column 1') cy.get('.tiptap').type('Column 1')
cy.get('button').contains('goToNextCell').click() cy.get('button').contains('Go to next cell').click()
cy.get('.tiptap').type('Column 2') cy.get('.tiptap').type('Column 2')
cy.get('button').contains('goToPreviousCell').click() cy.get('button').contains('Go to previous cell').click()
cy.get('.tiptap th').then(elements => { cy.get('.tiptap th').then(elements => {
expect(elements[0].innerText).to.equal('Column 1') expect(elements[0].innerText).to.equal('Column 1')

View File

@ -1,61 +1,70 @@
<template> <template>
<div v-if="editor"> <div v-if="editor" class="container">
<button @click="editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()"> <div class="control-group">
insertTable <div class="button-group">
</button> <button @click="editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()">
<button @click="editor.chain().focus().addColumnBefore().run()" :disabled="!editor.can().addColumnBefore()"> Insert table
addColumnBefore </button>
</button> <button
<button @click="editor.chain().focus().addColumnAfter().run()" :disabled="!editor.can().addColumnAfter()"> @click="editor.chain().focus().insertContent(tableHTML, { parseOptions: { preserveWhitespace: false }}).run()"
addColumnAfter >
</button> Insert HTML table
<button @click="editor.chain().focus().deleteColumn().run()" :disabled="!editor.can().deleteColumn()"> </button>
deleteColumn <button @click="editor.chain().focus().addColumnBefore().run()" :disabled="!editor.can().addColumnBefore()">
</button> Add column before
<button @click="editor.chain().focus().addRowBefore().run()" :disabled="!editor.can().addRowBefore()"> </button>
addRowBefore <button @click="editor.chain().focus().addColumnAfter().run()" :disabled="!editor.can().addColumnAfter()">
</button> Add column after
<button @click="editor.chain().focus().addRowAfter().run()" :disabled="!editor.can().addRowAfter()"> </button>
addRowAfter <button @click="editor.chain().focus().deleteColumn().run()" :disabled="!editor.can().deleteColumn()">
</button> Delete column
<button @click="editor.chain().focus().deleteRow().run()" :disabled="!editor.can().deleteRow()"> </button>
deleteRow <button @click="editor.chain().focus().addRowBefore().run()" :disabled="!editor.can().addRowBefore()">
</button> Add row before
<button @click="editor.chain().focus().deleteTable().run()" :disabled="!editor.can().deleteTable()"> </button>
deleteTable <button @click="editor.chain().focus().addRowAfter().run()" :disabled="!editor.can().addRowAfter()">
</button> Add row after
<button @click="editor.chain().focus().mergeCells().run()" :disabled="!editor.can().mergeCells()"> </button>
mergeCells <button @click="editor.chain().focus().deleteRow().run()" :disabled="!editor.can().deleteRow()">
</button> Delete row
<button @click="editor.chain().focus().splitCell().run()" :disabled="!editor.can().splitCell()"> </button>
splitCell <button @click="editor.chain().focus().deleteTable().run()" :disabled="!editor.can().deleteTable()">
</button> Delete table
<button @click="editor.chain().focus().toggleHeaderColumn().run()" :disabled="!editor.can().toggleHeaderColumn()"> </button>
toggleHeaderColumn <button @click="editor.chain().focus().mergeCells().run()" :disabled="!editor.can().mergeCells()">
</button> Merge cells
<button @click="editor.chain().focus().toggleHeaderRow().run()" :disabled="!editor.can().toggleHeaderRow()"> </button>
toggleHeaderRow <button @click="editor.chain().focus().splitCell().run()" :disabled="!editor.can().splitCell()">
</button> Split cell
<button @click="editor.chain().focus().toggleHeaderCell().run()" :disabled="!editor.can().toggleHeaderCell()"> </button>
toggleHeaderCell <button @click="editor.chain().focus().toggleHeaderColumn().run()" :disabled="!editor.can().toggleHeaderColumn()">
</button> Toggle header column
<button @click="editor.chain().focus().mergeOrSplit().run()" :disabled="!editor.can().mergeOrSplit()"> </button>
mergeOrSplit <button @click="editor.chain().focus().toggleHeaderRow().run()" :disabled="!editor.can().toggleHeaderRow()">
</button> Toggle header row
<button @click="editor.chain().focus().setCellAttribute('backgroundColor', '#FAF594').run()" :disabled="!editor.can().setCellAttribute('backgroundColor', '#FAF594')"> </button>
setCellAttribute <button @click="editor.chain().focus().toggleHeaderCell().run()" :disabled="!editor.can().toggleHeaderCell()">
</button> Toggle header cell
<button @click="editor.chain().focus().fixTables().run()" :disabled="!editor.can().fixTables()"> </button>
fixTables <button @click="editor.chain().focus().mergeOrSplit().run()" :disabled="!editor.can().mergeOrSplit()">
</button> Merge or split
<button @click="editor.chain().focus().goToNextCell().run()" :disabled="!editor.can().goToNextCell()"> </button>
goToNextCell <button @click="editor.chain().focus().setCellAttribute('backgroundColor', '#FAF594').run()" :disabled="!editor.can().setCellAttribute('backgroundColor', '#FAF594')">
</button> Set cell attribute
<button @click="editor.chain().focus().goToPreviousCell().run()" :disabled="!editor.can().goToPreviousCell()"> </button>
goToPreviousCell <button @click="editor.chain().focus().fixTables().run()" :disabled="!editor.can().fixTables()">
</button> Fix tables
</button>
<button @click="editor.chain().focus().goToNextCell().run()" :disabled="!editor.can().goToNextCell()">
Go to next cell
</button>
<button @click="editor.chain().focus().goToPreviousCell().run()" :disabled="!editor.can().goToPreviousCell()">
Go to previous cell
</button>
</div>
</div>
<editor-content :editor="editor" />
</div> </div>
<editor-content :editor="editor" />
</template> </template>
<script> <script>
@ -95,6 +104,29 @@ export default {
data() { data() {
return { return {
editor: null, editor: null,
tableHTML: `
<table style="width:100%">
<tr>
<th>Firstname</th>
<th>Lastname</th>
<th>Age</th>
</tr>
<tr>
<td>Jill</td>
<td>Smith</td>
<td>50</td>
</tr>
<tr>
<td>Eve</td>
<td>Jackson</td>
<td>94</td>
</tr>
<tr>
<td>John</td>
<td>Doe</td>
<td>80</td>
</tr>
</table>`,
} }
}, },
@ -117,9 +149,9 @@ export default {
Have you seen our tables? They are amazing! Have you seen our tables? They are amazing!
</h3> </h3>
<ul> <ul>
<li>tables with rows, cells and headers (optional)</li> <li>Tables with rows, cells and headers (optional)</li>
<li>support for <code>colgroup</code> and <code>rowspan</code></li> <li>Support for <code>colgroup</code> and <code>rowspan</code></li>
<li>and even resizable columns (optional)</li> <li>And even resizable columns (optional)</li>
</ul> </ul>
<p> <p>
Here is an example: Here is an example:
@ -132,20 +164,20 @@ export default {
</tr> </tr>
<tr> <tr>
<td>Cyndi Lauper</td> <td>Cyndi Lauper</td>
<td>singer</td> <td>Singer</td>
<td>songwriter</td> <td>Songwriter</td>
<td>actress</td> <td>Actress</td>
</tr> </tr>
<tr> <tr>
<td>Marie Curie</td> <td>Marie Curie</td>
<td>scientist</td> <td>Scientist</td>
<td>chemist</td> <td>Chemist</td>
<td>physicist</td> <td>Physicist</td>
</tr> </tr>
<tr> <tr>
<td>Indira Gandhi</td> <td>Indira Gandhi</td>
<td>prime minister</td> <td>Prime minister</td>
<td colspan="2">politician</td> <td colspan="2">Politician</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -162,17 +194,23 @@ export default {
<style lang="scss"> <style lang="scss">
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
margin: 1rem 0; :first-child {
margin-top: 0;
> * + * {
margin-top: 0.75em;
} }
/* List styles */
ul, ul,
ol { ol {
padding: 0 1rem; padding: 0 1rem;
margin: 1.25rem 1rem 1.25rem 0.4rem;
li p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
} }
/* Heading styles */
h1, h1,
h2, h2,
h3, h3,
@ -180,62 +218,87 @@ export default {
h5, h5,
h6 { h6 {
line-height: 1.1; line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
} }
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code { code {
background-color: rgba(#616161, 0.1); background-color: var(--purple-light);
color: #616161; border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
} }
pre { pre {
background: #0D0D0D; background: var(--black);
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem; border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code { code {
color: inherit;
padding: 0;
background: none; background: none;
color: inherit;
font-size: 0.8rem; font-size: 0.8rem;
padding: 0;
} }
} }
img {
max-width: 100%;
height: auto;
}
blockquote { blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem; padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
} }
hr { hr {
border: none; border: none;
border-top: 2px solid rgba(#0D0D0D, 0.1); border-top: 1px solid var(--gray-2);
margin: 2rem 0; margin: 2rem 0;
} }
}
/* Table-specific styling */ /* Table-specific styling */
.tiptap {
table { table {
border-collapse: collapse; border-collapse: collapse;
table-layout: fixed;
width: 100%;
margin: 0; margin: 0;
overflow: hidden; overflow: hidden;
table-layout: fixed;
width: 100%;
td, td,
th { th {
min-width: 1em; border: 1px solid var(--gray-3);
border: 2px solid #ced4da;
padding: 3px 5px;
vertical-align: top;
box-sizing: border-box; box-sizing: border-box;
min-width: 1em;
padding: 6px 8px;
position: relative; position: relative;
vertical-align: top;
> * { > * {
margin-bottom: 0; margin-bottom: 0;
@ -243,42 +306,39 @@ export default {
} }
th { th {
background-color: var(--gray-1);
font-weight: bold; font-weight: bold;
text-align: left; text-align: left;
background-color: #f1f3f5;
} }
.selectedCell:after { .selectedCell:after {
z-index: 2; background: var(--gray-2);
position: absolute;
content: ""; content: "";
left: 0; right: 0; top: 0; bottom: 0; left: 0; right: 0; top: 0; bottom: 0;
background: rgba(200, 200, 255, 0.4);
pointer-events: none; pointer-events: none;
position: absolute;
z-index: 2;
} }
.column-resize-handle { .column-resize-handle {
background-color: var(--purple);
bottom: -2px;
pointer-events: none;
position: absolute; position: absolute;
right: -2px; right: -2px;
top: 0; top: 0;
bottom: -2px;
width: 4px; width: 4px;
background-color: #adf;
pointer-events: none;
}
p {
margin: 0;
} }
} }
}
.tableWrapper { .tableWrapper {
overflow-x: auto; margin: 1.5rem 0;
} overflow-x: auto;
}
.resize-cursor { .resize-cursor {
cursor: ew-resize; cursor: ew-resize;
cursor: col-resize; cursor: col-resize;
}
} }
</style> </style>

View File

@ -1,23 +1,44 @@
ul[data-type="taskList"] { /* Basic editor styles */
list-style: none; .tiptap {
padding: 0; :first-child {
margin-top: 0;
}
li { /* List styles */
display: flex; ul,
align-items: center; ol {
padding: 0 1rem;
margin: 1.25rem 1rem 1.25rem 0.4rem;
> label { li p {
flex: 0 0 auto; margin-top: 0.25em;
margin-right: 0.5rem; margin-bottom: 0.25em;
user-select: none;
}
> div {
flex: 1 1 auto;
} }
} }
input[type="checkbox"] { /* Task list specific styles */
cursor: pointer; ul[data-type="taskList"] {
list-style: none;
margin-left: 0;
padding: 0;
li {
align-items: center;
display: flex;
> label {
flex: 0 0 auto;
margin-right: 0.5rem;
user-select: none;
}
> div {
flex: 1 1 auto;
}
}
input[type="checkbox"] {
cursor: pointer;
}
} }
} }

View File

@ -59,27 +59,48 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
ul[data-type="taskList"] { /* Basic editor styles */
list-style: none; .tiptap {
padding: 0; :first-child {
margin-top: 0;
}
li { /* List styles */
display: flex; ul,
align-items: center; ol {
padding: 0 1rem;
margin: 1.25rem 1rem 1.25rem 0.4rem;
> label { li p {
flex: 0 0 auto; margin-top: 0.25em;
margin-right: 0.5rem; margin-bottom: 0.25em;
user-select: none;
}
> div {
flex: 1 1 auto;
} }
} }
input[type="checkbox"] { /* Task list specific styles */
cursor: pointer; ul[data-type="taskList"] {
list-style: none;
margin-left: 0;
padding: 0;
li {
align-items: center;
display: flex;
> label {
flex: 0 0 auto;
margin-right: 0.5rem;
user-select: none;
}
> div {
flex: 1 1 auto;
}
}
input[type="checkbox"] {
cursor: pointer;
}
} }
} }
</style> </style>

View File

@ -1,69 +1,71 @@
<template> <template>
<div> <div v-if="editor" class="container">
<div v-if="editor" class="menu"> <div class="control-group">
<button @click="editor.chain().focus().toggleBold().run()" :class="{ 'is-active': editor.isActive('bold') }"> <div class="button-group">
bold <button @click="editor.chain().focus().toggleBold().run()" :class="{ 'is-active': editor.isActive('bold') }">
</button> Bold
<button @click="editor.chain().focus().toggleItalic().run()" :class="{ 'is-active': editor.isActive('italic') }"> </button>
italic <button @click="editor.chain().focus().toggleItalic().run()" :class="{ 'is-active': editor.isActive('italic') }">
</button> Italic
<button @click="editor.chain().focus().toggleStrike().run()" :class="{ 'is-active': editor.isActive('strike') }"> </button>
strike <button @click="editor.chain().focus().toggleStrike().run()" :class="{ 'is-active': editor.isActive('strike') }">
</button> Strike
<button @click="editor.chain().focus().toggleCode().run()" :class="{ 'is-active': editor.isActive('code') }"> </button>
code <button @click="editor.chain().focus().toggleCode().run()" :class="{ 'is-active': editor.isActive('code') }">
</button> Code
<button @click="editor.chain().focus().unsetAllMarks().run()"> </button>
clear marks <button @click="editor.chain().focus().unsetAllMarks().run()">
</button> Clear marks
<button @click="editor.chain().focus().clearNodes().run()"> </button>
clear nodes <button @click="editor.chain().focus().clearNodes().run()">
</button> Clear nodes
<button @click="editor.chain().focus().setParagraph().run()" :class="{ 'is-active': editor.isActive('paragraph') }"> </button>
paragraph <button @click="editor.chain().focus().setParagraph().run()" :class="{ 'is-active': editor.isActive('paragraph') }">
</button> Paragraph
<button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"> </button>
h1 <button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 1 }) }">
</button> H1
<button @click="editor.chain().focus().toggleHeading({ level: 2 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"> </button>
h2 <button @click="editor.chain().focus().toggleHeading({ level: 2 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 2 }) }">
</button> H2
<button @click="editor.chain().focus().toggleHeading({ level: 3 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"> </button>
h3 <button @click="editor.chain().focus().toggleHeading({ level: 3 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 3 }) }">
</button> H3
<button @click="editor.chain().focus().toggleHeading({ level: 4 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 4 }) }"> </button>
h4 <button @click="editor.chain().focus().toggleHeading({ level: 4 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 4 }) }">
</button> H4
<button @click="editor.chain().focus().toggleHeading({ level: 5 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 5 }) }"> </button>
h5 <button @click="editor.chain().focus().toggleHeading({ level: 5 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 5 }) }">
</button> H5
<button @click="editor.chain().focus().toggleHeading({ level: 6 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 6 }) }"> </button>
h6 <button @click="editor.chain().focus().toggleHeading({ level: 6 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 6 }) }">
</button> h6
<button @click="editor.chain().focus().toggleBulletList().run()" :class="{ 'is-active': editor.isActive('bulletList') }"> </button>
bullet list <button @click="editor.chain().focus().toggleBulletList().run()" :class="{ 'is-active': editor.isActive('bulletList') }">
</button> Bullet list
<button @click="editor.chain().focus().toggleOrderedList().run()" :class="{ 'is-active': editor.isActive('orderedList') }"> </button>
ordered list <button @click="editor.chain().focus().toggleOrderedList().run()" :class="{ 'is-active': editor.isActive('orderedList') }">
</button> Ordered list
<button @click="editor.chain().focus().toggleCodeBlock().run()" :class="{ 'is-active': editor.isActive('codeBlock') }"> </button>
code block <button @click="editor.chain().focus().toggleCodeBlock().run()" :class="{ 'is-active': editor.isActive('codeBlock') }">
</button> Code block
<button @click="editor.chain().focus().toggleBlockquote().run()" :class="{ 'is-active': editor.isActive('blockquote') }"> </button>
blockquote <button @click="editor.chain().focus().toggleBlockquote().run()" :class="{ 'is-active': editor.isActive('blockquote') }">
</button> Blockquote
<button @click="editor.chain().focus().setHorizontalRule().run()"> </button>
horizontal rule <button @click="editor.chain().focus().setHorizontalRule().run()">
</button> Horizontal rule
<button @click="editor.chain().focus().setHardBreak().run()"> </button>
hard break <button @click="editor.chain().focus().setHardBreak().run()">
</button> Hard break
<button @click="editor.chain().focus().undo().run()"> </button>
undo <button @click="editor.chain().focus().undo().run()">
</button> Undo
<button @click="editor.chain().focus().redo().run()"> </button>
redo <button @click="editor.chain().focus().redo().run()">
</button> Redo
</button>
</div>
</div> </div>
<editor-content :editor="editor" /> <editor-content :editor="editor" />
</div> </div>
@ -176,7 +178,7 @@ export default {
Hi there, Hi there,
</h2> </h2>
<p> <p>
this is a <em>basic</em> example of <strong>tiptap</strong>. Sure, there are all kind of basic text styles youd probably expect from a text editor. But wait until you see the lists: this is a <em>basic</em> example of <strong>Tiptap</strong>. Sure, there are all kind of basic text styles youd probably expect from a text editor. But wait until you see the lists:
</p> </p>
<ul> <ul>
<li> <li>
@ -232,26 +234,26 @@ export default {
</tr> </tr>
<tr> <tr>
<td>Cyndi Lauper</td> <td>Cyndi Lauper</td>
<td>singer</td> <td>Singer</td>
<td>songwriter</td> <td>Songwriter</td>
<td>actress</td> <td>Actress</td>
</tr> </tr>
<tr> <tr>
<td>Marie Curie</td> <td>Marie Curie</td>
<td>scientist</td> <td>Scientist</td>
<td>chemist</td> <td>Chemist</td>
<td>physicist</td> <td>Physicist</td>
</tr> </tr>
<tr> <tr>
<td>Indira Gandhi</td> <td>Indira Gandhi</td>
<td>prime minister</td> <td>Prime minister</td>
<td colspan="2">politician</td> <td colspan="2">Politician</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<p>This is a basic example of implementing images. Drag to re-order.</p> <p>This is a basic example of implementing images. Drag to re-order.</p>
<img src="https://source.unsplash.com/8xznAGy4HcY/800x400" /> <img src="https://placehold.co/800x400" />
<img src="https://source.unsplash.com/K9QHL52rE2k/800x400" /> <img src="https://placehold.co/800x400/6A00F5/white" />
`, `,
}) })
}, },
@ -263,16 +265,73 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
/* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
/* Placeholder (on every new line) */
.is-empty::before {
color: var(--gray-4);
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}
a {
color: var(--purple);
cursor: pointer;
&:hover {
color: var(--purple-contrast);
}
}
/* List styles */
ul, ul,
ol { ol {
padding: 0 1rem; padding: 0 1rem;
margin: 1.25rem 1rem 1.25rem 0.4rem;
li p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
} }
/* Task list specific styles */
ul[data-type="taskList"] {
list-style: none;
margin-left: 0;
padding: 0;
li {
align-items: flex-start;
display: flex;
> label {
flex: 0 0 auto;
margin-right: 0.5rem;
user-select: none;
}
> div {
flex: 1 1 auto;
}
}
input[type="checkbox"] {
cursor: pointer;
}
ul[data-type="taskList"] {
margin: 0;
}
}
/* Heading styles */
h1, h1,
h2, h2,
h3, h3,
@ -280,27 +339,64 @@ export default {
h5, h5,
h6 { h6 {
line-height: 1.1; line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
} }
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Display empty p */
p:empty::before {
content: '\00a0';
}
/* Code and preformatted text styles */
code { code {
background-color: rgba(#616161, 0.1); background-color: var(--purple-light);
color: #616161; border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
} }
pre { pre {
background: #0D0D0D; background: var(--black);
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem; border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code { code {
color: inherit;
padding: 0;
background: none; background: none;
color: inherit;
font-size: 0.8rem; font-size: 0.8rem;
padding: 0;
} }
/* Code styling */
.hljs-comment, .hljs-comment,
.hljs-quote { .hljs-quote {
color: #616161; color: #616161;
@ -316,7 +412,7 @@ export default {
.hljs-name, .hljs-name,
.hljs-selector-id, .hljs-selector-id,
.hljs-selector-class { .hljs-selector-class {
color: #F98181; color: #f98181;
} }
.hljs-number, .hljs-number,
@ -326,23 +422,23 @@ export default {
.hljs-literal, .hljs-literal,
.hljs-type, .hljs-type,
.hljs-params { .hljs-params {
color: #FBBC88; color: #fbbc88;
} }
.hljs-string, .hljs-string,
.hljs-symbol, .hljs-symbol,
.hljs-bullet { .hljs-bullet {
color: #B9F18D; color: #b9f18d;
} }
.hljs-title, .hljs-title,
.hljs-section { .hljs-section {
color: #FAF594; color: #faf594;
} }
.hljs-keyword, .hljs-keyword,
.hljs-selector-tag { .hljs-selector-tag {
color: #70CFF8; color: #70cff8;
} }
.hljs-emphasis { .hljs-emphasis {
@ -354,37 +450,91 @@ export default {
} }
} }
.code-block {
position: relative;
select {
position: absolute;
background-color: var(--white);
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="Black" d="M7 10l5 5 5-5z"/></svg>');
right: 0.5rem;
top: 0.5rem;
}
}
mark {
background-color: #FAF594;
border-radius: 0.4rem;
box-decoration-break: clone;
padding: 0.1rem 0.3rem;
}
img { img {
max-width: 100%; display: block;
height: auto; height: auto;
margin: 1.5rem 0;
max-width: 100%;
&.ProseMirror-selectednode {
outline: 3px solid var(--purple);
transition: outline 0.15s;
}
} }
blockquote { blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem; padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
} }
hr { hr {
border: none; border: none;
border-top: 2px solid rgba(#0D0D0D, 0.1); border-top: 1px solid var(--gray-2);
margin: 2rem 0; margin: 2rem 0;
} }
.mention {
background-color: var(--purple-light);
border-radius: 0.4rem;
box-decoration-break: clone;
color: var(--purple);
padding: 0.1rem 0.3rem;
}
/* Color swatches */
.color {
white-space: nowrap;
&::before {
background-color: var(--color);
border: 1px solid rgba(128, 128, 128, 0.3);
border-radius: 2px;
content: " ";
display: inline-block;
height: 1em;
margin-bottom: 0.15em;
margin-right: 0.1em;
vertical-align: middle;
width: 1em;
}
}
/* Table-specific styling */
table { table {
border-collapse: collapse; border-collapse: collapse;
table-layout: fixed;
width: 100%;
margin: 0; margin: 0;
overflow: hidden; overflow: hidden;
table-layout: fixed;
width: 100%;
td, td,
th { th {
min-width: 1em; border: 1px solid var(--gray-3);
border: 2px solid #ced4da;
padding: 3px 5px;
vertical-align: top;
box-sizing: border-box; box-sizing: border-box;
min-width: 1em;
padding: 6px 8px;
position: relative; position: relative;
vertical-align: top;
> * { > * {
margin-bottom: 0; margin-bottom: 0;
@ -392,87 +542,91 @@ export default {
} }
th { th {
background-color: var(--gray-1);
font-weight: bold; font-weight: bold;
text-align: left; text-align: left;
background-color: #f1f3f5;
} }
.selectedCell:after { .selectedCell:after {
z-index: 2; background: var(--gray-2);
position: absolute;
content: ""; content: "";
left: 0; right: 0; top: 0; bottom: 0; left: 0; right: 0; top: 0; bottom: 0;
background: rgba(200, 200, 255, 0.4);
pointer-events: none; pointer-events: none;
position: absolute;
z-index: 2;
} }
.column-resize-handle { .column-resize-handle {
background-color: var(--purple);
bottom: -2px;
pointer-events: none;
position: absolute; position: absolute;
right: -2px; right: -2px;
top: 0; top: 0;
bottom: -2px;
width: 4px; width: 4px;
background-color: #adf;
pointer-events: none;
}
p {
margin: 0;
} }
} }
ul[data-type="taskList"] { .tableWrapper {
list-style: none; margin: 1.5rem 0;
padding: 0; overflow-x: auto;
}
p { .resize-cursor {
margin: 0; cursor: ew-resize;
cursor: col-resize;
}
}
/* Floating/Bubble Menus */
.bubble-menu {
background-color: var(--white);
border: 1px solid var(--gray-1);
border-radius: 0.7rem;
box-shadow: var(--shadow);
display: flex;
padding: 0.2rem;
button {
background-color: unset;
&:hover {
background-color: var(--gray-3);
} }
li { &.is-active {
display: flex; background-color: var(--purple);
> label { &:hover {
flex: 0 0 auto; background-color: var(--purple-contrast);
margin-right: 0.5rem;
user-select: none;
}
> div {
flex: 1 1 auto;
} }
} }
} }
} }
.tiptap p.is-empty::before { .floating-menu {
content: attr(data-placeholder); display: flex;
float: left; background-color: var(--gray-3);
color: #adb5bd; padding: 0.1rem;
pointer-events: none; border-radius: 0.5rem;
height: 0;
}
.tableWrapper { button {
overflow-x: auto; background-color: unset;
} padding: 0.275rem 0.425rem;
border-radius: 0.3rem;
.resize-cursor { &:hover {
cursor: ew-resize; background-color: var(--gray-3);
cursor: col-resize; }
}
.mention { &.is-active {
border: 1px solid #000; background-color: var(--white);
border-radius: 0.4rem; color: var(--purple);
padding: 0.1rem 0.3rem;
box-decoration-break: clone;
}
.menu { &:hover {
position: sticky; color: var(--purple-contrast);
top: 0; }
background: #fff; }
z-index: 1; }
} }
</style> </style>

View File

@ -15,7 +15,7 @@ context('/src/Experiments/CollaborationAnnotation/Vue/', () => {
cy.window().then(win => { cy.window().then(win => {
cy.stub(win, 'prompt', () => 'This is a test comment') cy.stub(win, 'prompt', () => 'This is a test comment')
cy.get('.editor-1 .tiptap').type('{selectall}{backspace}Hello world{selectall}') cy.get('.editor-1 .tiptap').type('{selectall}{backspace}Hello world{selectall}')
cy.get('button').contains('comment').eq(0).click() cy.get('button').contains('Comment').eq(0).click()
cy.get('.editor-1 .tiptap').type('{end}') cy.get('.editor-1 .tiptap').type('{end}')
cy.get('.tiptap .annotation').should('have.length', 2) cy.get('.tiptap .annotation').should('have.length', 2)
cy.get('.comment').should('exist').contains('This is a test comment') cy.get('.comment').should('exist').contains('This is a test comment')
@ -42,11 +42,11 @@ context('/src/Experiments/CollaborationAnnotation/Vue/', () => {
}) })
cy.get('.editor-1 .tiptap').type('{selectall}{backspace}Hello world{selectall}') cy.get('.editor-1 .tiptap').type('{selectall}{backspace}Hello world{selectall}')
cy.get('button').contains('comment').eq(0).click() cy.get('button').contains('Comment').eq(0).click()
cy.wait(1000) cy.wait(1000)
cy.get('.editor-1 .tiptap').find('.annotation').click() cy.get('.editor-1 .tiptap').find('.annotation').click()
cy.get('.comment').should('exist').contains('This is a test comment') cy.get('.comment').should('exist').contains('This is a test comment')
cy.get('button').contains('update').click() cy.get('button').contains('Update').click()
cy.wait(1000) cy.wait(1000)
cy.get('.comment').should('exist').contains('This is the new comment') cy.get('.comment').should('exist').contains('This is the new comment')
}) })
@ -57,11 +57,11 @@ context('/src/Experiments/CollaborationAnnotation/Vue/', () => {
cy.stub(win, 'prompt', () => 'This is a test comment') cy.stub(win, 'prompt', () => 'This is a test comment')
cy.get('.editor-1 .tiptap').type('{selectall}{backspace}Hello world{selectall}') cy.get('.editor-1 .tiptap').type('{selectall}{backspace}Hello world{selectall}')
cy.get('button').contains('comment').eq(0).click() cy.get('button').contains('Comment').eq(0).click()
cy.wait(1000) cy.wait(1000)
cy.get('.editor-1 .tiptap').find('.annotation').click() cy.get('.editor-1 .tiptap').find('.annotation').click()
cy.get('.comment').should('exist').contains('This is a test comment') cy.get('.comment').should('exist').contains('This is a test comment')
cy.get('button').contains('remove').click() cy.get('button').contains('Remove').click()
cy.get('.tiptap .annotation').should('not.exist') cy.get('.tiptap .annotation').should('not.exist')
cy.wait(1000) cy.wait(1000)
cy.get('.comment').should('not.exist') cy.get('.comment').should('not.exist')

View File

@ -1,31 +1,38 @@
<template> <template>
<div> <div>
<div v-if="editor"> <div v-if="editor" class="container">
<h2> <div class="control-group">
Original Editor <h2>Original Editor</h2>
</h2> <div class="button-group">
<button @click="addComment" :disabled="!editor.can().addAnnotation()"> <button @click="addComment" :disabled="!editor.can().addAnnotation()">
comment Comment
</button> </button>
</div>
</div>
<editor-content class="editor-1" :editor="editor" /> <editor-content class="editor-1" :editor="editor" />
<div class="comment" v-for="comment in comments" :key="comment.id">
<div class="output-group" v-for="comment in comments" :key="comment.id">
{{ comment }} {{ comment }}
<button @click="updateComment(comment.id)"> <div class="button-group">
update <button @click="updateComment(comment.id)">
</button> Update
</button>
<button @click="deleteComment(comment.id)"> <button @click="deleteComment(comment.id)">
remove Remove
</button> </button>
</div>
</div> </div>
<h2> <div class="control-group">
Another Editor <h2>Another Editor</h2>
</h2> <div class="button-group">
<button @click="addAnotherComment" :disabled="!anotherEditor.can().addAnnotation()"> <button @click="addAnotherComment" :disabled="!anotherEditor.can().addAnnotation()">
comment Comment
</button> </button>
</div>
</div>
<editor-content class="editor-2" :editor="anotherEditor" /> <editor-content class="editor-2" :editor="anotherEditor" />
</div> </div>
</div> </div>
@ -80,7 +87,7 @@ export default {
Annotations can be used to add additional information to the content, for example comments. They live on a different level than the actual editor content. Annotations can be used to add additional information to the content, for example comments. They live on a different level than the actual editor content.
</p> </p>
<p> <p>
This example allows you to add plain text, but youre free to add more complex data, for example JSON from another tiptap instance. :-) This example allows you to add plain text, but youre free to add more complex data, for example JSON from another Tiptap instance. :-)
</p> </p>
`, `,
}) })
@ -138,8 +145,44 @@ export default {
<style lang="scss"> <style lang="scss">
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
}
/* Heading styles */
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
}
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
} }
} }

View File

@ -1,14 +1,13 @@
<template> <template>
<div class="items"> <div class="dropdown-menu">
<template v-if="items.length"> <template v-if="items.length">
<button <button
class="item"
:class="{ 'is-selected': index === selectedIndex }" :class="{ 'is-selected': index === selectedIndex }"
v-for="(item, index) in items" v-for="(item, index) in items"
:key="index" :key="index"
@click="selectItem(index)" @click="selectItem(index)"
> >
{{ item }} {{ item.title }}
</button> </button>
</template> </template>
<div class="item" v-else> <div class="item" v-else>
@ -87,32 +86,35 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
.items { /* Dropdown menu */
padding: 0.2rem; .dropdown-menu {
background: var(--white);
border: 1px solid var(--gray-1);
border-radius: 0.7rem;
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
gap: 0.1rem;
overflow: auto;
padding: 0.4rem;
position: relative; position: relative;
border-radius: 0.5rem;
background: #FFF;
color: rgba(0, 0, 0, 0.8);
overflow: hidden;
font-size: 0.9rem;
box-shadow:
0 0 0 1px rgba(0, 0, 0, 0.05),
0px 10px 20px rgba(0, 0, 0, 0.1),
;
}
.item { button {
display: block; align-items: center;
margin: 0; background-color: transparent;
width: 100%; display: flex;
text-align: left; gap: 0.25rem;
background: transparent; text-align: left;
border-radius: 0.4rem; width: 100%;
border: 1px solid transparent;
padding: 0.2rem 0.4rem;
&.is-selected { &:hover,
border-color: #000; &:hover.is-selected {
background-color: var(--gray-3);
}
&.is-selected {
background-color: var(--gray-2);
}
} }
} }
</style> </style>

View File

@ -19,8 +19,8 @@ context('/src/Experiments/Commands/Vue/', () => {
items.forEach((item, i) => { items.forEach((item, i) => {
cy.get('.tiptap').type('{selectall}{backspace}/') cy.get('.tiptap').type('{selectall}{backspace}/')
cy.get('.tippy-content .items').should('exist') cy.get('.tippy-content .dropdown-menu').should('exist')
cy.get('.tippy-content .items .item').eq(i).click() cy.get('.tippy-content .dropdown-menu button').eq(i).click()
cy.get('.tiptap').type(`I am a ${item.tag}`) cy.get('.tiptap').type(`I am a ${item.tag}`)
cy.get(`.tiptap ${item.tag}`).should('exist').should('have.text', `I am a ${item.tag}`) cy.get(`.tiptap ${item.tag}`).should('exist').should('have.text', `I am a ${item.tag}`)
}) })
@ -28,17 +28,17 @@ context('/src/Experiments/Commands/Vue/', () => {
it('should close the popup without any command via esc', () => { it('should close the popup without any command via esc', () => {
cy.get('.tiptap').type('{selectall}{backspace}/') cy.get('.tiptap').type('{selectall}{backspace}/')
cy.get('.tippy-content .items').should('exist') cy.get('.tippy-content .dropdown-menu').should('exist')
cy.get('.tiptap').type('{esc}') cy.get('.tiptap').type('{esc}')
cy.get('.tippy-content .items').should('not.exist') cy.get('.tippy-content .dropdown-menu').should('not.exist')
}) })
it('should open the popup when the cursor is after a slash', () => { it('should open the popup when the cursor is after a slash', () => {
cy.get('.tiptap').type('{selectall}{backspace}/') cy.get('.tiptap').type('{selectall}{backspace}/')
cy.get('.tippy-content .items').should('exist') cy.get('.tippy-content .dropdown-menu').should('exist')
cy.get('.tiptap').type('{leftArrow}') cy.get('.tiptap').type('{leftArrow}')
cy.get('.tippy-content .items').should('not.exist') cy.get('.tippy-content .dropdown-menu').should('not.exist')
cy.get('.tiptap').type('{rightArrow}') cy.get('.tiptap').type('{rightArrow}')
cy.get('.tippy-content .items').should('exist') cy.get('.tippy-content .dropdown-menu').should('exist')
}) })
}) })

View File

@ -45,16 +45,95 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
/* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
}
/* List styles */
ul,
ol {
padding: 0 1rem;
margin: 1.25rem 1rem 1.25rem 0.4rem;
li p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
}
/* Heading styles */
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
}
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code {
background-color: var(--purple-light);
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}
pre {
background: var(--black);
border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
}
blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem;
}
hr {
border: none;
border-top: 1px solid var(--gray-2);
margin: 2rem 0;
} }
} }
.mention {
border: 1px solid #000;
border-radius: 0.4rem;
padding: 0.1rem 0.3rem;
box-decoration-break: clone;
}
</style> </style>

View File

@ -7,7 +7,7 @@ export default {
items: ({ query }) => { items: ({ query }) => {
return [ return [
{ {
title: 'H1', title: 'Heading 1',
command: ({ editor, range }) => { command: ({ editor, range }) => {
editor editor
.chain() .chain()
@ -18,7 +18,7 @@ export default {
}, },
}, },
{ {
title: 'H2', title: 'Heading 2',
command: ({ editor, range }) => { command: ({ editor, range }) => {
editor editor
.chain() .chain()
@ -29,7 +29,7 @@ export default {
}, },
}, },
{ {
title: 'bold', title: 'Bold',
command: ({ editor, range }) => { command: ({ editor, range }) => {
editor editor
.chain() .chain()
@ -40,7 +40,7 @@ export default {
}, },
}, },
{ {
title: 'italic', title: 'Italic',
command: ({ editor, range }) => { command: ({ editor, range }) => {
editor editor
.chain() .chain()

View File

@ -1,9 +1,11 @@
<template> <template>
<div v-if="editor"> <div v-if="editor" class="container">
<button @click="() => editor.chain().toggleBold().focus().run()">Make bold</button> <div class="control-group">
<button @click="() => editor.destroy()">Destroy editor</button> <div class="button-group">
</div> <button @click="() => editor.chain().toggleBold().focus().run()">Make bold</button>
<div v-if="editor"> <button @click="() => editor.destroy()">Destroy editor</button>
</div>
</div>
<editor-content :editor="editor" /> <editor-content :editor="editor" />
</div> </div>
</template> </template>
@ -41,9 +43,95 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
/* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
}
/* List styles */
ul,
ol {
padding: 0 1rem;
margin: 1.25rem 1rem 1.25rem 0.4rem;
li p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
}
/* Heading styles */
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
}
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code {
background-color: var(--purple-light);
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}
pre {
background: var(--black);
border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
}
blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem;
}
hr {
border: none;
border-top: 1px solid var(--gray-2);
margin: 2rem 0;
} }
} }
</style> </style>

View File

@ -1,9 +1,12 @@
<template> <template>
<div v-if="editor"> <div v-if="editor" class="container">
<button @click="addIframe"> <div class="control-group">
add iframe <div class="button-group">
</button> <button @click="addIframe">
Add iFrame
</button>
</div>
</div>
<editor-content :editor="editor" /> <editor-content :editor="editor" />
</div> </div>
</template> </template>

View File

@ -18,7 +18,7 @@ export default () => {
], ],
content: ` content: `
<p> <p>
This is a radically reduced version of tiptap. It has support for a document, with paragraphs and text. Thats it. Its probably too much for real minimalists though. This is a radically reduced version of Tiptap. It has support for a document, with paragraphs and text. Thats it. Its probably too much for real minimalists though.
</p> </p>
<p> <p>
The paragraph extension is not really required, but you need at least one node. Sure, that node can be something different. The paragraph extension is not really required, but you need at least one node. Sure, that node can be something different.
@ -28,8 +28,10 @@ export default () => {
return ( return (
<> <>
reactive storage: {editor?.storage.custom.foo}
<EditorContent editor={editor} /> <EditorContent editor={editor} />
<div className="output-group">
Reactive storage: {editor?.storage.custom.foo}
</div>
</> </>
) )
} }

View File

@ -1,6 +1,6 @@
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
} }

View File

@ -1,6 +1,10 @@
<template> <template>
reactive storage: {{ editor?.storage.custom.foo }} <div class="container">
<editor-content :editor="editor" /> <editor-content :editor="editor" />
<div class="output-group">
Reactive storage: {{ editor?.storage.custom.foo }}
</div>
</div>
</template> </template>
<script> <script>
@ -32,7 +36,7 @@ export default {
], ],
content: ` content: `
<p> <p>
This is a radically reduced version of tiptap. It has support for a document, with paragraphs and text. Thats it. Its probably too much for real minimalists though. This is a radically reduced version of Tiptap. It has support for a document, with paragraphs and text. Thats it. Its probably too much for real minimalists though.
</p> </p>
<p> <p>
The paragraph extension is not really required, but you need at least one node. Sure, that node can be something different. The paragraph extension is not really required, but you need at least one node. Sure, that node can be something different.
@ -50,8 +54,8 @@ export default {
<style lang="scss"> <style lang="scss">
/* Basic editor styles */ /* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
} }
</style> </style>

View File

@ -1,24 +1,30 @@
<template> <template>
<div v-if="editor"> <div v-if="editor" class="container">
<button @click="addFigure"> <div class="control-group">
figure <div class="button-group">
</button> <button @click="addFigure">
<button Add image with caption
@click="editor.chain().focus().imageToFigure().run()" </button>
:disabled="!editor.can().imageToFigure()" <button
> @click="editor.chain().focus().imageToFigure().run()"
image to figure :disabled="!editor.can().imageToFigure()"
</button> >
<button Add caption to image
@click="editor.chain().focus().figureToImage().run()" </button>
:disabled="!editor.can().figureToImage()" <button
> @click="editor.chain().focus().figureToImage().run()"
figure to image :disabled="!editor.can().figureToImage()"
</button> >
Remove caption from image
</button>
</div>
</div>
<editor-content :editor="editor" /> <editor-content :editor="editor" />
<h2>HTML</h2> <div class="output-group">
{{ editor.getHTML() }} <label>HTML</label>
<code>{{ editor.getHTML() }}</code>
</div>
</div> </div>
</template> </template>
@ -65,14 +71,14 @@ export default {
content: ` content: `
<p>Figure + Figcaption</p> <p>Figure + Figcaption</p>
<figure> <figure>
<img src="https://source.unsplash.com/8xznAGy4HcY/800x400" alt="Random photo of something" title="Whos dat?"> <img src="https://placehold.co/800x400/orange/white" alt="Random photo of something" title="Whos dat?">
<figcaption> <figcaption>
<p>Amazing caption</p> <p>Amazing caption</p>
</figcaption> </figcaption>
</figure> </figure>
<img src="https://source.unsplash.com/K9QHL52rE2k/800x400"> <img src="https://placehold.co/800x400/green/white">
<img src="https://source.unsplash.com/8xznAGy4HcY/800x400"> <img src="https://placehold.co/800x400/blue/white">
<img src="https://source.unsplash.com/K9QHL52rE2k/800x400"> <img src="https://placehold.co/800x400/black/white">
<p>Thats it.</p> <p>Thats it.</p>
`, `,
}) })
@ -85,32 +91,136 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
/* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
figure { /* List styles */
max-width: 25rem; ul,
border: 3px solid #0D0D0D; ol {
border-radius: 0.5rem; padding: 0 1rem;
margin: 1rem 0; margin: 1.25rem 1rem 1.25rem 0.4rem;
padding: 0.5rem;
li p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
} }
figcaption { /* Heading styles */
margin-top: 0.25rem; h1,
text-align: center; h2,
padding: 0.5rem; h3,
border: 2px dashed #0D0D0D20; h4,
h5,
h6 {
line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
}
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code {
background-color: var(--purple-light);
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}
pre {
background: var(--black);
border-radius: 0.5rem; border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
}
blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem;
}
hr {
border: none;
border-top: 1px solid var(--gray-2);
margin: 2rem 0;
} }
img { img {
display: block; display: block;
max-width: min(100%, 25rem);
height: auto; height: auto;
margin: 1.5rem 0;
max-width: 100%;
&.ProseMirror-selectednode {
outline: 3px solid var(--purple);
}
}
/* Figure */
figure {
align-items: start;
border: 2px solid var(--black);
border-radius: 0.5rem; border-radius: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
margin: 1rem 0;
padding: 0.5rem;
width: fit-content;
> *:not(figcaption) {
margin: 0;
max-width: 100%;
}
&:has(figcaption:active) {
border-color: var(--purple);
}
figcaption {
border-radius: 0.5rem;
border: 2px dashed #0D0D0D20;
padding: 0.5rem;
text-align: center;
width: 100%;
}
} }
} }
</style> </style>

View File

@ -1,17 +1,21 @@
<template> <template>
<div v-if="editor"> <div v-if="editor" class="container">
<button @click="addCapturedTable"> <div class="control-group">
add table <div class="button-group">
</button> <button @click="addCapturedTable">
<button @click="addCapturedImage"> Add table with caption
add image </button>
</button> <button @click="addCapturedImage">
<button @click="removeCapturedTable"> Add image with caption
remove table </button>
</button> <button @click="removeCapturedTable">
<button @click="removeCapturedImage"> Remove table with caption
remove image </button>
</button> <button @click="removeCapturedImage">
Remove image with caption
</button>
</div>
</div>
<editor-content :editor="editor" /> <editor-content :editor="editor" />
</div> </div>
</template> </template>
@ -62,14 +66,14 @@ export default {
content: [ content: [
{ {
type: 'text', type: 'text',
text: 'image caption', text: 'Image caption',
}, },
], ],
}, },
{ {
type: 'image', type: 'image',
attrs: { attrs: {
src: 'https://source.unsplash.com/K9QHL52rE2k/800x400', src: 'https://placehold.co/800x400/orange/white',
}, },
}, },
], ],
@ -89,7 +93,7 @@ export default {
content: [ content: [
{ {
type: 'text', type: 'text',
text: 'table caption', text: 'Table caption',
}, },
], ],
}, },
@ -107,7 +111,7 @@ export default {
content: [ content: [
{ {
type: 'text', type: 'text',
text: 'cell 1', text: 'Cell 1',
}, },
], ],
}, },
@ -121,7 +125,7 @@ export default {
content: [ content: [
{ {
type: 'text', type: 'text',
text: 'cell 2', text: 'Cell 2',
}, },
], ],
}, },
@ -172,10 +176,10 @@ export default {
<figcaption> <figcaption>
Image caption Image caption
</figcaption> </figcaption>
<img src="https://source.unsplash.com/8xznAGy4HcY/800x400" alt="Random photo of something" title="Whos dat?"> <img src="https://placehold.co/800x400/black/white" alt="Random photo of something" title="Whos dat?">
</figure> </figure>
<p>Some text</p> <p>Some text</p>
<img src="https://source.unsplash.com/K9QHL52rE2k/800x400"> <img src="https://placehold.co/800x400">
<p>Some text</p> <p>Some text</p>
<figure data-type="capturedTable"> <figure data-type="capturedTable">
<figcaption> <figcaption>
@ -189,20 +193,20 @@ export default {
</tr> </tr>
<tr> <tr>
<td>Cyndi Lauper</td> <td>Cyndi Lauper</td>
<td>singer</td> <td>Singer</td>
<td>songwriter</td> <td>Songwriter</td>
<td>actress</td> <td>Actress</td>
</tr> </tr>
<tr> <tr>
<td>Marie Curie</td> <td>Marie Curie</td>
<td>scientist</td> <td>Scientist</td>
<td>chemist</td> <td>Chemist</td>
<td>physicist</td> <td>Physicist</td>
</tr> </tr>
<tr> <tr>
<td>Indira Gandhi</td> <td>Indira Gandhi</td>
<td>prime minister</td> <td>Prime minister</td>
<td colspan="2">politician</td> <td colspan="2">Politician</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -216,20 +220,20 @@ export default {
</tr> </tr>
<tr> <tr>
<td>Cyndi Lauper</td> <td>Cyndi Lauper</td>
<td>singer</td> <td>Singer</td>
<td>songwriter</td> <td>Songwriter</td>
<td>actress</td> <td>Actress</td>
</tr> </tr>
<tr> <tr>
<td>Marie Curie</td> <td>Marie Curie</td>
<td>scientist</td> <td>Scientist</td>
<td>chemist</td> <td>Chemist</td>
<td>physicist</td> <td>Physicist</td>
</tr> </tr>
<tr> <tr>
<td>Indira Gandhi</td> <td>Indira Gandhi</td>
<td>prime minister</td> <td>Prime minister</td>
<td colspan="2">politician</td> <td colspan="2">Politician</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -244,57 +248,123 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
/* Basic editor styles */
.tiptap { .tiptap {
> * + * { :first-child {
margin-top: 0.75em; margin-top: 0;
} }
figure { /* List styles */
max-width: 25rem; ul,
border: 3px solid #0D0D0D; ol {
border-radius: 0.5rem; padding: 0 1rem;
margin: 1rem 0; margin: 1.25rem 1rem 1.25rem 0.4rem;
padding: 0.5rem;
li p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
} }
figcaption { /* Heading styles */
margin: 0.25rem 0; h1,
text-align: center; h2,
padding: 0.5rem; h3,
border: 2px dashed #0D0D0D20; h4,
h5,
h6 {
line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
}
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code {
background-color: var(--purple-light);
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}
pre {
background: var(--black);
border-radius: 0.5rem; border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
&:first-child { code {
margin-top: 0; background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
} }
}
&:last-child { blockquote {
margin-bottom: 0; border-left: 3px solid var(--gray-3);
} margin: 1.5rem 0;
padding-left: 1rem;
}
hr {
border: none;
border-top: 1px solid var(--gray-2);
margin: 2rem 0;
} }
img { img {
display: block; display: block;
max-width: min(100%, 25rem);
height: auto; height: auto;
border-radius: 0.5rem; margin: 1.5rem 0;
max-width: 100%;
&.ProseMirror-selectednode {
outline: 3px solid var(--purple);
}
} }
table { table {
border-collapse: collapse; border-collapse: collapse;
table-layout: fixed;
width: 100%;
margin: 0; margin: 0;
overflow: hidden; overflow: hidden;
table-layout: fixed;
width: 100%;
td, td,
th { th {
min-width: 1em; border: 1px solid var(--gray-3);
border: 2px solid #ced4da;
padding: 3px 5px;
vertical-align: top;
box-sizing: border-box; box-sizing: border-box;
min-width: 1em;
padding: 6px 8px;
position: relative; position: relative;
vertical-align: top;
> * { > * {
margin-bottom: 0; margin-bottom: 0;
@ -302,28 +372,68 @@ export default {
} }
th { th {
background-color: var(--gray-1);
font-weight: bold; font-weight: bold;
text-align: left; text-align: left;
background-color: #f1f3f5;
} }
.selectedCell:after { .selectedCell:after {
z-index: 2; background: var(--gray-2);
position: absolute;
content: ""; content: "";
left: 0; right: 0; top: 0; bottom: 0; left: 0; right: 0; top: 0; bottom: 0;
background: rgba(200, 200, 255, 0.4);
pointer-events: none; pointer-events: none;
position: absolute;
z-index: 2;
} }
.column-resize-handle { .column-resize-handle {
background-color: var(--purple);
bottom: -2px;
pointer-events: none;
position: absolute; position: absolute;
right: -2px; right: -2px;
top: 0; top: 0;
bottom: -2px;
width: 4px; width: 4px;
background-color: #adf; }
pointer-events: none; }
.tableWrapper {
margin: 1.5rem 0;
overflow-x: auto;
}
.resize-cursor {
cursor: ew-resize;
cursor: col-resize;
}
/* Figure */
figure {
align-items: start;
border: 2px solid var(--black);
border-radius: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
margin: 1rem 0;
padding: 0.5rem;
width: fit-content;
> *:not(figcaption) {
margin: 0;
max-width: 100%;
}
&:has(figcaption:active) {
border-color: var(--purple);
}
figcaption {
border-radius: 0.5rem;
border: 2px dashed #0D0D0D20;
padding: 0.5rem;
text-align: center;
width: 100%;
} }
} }
} }

View File

@ -28,14 +28,14 @@ export default {
DragHandle, DragHandle,
], ],
content: ` content: `
<p>paragraph 1</p> <p>Paragraph 1</p>
<p>paragraph 2</p> <p>Paragraph 2</p>
<p>paragraph 3</p> <p>Paragraph 3</p>
<ul> <ul>
<li>list item 1</li> <li>List item 1</li>
<li>list item 2</li> <li>List item 2</li>
</ul> </ul>
<pre>code</pre> <pre>Code</pre>
`, `,
onUpdate: () => { onUpdate: () => {
// eslint-disable-next-line // eslint-disable-next-line

Some files were not shown because too many files have changed in this diff Show More