mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-15 15:44:45 -04:00
Wrap markdown rendering in iframe (#837)
Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
parent
bd31076928
commit
586969f368
45 changed files with 1014 additions and 287 deletions
|
@ -61,6 +61,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
- A toggle in the editor preferences for turning ligatures on and off.
|
- A toggle in the editor preferences for turning ligatures on and off.
|
||||||
- Easier possibility to share notes via native share-buttons on supported devices.
|
- Easier possibility to share notes via native share-buttons on supported devices.
|
||||||
- Surround selected text with a link via shortcut (ctrl+k or cmd+k).
|
- Surround selected text with a link via shortcut (ctrl+k or cmd+k).
|
||||||
|
- Improved security by wrapping the markdown rendering into an iframe
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,8 @@ describe('Autocompletion', () => {
|
||||||
.should('have.text', '```abnf')
|
.should('have.text', '```abnf')
|
||||||
cy.get('.CodeMirror-code > div:nth-of-type(3) > .CodeMirror-line > span span')
|
cy.get('.CodeMirror-code > div:nth-of-type(3) > .CodeMirror-line > span span')
|
||||||
.should('have.text', '```')
|
.should('have.text', '```')
|
||||||
cy.get('.markdown-body > pre > code')
|
cy.getMarkdownBody()
|
||||||
|
.find('pre > code')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
})
|
})
|
||||||
it('via doubleclick', () => {
|
it('via doubleclick', () => {
|
||||||
|
@ -40,7 +41,8 @@ describe('Autocompletion', () => {
|
||||||
.should('have.text', '```abnf')
|
.should('have.text', '```abnf')
|
||||||
cy.get('.CodeMirror-code > div:nth-of-type(3) > .CodeMirror-line > span span')
|
cy.get('.CodeMirror-code > div:nth-of-type(3) > .CodeMirror-line > span span')
|
||||||
.should('have.text', '```')
|
.should('have.text', '```')
|
||||||
cy.get('.markdown-body > pre > code')
|
cy.getMarkdownBody()
|
||||||
|
.find('pre > code')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -58,7 +60,8 @@ describe('Autocompletion', () => {
|
||||||
.should('have.text', ':::success')
|
.should('have.text', ':::success')
|
||||||
cy.get('.CodeMirror-code > div:nth-of-type(3) > .CodeMirror-line > span span')
|
cy.get('.CodeMirror-code > div:nth-of-type(3) > .CodeMirror-line > span span')
|
||||||
.should('have.text', '::: ')
|
.should('have.text', '::: ')
|
||||||
cy.get('.markdown-body > div.alert')
|
cy.getMarkdownBody()
|
||||||
|
.find('div.alert')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
})
|
})
|
||||||
it('via doubleclick', () => {
|
it('via doubleclick', () => {
|
||||||
|
@ -72,7 +75,8 @@ describe('Autocompletion', () => {
|
||||||
.should('have.text', ':::success')
|
.should('have.text', ':::success')
|
||||||
cy.get('.CodeMirror-code > div:nth-of-type(3) > .CodeMirror-line > span span')
|
cy.get('.CodeMirror-code > div:nth-of-type(3) > .CodeMirror-line > span span')
|
||||||
.should('have.text', '::: ')
|
.should('have.text', '::: ')
|
||||||
cy.get('.markdown-body > div.alert')
|
cy.getMarkdownBody()
|
||||||
|
.find('div.alert')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -80,8 +84,7 @@ describe('Autocompletion', () => {
|
||||||
describe('emoji', () => {
|
describe('emoji', () => {
|
||||||
describe('normal emoji', () => {
|
describe('normal emoji', () => {
|
||||||
it('via Enter', () => {
|
it('via Enter', () => {
|
||||||
cy.get('@codeinput')
|
cy.codemirrorFill(':hedg')
|
||||||
.fill(':hedg')
|
|
||||||
cy.get('.CodeMirror-hints')
|
cy.get('.CodeMirror-hints')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
cy.get('@codeinput')
|
cy.get('@codeinput')
|
||||||
|
@ -90,12 +93,11 @@ describe('Autocompletion', () => {
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||||
.should('have.text', ':hedgehog:')
|
.should('have.text', ':hedgehog:')
|
||||||
cy.get('.markdown-body')
|
cy.getMarkdownBody()
|
||||||
.should('have.text', '🦔')
|
.should('have.text', '🦔')
|
||||||
})
|
})
|
||||||
it('via doubleclick', () => {
|
it('via doubleclick', () => {
|
||||||
cy.get('@codeinput')
|
cy.codemirrorFill(':hedg')
|
||||||
.fill(':hedg')
|
|
||||||
cy.get('.CodeMirror-hints > li')
|
cy.get('.CodeMirror-hints > li')
|
||||||
.first()
|
.first()
|
||||||
.dblclick()
|
.dblclick()
|
||||||
|
@ -103,15 +105,14 @@ describe('Autocompletion', () => {
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||||
.should('have.text', ':hedgehog:')
|
.should('have.text', ':hedgehog:')
|
||||||
cy.get('.markdown-body')
|
cy.getMarkdownBody()
|
||||||
.should('have.text', '🦔')
|
.should('have.text', '🦔')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('fork-awesome-icon', () => {
|
describe('fork-awesome-icon', () => {
|
||||||
it('via Enter', () => {
|
it('via Enter', () => {
|
||||||
cy.get('@codeinput')
|
cy.codemirrorFill(':fa-face')
|
||||||
.fill(':fa-face')
|
|
||||||
cy.get('.CodeMirror-hints')
|
cy.get('.CodeMirror-hints')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
cy.get('@codeinput')
|
cy.get('@codeinput')
|
||||||
|
@ -120,12 +121,12 @@ describe('Autocompletion', () => {
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||||
.should('have.text', ':fa-facebook:')
|
.should('have.text', ':fa-facebook:')
|
||||||
cy.get('.markdown-body > p > i.fa.fa-facebook')
|
cy.getMarkdownBody()
|
||||||
|
.find('p > i.fa.fa-facebook')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
})
|
})
|
||||||
it('via doubleclick', () => {
|
it('via doubleclick', () => {
|
||||||
cy.get('@codeinput')
|
cy.codemirrorFill(':fa-face')
|
||||||
.fill(':fa-face')
|
|
||||||
cy.get('.CodeMirror-hints > li')
|
cy.get('.CodeMirror-hints > li')
|
||||||
.first()
|
.first()
|
||||||
.dblclick()
|
.dblclick()
|
||||||
|
@ -133,7 +134,8 @@ describe('Autocompletion', () => {
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||||
.should('have.text', ':fa-facebook:')
|
.should('have.text', ':fa-facebook:')
|
||||||
cy.get('.markdown-body > p > i.fa.fa-facebook')
|
cy.getMarkdownBody()
|
||||||
|
.find('p > i.fa.fa-facebook')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -150,7 +152,8 @@ describe('Autocompletion', () => {
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||||
.should('have.text', '# ')
|
.should('have.text', '# ')
|
||||||
cy.get('.markdown-body > h1 ')
|
cy.getMarkdownBody()
|
||||||
|
.find('h1 ')
|
||||||
.should('have.text', ' ')
|
.should('have.text', ' ')
|
||||||
})
|
})
|
||||||
it('via doubleclick', () => {
|
it('via doubleclick', () => {
|
||||||
|
@ -162,7 +165,8 @@ describe('Autocompletion', () => {
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||||
.should('have.text', '# ')
|
.should('have.text', '# ')
|
||||||
cy.get('.markdown-body > h1')
|
cy.getMarkdownBody()
|
||||||
|
.find('h1')
|
||||||
.should('have.text', ' ')
|
.should('have.text', ' ')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -178,7 +182,8 @@ describe('Autocompletion', () => {
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||||
.should('have.text', '')
|
.should('have.text', '')
|
||||||
cy.get('.markdown-body > p > img')
|
cy.getMarkdownBody()
|
||||||
|
.find('p > img')
|
||||||
.should('have.attr', 'alt', 'image alt')
|
.should('have.attr', 'alt', 'image alt')
|
||||||
.should('have.attr', 'src', 'https://')
|
.should('have.attr', 'src', 'https://')
|
||||||
.should('have.attr', 'title', 'title')
|
.should('have.attr', 'title', 'title')
|
||||||
|
@ -192,7 +197,8 @@ describe('Autocompletion', () => {
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||||
.should('have.text', '')
|
.should('have.text', '')
|
||||||
cy.get('.markdown-body > p > img')
|
cy.getMarkdownBody()
|
||||||
|
.find('p > img')
|
||||||
.should('have.attr', 'alt', 'image alt')
|
.should('have.attr', 'alt', 'image alt')
|
||||||
.should('have.attr', 'src', 'https://')
|
.should('have.attr', 'src', 'https://')
|
||||||
.should('have.attr', 'title', 'title')
|
.should('have.attr', 'title', 'title')
|
||||||
|
@ -210,7 +216,8 @@ describe('Autocompletion', () => {
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||||
.should('have.text', '[link text](https:// "title") ')
|
.should('have.text', '[link text](https:// "title") ')
|
||||||
cy.get('.markdown-body > p > a')
|
cy.getMarkdownBody()
|
||||||
|
.find('p > a')
|
||||||
.should('have.text', 'link text')
|
.should('have.text', 'link text')
|
||||||
.should('have.attr', 'href', 'https://')
|
.should('have.attr', 'href', 'https://')
|
||||||
.should('have.attr', 'title', 'title')
|
.should('have.attr', 'title', 'title')
|
||||||
|
@ -224,7 +231,8 @@ describe('Autocompletion', () => {
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||||
.should('have.text', '[link text](https:// "title") ')
|
.should('have.text', '[link text](https:// "title") ')
|
||||||
cy.get('.markdown-body > p > a')
|
cy.getMarkdownBody()
|
||||||
|
.find('p > a')
|
||||||
.should('have.text', 'link text')
|
.should('have.text', 'link text')
|
||||||
.should('have.attr', 'href', 'https://')
|
.should('have.attr', 'href', 'https://')
|
||||||
.should('have.attr', 'title', 'title')
|
.should('have.attr', 'title', 'title')
|
||||||
|
@ -242,7 +250,8 @@ describe('Autocompletion', () => {
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||||
.should('have.text', '{%pdf https:// %}')
|
.should('have.text', '{%pdf https:// %}')
|
||||||
cy.get('.markdown-body > p')
|
cy.getMarkdownBody()
|
||||||
|
.find('p')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
})
|
})
|
||||||
it('via doubleclick', () => {
|
it('via doubleclick', () => {
|
||||||
|
@ -254,7 +263,8 @@ describe('Autocompletion', () => {
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||||
.should('have.text', '{%pdf https:// %}')
|
.should('have.text', '{%pdf https:// %}')
|
||||||
cy.get('.markdown-body > p')
|
cy.getMarkdownBody()
|
||||||
|
.find('p')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -270,7 +280,8 @@ describe('Autocompletion', () => {
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||||
.should('have.text', '</details>') // after selecting the hint, the last line of the inserted suggestion is active
|
.should('have.text', '</details>') // after selecting the hint, the last line of the inserted suggestion is active
|
||||||
cy.get('.markdown-body > details')
|
cy.getMarkdownBody()
|
||||||
|
.find('details')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
})
|
})
|
||||||
it('via doubleclick', () => {
|
it('via doubleclick', () => {
|
||||||
|
@ -282,7 +293,8 @@ describe('Autocompletion', () => {
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||||
.should('have.text', '</details>')
|
.should('have.text', '</details>')
|
||||||
cy.get('.markdown-body > details')
|
cy.getMarkdownBody()
|
||||||
|
.find('details')
|
||||||
.should('exist')
|
.should('exist')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -87,7 +87,8 @@ describe('Document Title', () => {
|
||||||
|
|
||||||
it('katex code looks right', () => {
|
it('katex code looks right', () => {
|
||||||
cy.codemirrorFill(`# $\\alpha$-foo`)
|
cy.codemirrorFill(`# $\\alpha$-foo`)
|
||||||
cy.get('.markdown-body > h1')
|
cy.getMarkdownRenderer()
|
||||||
|
.find('h1')
|
||||||
.should('contain', 'α')
|
.should('contain', 'α')
|
||||||
cy.get('.CodeMirror textarea')
|
cy.get('.CodeMirror textarea')
|
||||||
.type('{Enter}{Enter}{Enter}{Enter}{Enter}') //This is a workaround because I don't know how to make sure, that the title gets updated in time.
|
.type('{Enter}{Enter}{Enter}{Enter}{Enter}') //This is a workaround because I don't know how to make sure, that the title gets updated in time.
|
||||||
|
|
|
@ -4,33 +4,37 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const findHljsCodeBlock = () => {
|
||||||
|
return cy.getMarkdownBody()
|
||||||
|
.find('pre > code.hljs')
|
||||||
|
.should('be.visible')
|
||||||
|
}
|
||||||
|
|
||||||
describe('Code', () => {
|
describe('Code', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('/n/test', {
|
cy.visit('/n/test')
|
||||||
onBeforeLoad (win: Window): void {
|
|
||||||
cy.spy(win.navigator.clipboard, 'writeText').as('copy')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('with just the language', () => {
|
describe('with just the language', () => {
|
||||||
it('doesn\'t show a gutter', () => {
|
it('doesn\'t show a gutter', () => {
|
||||||
cy.codemirrorFill('```javascript \nlet x = 0\n```')
|
cy.codemirrorFill('```javascript \nlet x = 0\n```')
|
||||||
cy.get('.markdown-body > pre > code.hljs')
|
findHljsCodeBlock()
|
||||||
.should('be.visible')
|
|
||||||
.should('not.have.class', 'showGutter')
|
.should('not.have.class', 'showGutter')
|
||||||
cy.get('.markdown-body > pre > code.hljs > .linenumber')
|
|
||||||
|
findHljsCodeBlock()
|
||||||
|
.find('.linenumber')
|
||||||
.should('not.be.visible')
|
.should('not.be.visible')
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('and line wrapping', () => {
|
describe('and line wrapping', () => {
|
||||||
it('doesn\'t show a gutter', () => {
|
it('doesn\'t show a gutter', () => {
|
||||||
cy.codemirrorFill('```javascript! \nlet x = 0\n```')
|
cy.codemirrorFill('```javascript! \nlet x = 0\n```')
|
||||||
cy.get('.markdown-body > pre > code.hljs')
|
findHljsCodeBlock()
|
||||||
.should('be.visible')
|
|
||||||
.should('not.have.class', 'showGutter')
|
.should('not.have.class', 'showGutter')
|
||||||
.should('have.class', 'wrapLines')
|
.should('have.class', 'wrapLines')
|
||||||
cy.get('.markdown-body > pre > code.hljs > .linenumber')
|
|
||||||
|
findHljsCodeBlock()
|
||||||
|
.find('.linenumber')
|
||||||
.should('not.be.visible')
|
.should('not.be.visible')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -39,10 +43,11 @@ describe('Code', () => {
|
||||||
describe('with the language and show gutter', () => {
|
describe('with the language and show gutter', () => {
|
||||||
it('shows the correct line number', () => {
|
it('shows the correct line number', () => {
|
||||||
cy.codemirrorFill('```javascript= \nlet x = 0\n```')
|
cy.codemirrorFill('```javascript= \nlet x = 0\n```')
|
||||||
cy.get('.markdown-body > pre > code.hljs')
|
findHljsCodeBlock()
|
||||||
.should('be.visible')
|
|
||||||
.should('have.class', 'showGutter')
|
.should('have.class', 'showGutter')
|
||||||
cy.get('.markdown-body > pre > code.hljs > .linenumber')
|
|
||||||
|
findHljsCodeBlock()
|
||||||
|
.find('.linenumber')
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
.text()
|
.text()
|
||||||
.should('eq', '1')
|
.should('eq', '1')
|
||||||
|
@ -51,11 +56,12 @@ describe('Code', () => {
|
||||||
describe('and line wrapping', () => {
|
describe('and line wrapping', () => {
|
||||||
it('shows the correct line number', () => {
|
it('shows the correct line number', () => {
|
||||||
cy.codemirrorFill('```javascript=! \nlet x = 0\n```')
|
cy.codemirrorFill('```javascript=! \nlet x = 0\n```')
|
||||||
cy.get('.markdown-body > pre > code.hljs')
|
findHljsCodeBlock()
|
||||||
.should('be.visible')
|
|
||||||
.should('have.class', 'showGutter')
|
.should('have.class', 'showGutter')
|
||||||
.should('have.class', 'wrapLines')
|
.should('have.class', 'wrapLines')
|
||||||
cy.get('.markdown-body > pre > code.hljs > .linenumber')
|
|
||||||
|
findHljsCodeBlock()
|
||||||
|
.find('.linenumber')
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
.text()
|
.text()
|
||||||
.should('eq', '1')
|
.should('eq', '1')
|
||||||
|
@ -66,10 +72,11 @@ describe('Code', () => {
|
||||||
describe('with the language, show gutter with a start number', () => {
|
describe('with the language, show gutter with a start number', () => {
|
||||||
it('shows the correct line number', () => {
|
it('shows the correct line number', () => {
|
||||||
cy.codemirrorFill('```javascript=100 \nlet x = 0\n```')
|
cy.codemirrorFill('```javascript=100 \nlet x = 0\n```')
|
||||||
cy.get('.markdown-body > pre > code.hljs')
|
findHljsCodeBlock()
|
||||||
.should('be.visible')
|
|
||||||
.should('have.class', 'showGutter')
|
.should('have.class', 'showGutter')
|
||||||
cy.get('.markdown-body > pre > code.hljs > .linenumber')
|
|
||||||
|
findHljsCodeBlock()
|
||||||
|
.find('.linenumber')
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
.text()
|
.text()
|
||||||
.should('eq', '100')
|
.should('eq', '100')
|
||||||
|
@ -77,8 +84,7 @@ describe('Code', () => {
|
||||||
|
|
||||||
it('shows the correct line number and continues in another codeblock', () => {
|
it('shows the correct line number and continues in another codeblock', () => {
|
||||||
cy.codemirrorFill('```javascript=100 \nlet x = 0\nlet y = 1\n```\n\n```javascript=+\nlet y = 2\n```\n')
|
cy.codemirrorFill('```javascript=100 \nlet x = 0\nlet y = 1\n```\n\n```javascript=+\nlet y = 2\n```\n')
|
||||||
cy.get('.markdown-body > pre > code.hljs')
|
findHljsCodeBlock()
|
||||||
.should('be.visible')
|
|
||||||
.should('have.class', 'showGutter')
|
.should('have.class', 'showGutter')
|
||||||
.first()
|
.first()
|
||||||
.find('.linenumber')
|
.find('.linenumber')
|
||||||
|
@ -86,14 +92,14 @@ describe('Code', () => {
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
.text()
|
.text()
|
||||||
.should('eq', '100')
|
.should('eq', '100')
|
||||||
cy.get('.markdown-body > pre > code.hljs')
|
findHljsCodeBlock()
|
||||||
.first()
|
.first()
|
||||||
.find('.linenumber')
|
.find('.linenumber')
|
||||||
.last()
|
.last()
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
.text()
|
.text()
|
||||||
.should('eq', '101')
|
.should('eq', '101')
|
||||||
cy.get('.markdown-body > pre > code.hljs')
|
findHljsCodeBlock()
|
||||||
.last()
|
.last()
|
||||||
.find('.linenumber')
|
.find('.linenumber')
|
||||||
.first()
|
.first()
|
||||||
|
@ -105,11 +111,11 @@ describe('Code', () => {
|
||||||
describe('and line wrapping', () => {
|
describe('and line wrapping', () => {
|
||||||
it('shows the correct line number', () => {
|
it('shows the correct line number', () => {
|
||||||
cy.codemirrorFill('```javascript=100! \nlet x = 0\n```')
|
cy.codemirrorFill('```javascript=100! \nlet x = 0\n```')
|
||||||
cy.get('.markdown-body > pre > code.hljs')
|
findHljsCodeBlock()
|
||||||
.should('be.visible')
|
|
||||||
.should('have.class', 'showGutter')
|
.should('have.class', 'showGutter')
|
||||||
.should('have.class', 'wrapLines')
|
.should('have.class', 'wrapLines')
|
||||||
cy.get('.markdown-body > pre > code.hljs > .linenumber')
|
findHljsCodeBlock()
|
||||||
|
.find('.linenumber')
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
.text()
|
.text()
|
||||||
.should('eq', '100')
|
.should('eq', '100')
|
||||||
|
@ -117,8 +123,7 @@ describe('Code', () => {
|
||||||
|
|
||||||
it('shows the correct line number and continues in another codeblock', () => {
|
it('shows the correct line number and continues in another codeblock', () => {
|
||||||
cy.codemirrorFill('```javascript=100! \nlet x = 0\nlet y = 1\n```\n\n```javascript=+\nlet y = 2\n```\n')
|
cy.codemirrorFill('```javascript=100! \nlet x = 0\nlet y = 1\n```\n\n```javascript=+\nlet y = 2\n```\n')
|
||||||
cy.get('.markdown-body > pre > code.hljs')
|
findHljsCodeBlock()
|
||||||
.should('be.visible')
|
|
||||||
.should('have.class', 'showGutter')
|
.should('have.class', 'showGutter')
|
||||||
.should('have.class', 'wrapLines')
|
.should('have.class', 'wrapLines')
|
||||||
.first()
|
.first()
|
||||||
|
@ -127,14 +132,14 @@ describe('Code', () => {
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
.text()
|
.text()
|
||||||
.should('eq', '100')
|
.should('eq', '100')
|
||||||
cy.get('.markdown-body > pre > code.hljs')
|
findHljsCodeBlock()
|
||||||
.first()
|
.first()
|
||||||
.find('.linenumber')
|
.find('.linenumber')
|
||||||
.last()
|
.last()
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
.text()
|
.text()
|
||||||
.should('eq', '101')
|
.should('eq', '101')
|
||||||
cy.get('.markdown-body > pre > code.hljs')
|
findHljsCodeBlock()
|
||||||
.last()
|
.last()
|
||||||
.find('.linenumber')
|
.find('.linenumber')
|
||||||
.first()
|
.first()
|
||||||
|
@ -147,9 +152,22 @@ describe('Code', () => {
|
||||||
|
|
||||||
it('has a working copy button', () => {
|
it('has a working copy button', () => {
|
||||||
cy.codemirrorFill('```javascript \nlet x = 0\n```')
|
cy.codemirrorFill('```javascript \nlet x = 0\n```')
|
||||||
cy.get('.markdown-body > pre > div > button > i')
|
|
||||||
.should('have.class', 'fa-files-o')
|
cy.get(`iframe[data-cy="documentIframe"]`)
|
||||||
|
.then(($element: JQuery) => {
|
||||||
|
const frame = $element[0] as HTMLIFrameElement
|
||||||
|
if (frame === null || frame.contentWindow === null) {
|
||||||
|
return cy.wrap(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
cy.spy(frame.contentWindow.navigator.clipboard, 'writeText').as("copy")
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.getMarkdownRenderer()
|
||||||
|
.find('[data-cy="copy-code-button"]')
|
||||||
.click()
|
.click()
|
||||||
cy.get('@copy').should('be.calledWithExactly', 'let x = 0\n')
|
|
||||||
|
cy.get("@copy")
|
||||||
|
.should('be.calledWithExactly', 'let x = 0\n')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -35,7 +35,8 @@ describe('The status bar text length info', () => {
|
||||||
cy.codemirrorFill(tooMuchTestContent)
|
cy.codemirrorFill(tooMuchTestContent)
|
||||||
cy.get('[data-cy="limitReachedModal"]')
|
cy.get('[data-cy="limitReachedModal"]')
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
cy.get('[data-cy="limitReachedMessage"]')
|
cy.getMarkdownRenderer()
|
||||||
|
.find('[data-cy="limitReachedMessage"]')
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -11,19 +11,22 @@ describe('YAML Array for deprecated syntax of document tags in frontmatter', ()
|
||||||
|
|
||||||
it('is shown when using old syntax', () => {
|
it('is shown when using old syntax', () => {
|
||||||
cy.codemirrorFill('---\ntags: a, b, c\n---')
|
cy.codemirrorFill('---\ntags: a, b, c\n---')
|
||||||
cy.get('[data-cy="yamlArrayDeprecationAlert"]')
|
cy.getMarkdownRenderer()
|
||||||
|
.find('[data-cy="yamlArrayDeprecationAlert"]')
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('isn\'t shown when using inline yaml-array', () => {
|
it('isn\'t shown when using inline yaml-array', () => {
|
||||||
cy.codemirrorFill('---\ntags: [\'a\', \'b\', \'c\']\n---')
|
cy.codemirrorFill('---\ntags: [\'a\', \'b\', \'c\']\n---')
|
||||||
cy.get('[data-cy="yamlArrayDeprecationAlert"]')
|
cy.getMarkdownRenderer()
|
||||||
|
.find('[data-cy="yamlArrayDeprecationAlert"]')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('isn\'t shown when using multi line yaml-array', () => {
|
it('isn\'t shown when using multi line yaml-array', () => {
|
||||||
cy.codemirrorFill('---\ntags:\n - a\n - b\n - c\n---')
|
cy.codemirrorFill('---\ntags:\n - a\n - b\n - c\n---')
|
||||||
cy.get('[data-cy="yamlArrayDeprecationAlert"]')
|
cy.getMarkdownRenderer()
|
||||||
|
.find('[data-cy="yamlArrayDeprecationAlert"]')
|
||||||
.should('not.exist')
|
.should('not.exist')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -49,6 +49,10 @@ beforeEach(() => {
|
||||||
version: 'mock',
|
version: 'mock',
|
||||||
sourceCodeUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
sourceCodeUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
||||||
issueTrackerUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
|
issueTrackerUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
|
||||||
|
},
|
||||||
|
"iframeCommunication": {
|
||||||
|
"editorOrigin": "http://127.0.0.1:3001",
|
||||||
|
"rendererOrigin": "http://127.0.0.1:3001"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
27
cypress/support/getMarkdownRenderer.ts
Normal file
27
cypress/support/getMarkdownRenderer.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare namespace Cypress {
|
||||||
|
interface Chainable {
|
||||||
|
getMarkdownRenderer (): Chainable<Element>
|
||||||
|
|
||||||
|
getMarkdownBody (): Chainable<Element>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Cypress.Commands.add('getMarkdownRenderer', () => {
|
||||||
|
return cy.get(`iframe[data-cy="documentIframe"]`)
|
||||||
|
.its('0.contentDocument')
|
||||||
|
.should('exist')
|
||||||
|
.its('body')
|
||||||
|
.should('not.be.undefined')
|
||||||
|
.then(cy.wrap.bind(cy))
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add('getMarkdownBody', () => {
|
||||||
|
return cy.getMarkdownRenderer()
|
||||||
|
.find('.markdown-body')
|
||||||
|
})
|
|
@ -24,4 +24,5 @@ import 'cypress-file-upload'
|
||||||
import './checkLinks'
|
import './checkLinks'
|
||||||
import './config'
|
import './config'
|
||||||
import './fill'
|
import './fill'
|
||||||
|
import './getMarkdownRenderer'
|
||||||
import './login'
|
import './login'
|
||||||
|
|
|
@ -39,5 +39,9 @@
|
||||||
"version": "mock",
|
"version": "mock",
|
||||||
"sourceCodeUrl": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
"sourceCodeUrl": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||||
"issueTrackerUrl": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
"issueTrackerUrl": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
||||||
|
},
|
||||||
|
"iframeCommunication": {
|
||||||
|
"editorOrigin": "http://localhost:3001",
|
||||||
|
"rendererOrigin": "http://localhost:3001"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
6
src/api/config/types.d.ts
vendored
6
src/api/config/types.d.ts
vendored
|
@ -16,6 +16,12 @@ export interface Config {
|
||||||
version: BackendVersion,
|
version: BackendVersion,
|
||||||
plantumlServer: string | null,
|
plantumlServer: string | null,
|
||||||
maxDocumentLength: number,
|
maxDocumentLength: number,
|
||||||
|
iframeCommunication: iframeCommunicationConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface iframeCommunicationConfig {
|
||||||
|
editorOrigin: string,
|
||||||
|
rendererOrigin: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BrandingConfig {
|
export interface BrandingConfig {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
/*
|
/*
|
||||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { Fragment, useRef } from 'react'
|
import React, { Fragment, useRef } from 'react'
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
|
@ -15,15 +15,22 @@ export interface CopyToClipboardButtonProps {
|
||||||
content: string
|
content: string
|
||||||
size?: 'sm' | 'lg'
|
size?: 'sm' | 'lg'
|
||||||
variant?: Variant
|
variant?: Variant
|
||||||
|
"data-cy"?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CopyToClipboardButton: React.FC<CopyToClipboardButtonProps> = ({ content, size = 'sm', variant = 'dark' }) => {
|
export const CopyToClipboardButton: React.FC<CopyToClipboardButtonProps> = ({
|
||||||
|
content,
|
||||||
|
size = 'sm',
|
||||||
|
variant = 'dark',
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const button = useRef<HTMLButtonElement>(null)
|
const button = useRef<HTMLButtonElement>(null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Button ref={button} size={size} variant={variant} title={t('renderer.highlightCode.copyCode')}>
|
<Button ref={button} size={size} variant={variant} title={t('renderer.highlightCode.copyCode')}
|
||||||
|
data-cy={props["data-cy"]}>
|
||||||
<ForkAwesomeIcon icon='files-o'/>
|
<ForkAwesomeIcon icon='files-o'/>
|
||||||
</Button>
|
</Button>
|
||||||
<CopyOverlay content={content} clickComponent={button}/>
|
<CopyOverlay content={content} clickComponent={button}/>
|
||||||
|
|
|
@ -6,8 +6,12 @@
|
||||||
|
|
||||||
export const download = (data: BlobPart, fileName: string, mimeType: string): void => {
|
export const download = (data: BlobPart, fileName: string, mimeType: string): void => {
|
||||||
const file = new Blob([data], { type: mimeType })
|
const file = new Blob([data], { type: mimeType })
|
||||||
|
downloadLink(URL.createObjectURL(file), fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const downloadLink = (url: string, fileName: string): void => {
|
||||||
const helperElement = document.createElement('a')
|
const helperElement = document.createElement('a')
|
||||||
helperElement.href = URL.createObjectURL(file)
|
helperElement.href = url
|
||||||
helperElement.download = fileName
|
helperElement.download = fileName
|
||||||
document.body.appendChild(helperElement)
|
document.body.appendChild(helperElement)
|
||||||
helperElement.click()
|
helperElement.click()
|
||||||
|
|
106
src/components/editor/document-renderer-pane/document-iframe.tsx
Normal file
106
src/components/editor/document-renderer-pane/document-iframe.tsx
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { useIsDarkModeActivated } from '../../../hooks/common/use-is-dark-mode-activated'
|
||||||
|
import { ApplicationState } from '../../../redux'
|
||||||
|
import { isTestMode } from '../../../utils/is-test-mode'
|
||||||
|
import { ImageLightboxModal } from '../../markdown-renderer/replace-components/image/image-lightbox-modal'
|
||||||
|
import { IframeEditorToRendererCommunicator } from '../../render-page/iframe-editor-to-renderer-communicator'
|
||||||
|
import { ImageDetails } from '../../render-page/rendering-message'
|
||||||
|
import { ScrollingDocumentRenderPaneProps } from './scrolling-document-render-pane'
|
||||||
|
|
||||||
|
export const DocumentIframe: React.FC<ScrollingDocumentRenderPaneProps> = (
|
||||||
|
{
|
||||||
|
markdownContent,
|
||||||
|
onTaskCheckedChange,
|
||||||
|
onMetadataChange,
|
||||||
|
scrollState,
|
||||||
|
onFirstHeadingChange,
|
||||||
|
wide,
|
||||||
|
onScroll,
|
||||||
|
onMakeScrollSource,
|
||||||
|
extraClasses
|
||||||
|
}) => {
|
||||||
|
const frameReference = useRef<HTMLIFrameElement>(null)
|
||||||
|
const darkMode = useIsDarkModeActivated()
|
||||||
|
const [lightboxDetails, setLightboxDetails] = useState<ImageDetails | undefined>(undefined)
|
||||||
|
|
||||||
|
const rendererOrigin = useSelector((state: ApplicationState) => state.config.iframeCommunication.rendererOrigin)
|
||||||
|
const renderPageUrl = `${rendererOrigin}/render`
|
||||||
|
const iframeCommunicator = useMemo(() => new IframeEditorToRendererCommunicator(), [])
|
||||||
|
useEffect(() => () => iframeCommunicator.unregisterEventListener(), [iframeCommunicator])
|
||||||
|
|
||||||
|
const [rendererReady, setRendererReady] = useState<boolean>(false)
|
||||||
|
|
||||||
|
useEffect(() => iframeCommunicator.onFirstHeadingChange(onFirstHeadingChange), [iframeCommunicator, onFirstHeadingChange])
|
||||||
|
useEffect(() => iframeCommunicator.onMetaDataChange(onMetadataChange), [iframeCommunicator, onMetadataChange])
|
||||||
|
useEffect(() => iframeCommunicator.onSetScrollState(onScroll), [iframeCommunicator, onScroll])
|
||||||
|
useEffect(() => iframeCommunicator.onSetScrollSourceToRenderer(onMakeScrollSource), [iframeCommunicator, onMakeScrollSource])
|
||||||
|
useEffect(() => iframeCommunicator.onTaskCheckboxChange(onTaskCheckedChange), [iframeCommunicator, onTaskCheckedChange])
|
||||||
|
useEffect(() => iframeCommunicator.onImageClicked(setLightboxDetails), [iframeCommunicator])
|
||||||
|
useEffect(() => iframeCommunicator.onRendererReady(() => setRendererReady(true)), [darkMode, iframeCommunicator, scrollState, wide])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (rendererReady) {
|
||||||
|
iframeCommunicator.sendSetMarkdownContent(markdownContent)
|
||||||
|
}
|
||||||
|
}, [iframeCommunicator, markdownContent, rendererReady])
|
||||||
|
useEffect(() => {
|
||||||
|
if (rendererReady) {
|
||||||
|
iframeCommunicator.sendSetDarkmode(darkMode)
|
||||||
|
}
|
||||||
|
}, [darkMode, iframeCommunicator, rendererReady])
|
||||||
|
useEffect(() => {
|
||||||
|
if (rendererReady) {
|
||||||
|
iframeCommunicator.sendScrollState(scrollState)
|
||||||
|
}
|
||||||
|
}, [iframeCommunicator, rendererReady, scrollState])
|
||||||
|
useEffect(() => {
|
||||||
|
if (rendererReady) {
|
||||||
|
iframeCommunicator.sendSetWide(wide ?? false)
|
||||||
|
}
|
||||||
|
}, [iframeCommunicator, rendererReady, wide])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (rendererReady) {
|
||||||
|
iframeCommunicator.sendSetBaseUrl(window.location.toString())
|
||||||
|
}
|
||||||
|
}, [iframeCommunicator, rendererReady,])
|
||||||
|
|
||||||
|
const sendToRenderPage = useRef<boolean>(true)
|
||||||
|
|
||||||
|
const onLoad = useCallback(() => {
|
||||||
|
const frame = frameReference.current
|
||||||
|
if (!frame || !frame.contentWindow) {
|
||||||
|
iframeCommunicator.unsetOtherSide()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sendToRenderPage.current) {
|
||||||
|
iframeCommunicator.setOtherSide(frame.contentWindow, rendererOrigin)
|
||||||
|
sendToRenderPage.current = false
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
setRendererReady(false)
|
||||||
|
console.error("Navigated away from unknown URL")
|
||||||
|
frame.src = renderPageUrl
|
||||||
|
sendToRenderPage.current = true
|
||||||
|
}
|
||||||
|
}, [iframeCommunicator, renderPageUrl, rendererOrigin])
|
||||||
|
|
||||||
|
const hideLightbox = useCallback(() => {
|
||||||
|
setLightboxDetails(undefined)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return <Fragment>
|
||||||
|
<ImageLightboxModal show={!!lightboxDetails} onHide={hideLightbox} src={lightboxDetails?.src}
|
||||||
|
alt={lightboxDetails?.alt} title={lightboxDetails?.title}/>
|
||||||
|
<iframe data-cy={'documentIframe'} onLoad={onLoad} title="render" src={renderPageUrl}
|
||||||
|
{...isTestMode() ? {} : { sandbox: 'allow-downloads allow-same-origin allow-scripts allow-popups' }}
|
||||||
|
ref={frameReference} className={`h-100 w-100 border-0 ${extraClasses ?? ''}`}/>
|
||||||
|
</Fragment>
|
||||||
|
}
|
|
@ -5,14 +5,13 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { TocAst } from 'markdown-it-toc-done-right'
|
import { TocAst } from 'markdown-it-toc-done-right'
|
||||||
import React, { RefObject, useRef, useState } from 'react'
|
import React, { MutableRefObject, useCallback, useRef, useState } from 'react'
|
||||||
import { Dropdown } from 'react-bootstrap'
|
import { Dropdown } from 'react-bootstrap'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import useResizeObserver from 'use-resize-observer'
|
import useResizeObserver from 'use-resize-observer'
|
||||||
import { ApplicationState } from '../../../redux'
|
|
||||||
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
||||||
import { ShowIf } from '../../common/show-if/show-if'
|
import { ShowIf } from '../../common/show-if/show-if'
|
||||||
import { FullMarkdownRenderer } from '../../markdown-renderer/full-markdown-renderer'
|
import { FullMarkdownRenderer } from '../../markdown-renderer/full-markdown-renderer'
|
||||||
|
import { ImageClickHandler } from '../../markdown-renderer/replace-components/image/image-replacer'
|
||||||
import { LineMarkerPosition } from '../../markdown-renderer/types'
|
import { LineMarkerPosition } from '../../markdown-renderer/types'
|
||||||
import { TableOfContents } from '../table-of-contents/table-of-contents'
|
import { TableOfContents } from '../table-of-contents/table-of-contents'
|
||||||
import { YAMLMetaData } from '../yaml-metadata/yaml-metadata'
|
import { YAMLMetaData } from '../yaml-metadata/yaml-metadata'
|
||||||
|
@ -21,14 +20,17 @@ import { YamlArrayDeprecationAlert } from './yaml-array-deprecation-alert'
|
||||||
|
|
||||||
export interface DocumentRenderPaneProps {
|
export interface DocumentRenderPaneProps {
|
||||||
extraClasses?: string
|
extraClasses?: string
|
||||||
onFirstHeadingChange: (firstHeading: string | undefined) => void
|
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
||||||
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
|
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
|
||||||
onMetadataChange: (metaData: YAMLMetaData | undefined) => void
|
onMetadataChange?: (metaData: YAMLMetaData | undefined) => void
|
||||||
onMouseEnterRenderer?: () => void
|
onMouseEnterRenderer?: () => void
|
||||||
onScrollRenderer?: () => void
|
onScrollRenderer?: () => void
|
||||||
onTaskCheckedChange: (lineInMarkdown: number, checked: boolean) => void
|
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
||||||
documentRenderPaneRef?: RefObject<HTMLDivElement>
|
documentRenderPaneRef?: MutableRefObject<HTMLDivElement | null>
|
||||||
wide?: boolean
|
wide?: boolean,
|
||||||
|
markdownContent: string,
|
||||||
|
baseUrl?: string
|
||||||
|
onImageClick?: ImageClickHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = (
|
export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = (
|
||||||
|
@ -41,25 +43,34 @@ export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = (
|
||||||
onScrollRenderer,
|
onScrollRenderer,
|
||||||
onTaskCheckedChange,
|
onTaskCheckedChange,
|
||||||
documentRenderPaneRef,
|
documentRenderPaneRef,
|
||||||
wide
|
wide,
|
||||||
|
baseUrl,
|
||||||
|
markdownContent,
|
||||||
|
onImageClick
|
||||||
}) => {
|
}) => {
|
||||||
const [tocAst, setTocAst] = useState<TocAst>()
|
const [tocAst, setTocAst] = useState<TocAst>()
|
||||||
const { width } = useResizeObserver(documentRenderPaneRef ? { ref: documentRenderPaneRef } : undefined)
|
const internalDocumentRenderPaneRef = useRef<HTMLDivElement>()
|
||||||
|
const { width } = useResizeObserver({ ref: internalDocumentRenderPaneRef.current })
|
||||||
const realWidth = width || 0
|
const realWidth = width || 0
|
||||||
const rendererRef = useRef<HTMLDivElement | null>(null)
|
const rendererRef = useRef<HTMLDivElement | null>(null)
|
||||||
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
|
|
||||||
const changeLineMarker = useAdaptedLineMarkerCallback(documentRenderPaneRef, rendererRef, onLineMarkerPositionChanged)
|
const changeLineMarker = useAdaptedLineMarkerCallback(documentRenderPaneRef, rendererRef, onLineMarkerPositionChanged)
|
||||||
|
const setContainerReference = useCallback((instance: HTMLDivElement | null) => {
|
||||||
|
if (documentRenderPaneRef) {
|
||||||
|
documentRenderPaneRef.current = instance || null
|
||||||
|
}
|
||||||
|
internalDocumentRenderPaneRef.current = instance || undefined
|
||||||
|
}, [documentRenderPaneRef])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`bg-light flex-fill pb-5 flex-row d-flex w-100 h-100 ${extraClasses ?? ''}`}
|
<div className={`bg-light m-0 pb-5 row ${extraClasses ?? ''}`}
|
||||||
ref={documentRenderPaneRef} onScroll={onScrollRenderer} onMouseEnter={onMouseEnterRenderer}>
|
ref={setContainerReference} onScroll={onScrollRenderer} onMouseEnter={onMouseEnterRenderer}>
|
||||||
<div className={'col-md'}/>
|
<div className={'col-md d-none d-md-block'}/>
|
||||||
<div className={'bg-light flex-fill'}>
|
<div className={'bg-light col'}>
|
||||||
<YamlArrayDeprecationAlert/>
|
<YamlArrayDeprecationAlert/>
|
||||||
<div>
|
<div>
|
||||||
<FullMarkdownRenderer
|
<FullMarkdownRenderer
|
||||||
rendererRef={rendererRef}
|
rendererRef={rendererRef}
|
||||||
className={'flex-fill mb-3'}
|
className={'flex-fill pt-4 mb-3'}
|
||||||
content={markdownContent}
|
content={markdownContent}
|
||||||
onFirstHeadingChange={onFirstHeadingChange}
|
onFirstHeadingChange={onFirstHeadingChange}
|
||||||
onLineMarkerPositionChanged={changeLineMarker}
|
onLineMarkerPositionChanged={changeLineMarker}
|
||||||
|
@ -67,13 +78,14 @@ export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = (
|
||||||
onTaskCheckedChange={onTaskCheckedChange}
|
onTaskCheckedChange={onTaskCheckedChange}
|
||||||
onTocChange={(tocAst) => setTocAst(tocAst)}
|
onTocChange={(tocAst) => setTocAst(tocAst)}
|
||||||
wide={wide}
|
wide={wide}
|
||||||
/>
|
baseUrl={baseUrl}
|
||||||
|
onImageClick={onImageClick}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={'col-md'}>
|
<div className={'col-md pt-4'}>
|
||||||
<ShowIf condition={realWidth >= 1280 && !!tocAst}>
|
<ShowIf condition={realWidth >= 1280 && !!tocAst}>
|
||||||
<TableOfContents ast={tocAst as TocAst} className={'position-fixed'}/>
|
<TableOfContents ast={tocAst as TocAst} className={'sticky'} baseUrl={baseUrl}/>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
<ShowIf condition={realWidth < 1280 && !!tocAst}>
|
<ShowIf condition={realWidth < 1280 && !!tocAst}>
|
||||||
<div className={'markdown-toc-sidebar-button'}>
|
<div className={'markdown-toc-sidebar-button'}>
|
||||||
|
@ -83,7 +95,7 @@ export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = (
|
||||||
</Dropdown.Toggle>
|
</Dropdown.Toggle>
|
||||||
<Dropdown.Menu>
|
<Dropdown.Menu>
|
||||||
<div className={'p-2'}>
|
<div className={'p-2'}>
|
||||||
<TableOfContents ast={tocAst as TocAst}/>
|
<TableOfContents ast={tocAst as TocAst} baseUrl={baseUrl}/>
|
||||||
</div>
|
</div>
|
||||||
</Dropdown.Menu>
|
</Dropdown.Menu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
|
@ -1,38 +1,48 @@
|
||||||
/*
|
/*
|
||||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useMemo, useRef, useState } from 'react'
|
import React, { useMemo, useRef, useState } from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import { ApplicationState } from '../../../redux'
|
|
||||||
import { LineMarkerPosition } from '../../markdown-renderer/types'
|
import { LineMarkerPosition } from '../../markdown-renderer/types'
|
||||||
|
import { useOnUserScroll } from '../scroll/hooks/use-on-user-scroll'
|
||||||
import { useScrollToLineMark } from '../scroll/hooks/use-scroll-to-line-mark'
|
import { useScrollToLineMark } from '../scroll/hooks/use-scroll-to-line-mark'
|
||||||
import { useUserScroll } from '../scroll/hooks/use-user-scroll'
|
|
||||||
import { ScrollProps } from '../scroll/scroll-props'
|
import { ScrollProps } from '../scroll/scroll-props'
|
||||||
import { DocumentRenderPane, DocumentRenderPaneProps } from './document-render-pane'
|
import { DocumentRenderPane, DocumentRenderPaneProps } from './document-render-pane'
|
||||||
|
|
||||||
export const ScrollingDocumentRenderPane: React.FC<DocumentRenderPaneProps & ScrollProps> = ({
|
type ImplementedProps =
|
||||||
scrollState,
|
'onLineMarkerPositionChanged'
|
||||||
wide,
|
| 'onScrollRenderer'
|
||||||
onFirstHeadingChange,
|
| 'rendererReference'
|
||||||
onMakeScrollSource,
|
| 'onMouseEnterRenderer'
|
||||||
onMetadataChange,
|
|
||||||
onScroll,
|
export type ScrollingDocumentRenderPaneProps = Omit<(DocumentRenderPaneProps & ScrollProps), ImplementedProps>
|
||||||
onTaskCheckedChange
|
|
||||||
}) => {
|
export const ScrollingDocumentRenderPane: React.FC<ScrollingDocumentRenderPaneProps> = (
|
||||||
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
|
{
|
||||||
|
scrollState,
|
||||||
|
wide,
|
||||||
|
onFirstHeadingChange,
|
||||||
|
onMakeScrollSource,
|
||||||
|
onMetadataChange,
|
||||||
|
onScroll,
|
||||||
|
onTaskCheckedChange,
|
||||||
|
markdownContent,
|
||||||
|
extraClasses,
|
||||||
|
baseUrl,
|
||||||
|
onImageClick
|
||||||
|
}) => {
|
||||||
const renderer = useRef<HTMLDivElement>(null)
|
const renderer = useRef<HTMLDivElement>(null)
|
||||||
const [lineMarks, setLineMarks] = useState<LineMarkerPosition[]>()
|
const [lineMarks, setLineMarks] = useState<LineMarkerPosition[]>()
|
||||||
|
|
||||||
const contentLineCount = useMemo(() => markdownContent.split('\n').length, [markdownContent])
|
const contentLineCount = useMemo(() => markdownContent.split('\n').length, [markdownContent])
|
||||||
useScrollToLineMark(scrollState, lineMarks, contentLineCount, renderer)
|
useScrollToLineMark(scrollState, lineMarks, contentLineCount, renderer)
|
||||||
const userScroll = useUserScroll(lineMarks, renderer, onScroll)
|
const userScroll = useOnUserScroll(lineMarks, renderer, onScroll)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DocumentRenderPane
|
<DocumentRenderPane
|
||||||
extraClasses={'overflow-y-scroll'}
|
extraClasses={`overflow-y-scroll h-100 ${extraClasses || ''}`}
|
||||||
documentRenderPaneRef={renderer}
|
documentRenderPaneRef={renderer}
|
||||||
wide={wide}
|
wide={wide}
|
||||||
onFirstHeadingChange={onFirstHeadingChange}
|
onFirstHeadingChange={onFirstHeadingChange}
|
||||||
|
@ -41,6 +51,9 @@ export const ScrollingDocumentRenderPane: React.FC<DocumentRenderPaneProps & Scr
|
||||||
onMouseEnterRenderer={onMakeScrollSource}
|
onMouseEnterRenderer={onMakeScrollSource}
|
||||||
onScrollRenderer={userScroll}
|
onScrollRenderer={userScroll}
|
||||||
onTaskCheckedChange={onTaskCheckedChange}
|
onTaskCheckedChange={onTaskCheckedChange}
|
||||||
|
markdownContent={markdownContent}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
onImageClick={onImageClick}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,14 +8,14 @@ import { RefObject, useCallback } from 'react'
|
||||||
import { LineMarkerPosition } from '../../markdown-renderer/types'
|
import { LineMarkerPosition } from '../../markdown-renderer/types'
|
||||||
|
|
||||||
export const useAdaptedLineMarkerCallback = (documentRenderPaneRef: RefObject<HTMLDivElement> | undefined,
|
export const useAdaptedLineMarkerCallback = (documentRenderPaneRef: RefObject<HTMLDivElement> | undefined,
|
||||||
rendererRef: RefObject<HTMLDivElement | null>,
|
rendererRef: RefObject<HTMLDivElement>,
|
||||||
onLineMarkerPositionChanged: ((lineMarkerPosition: LineMarkerPosition[]) => void) | undefined): ((lineMarkerPosition: LineMarkerPosition[]) => void) => {
|
onLineMarkerPositionChanged: ((lineMarkerPosition: LineMarkerPosition[]) => void) | undefined): ((lineMarkerPosition: LineMarkerPosition[]) => void) => {
|
||||||
return useCallback((linkMarkerPositions) => {
|
return useCallback((linkMarkerPositions) => {
|
||||||
if (!onLineMarkerPositionChanged) {
|
if (!onLineMarkerPositionChanged || !documentRenderPaneRef || !documentRenderPaneRef.current || !rendererRef.current) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const documentRenderPaneTop = (documentRenderPaneRef?.current?.offsetTop ?? 0)
|
const documentRenderPaneTop = (documentRenderPaneRef.current.offsetTop ?? 0)
|
||||||
const rendererTop = (rendererRef.current?.offsetTop ?? 0)
|
const rendererTop = (rendererRef.current.offsetTop ?? 0)
|
||||||
const offset = rendererTop - documentRenderPaneTop
|
const offset = rendererTop - documentRenderPaneTop
|
||||||
onLineMarkerPositionChanged(linkMarkerPositions.map(oldMarker => ({
|
onLineMarkerPositionChanged(linkMarkerPositions.map(oldMarker => ({
|
||||||
line: oldMarker.line,
|
line: oldMarker.line,
|
||||||
|
|
|
@ -18,9 +18,11 @@ export const YamlArrayDeprecationAlert: React.FC = () => {
|
||||||
const yamlDeprecatedTags = useSelector((state: ApplicationState) => state.documentContent.metadata.deprecatedTagsSyntax)
|
const yamlDeprecatedTags = useSelector((state: ApplicationState) => state.documentContent.metadata.deprecatedTagsSyntax)
|
||||||
|
|
||||||
return <ShowIf condition={yamlDeprecatedTags}>
|
return <ShowIf condition={yamlDeprecatedTags}>
|
||||||
<Alert data-cy={'yamlArrayDeprecationAlert'} variant='warning' dir='auto'>
|
<Alert data-cy={'yamlArrayDeprecationAlert'} className={'text-wrap'} variant='warning' dir='auto'>
|
||||||
<span className={'text-wrap'}>
|
<span className={'text-wrap'}>
|
||||||
<Trans i18nKey='editor.deprecatedTags'/>
|
<span className={'text-wrap'}>
|
||||||
|
<Trans i18nKey='editor.deprecatedTags' />
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<br/>
|
<br/>
|
||||||
<TranslatedExternalLink i18nKey={'common.readForMoreInfo'} href={links.faq} className={'text-primary'}/>
|
<TranslatedExternalLink i18nKey={'common.readForMoreInfo'} href={links.faq} className={'text-primary'}/>
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { MotdBanner } from '../common/motd-banner/motd-banner'
|
||||||
import { AppBar, AppBarMode } from './app-bar/app-bar'
|
import { AppBar, AppBarMode } from './app-bar/app-bar'
|
||||||
import { EditorMode } from './app-bar/editor-view-mode'
|
import { EditorMode } from './app-bar/editor-view-mode'
|
||||||
import { DocumentBar } from './document-bar/document-bar'
|
import { DocumentBar } from './document-bar/document-bar'
|
||||||
import { ScrollingDocumentRenderPane } from './document-renderer-pane/scrolling-document-render-pane'
|
import { DocumentIframe } from './document-renderer-pane/document-iframe'
|
||||||
import { EditorPane } from './editor-pane/editor-pane'
|
import { EditorPane } from './editor-pane/editor-pane'
|
||||||
import { editorTestContent } from './editorTestContent'
|
import { editorTestContent } from './editorTestContent'
|
||||||
import { useViewModeShortcuts } from './hooks/useViewModeShortcuts'
|
import { useViewModeShortcuts } from './hooks/useViewModeShortcuts'
|
||||||
|
@ -121,6 +121,10 @@ export const Editor: React.FC = () => {
|
||||||
useApplyDarkMode()
|
useApplyDarkMode()
|
||||||
useDocumentTitle(documentTitle)
|
useDocumentTitle(documentTitle)
|
||||||
|
|
||||||
|
const setRendererToScrollSource = useCallback(() => {
|
||||||
|
scrollSource.current = ScrollSource.RENDERER
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<MotdBanner/>
|
<MotdBanner/>
|
||||||
|
@ -140,16 +144,14 @@ export const Editor: React.FC = () => {
|
||||||
}
|
}
|
||||||
showRight={editorMode === EditorMode.PREVIEW || (editorMode === EditorMode.BOTH)}
|
showRight={editorMode === EditorMode.PREVIEW || (editorMode === EditorMode.BOTH)}
|
||||||
right={
|
right={
|
||||||
<ScrollingDocumentRenderPane
|
<DocumentIframe markdownContent={markdownContent}
|
||||||
onFirstHeadingChange={onFirstHeadingChange}
|
onMakeScrollSource={setRendererToScrollSource}
|
||||||
onMakeScrollSource={() => {
|
onFirstHeadingChange={onFirstHeadingChange}
|
||||||
scrollSource.current = ScrollSource.RENDERER
|
onTaskCheckedChange={onTaskCheckedChange}
|
||||||
}}
|
onMetadataChange={onMetadataChange}
|
||||||
onMetadataChange={onMetadataChange}
|
onScroll={onMarkdownRendererScroll}
|
||||||
onScroll={onMarkdownRendererScroll}
|
wide={editorMode === EditorMode.PREVIEW}
|
||||||
onTaskCheckedChange={onTaskCheckedChange}
|
scrollState={scrollState.rendererScrollState}
|
||||||
scrollState={scrollState.rendererScrollState}
|
|
||||||
wide={editorMode === EditorMode.PREVIEW}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
containerClassName={'overflow-hidden'}/>
|
containerClassName={'overflow-hidden'}/>
|
||||||
|
@ -157,5 +159,4 @@ export const Editor: React.FC = () => {
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Editor
|
export default Editor
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { RefObject, useCallback } from 'react'
|
||||||
import { LineMarkerPosition } from '../../../markdown-renderer/types'
|
import { LineMarkerPosition } from '../../../markdown-renderer/types'
|
||||||
import { ScrollState } from '../scroll-props'
|
import { ScrollState } from '../scroll-props'
|
||||||
|
|
||||||
export const useUserScroll = (lineMarks: LineMarkerPosition[] | undefined, renderer: RefObject<HTMLElement>, onScroll: ((newScrollState: ScrollState) => void)|undefined): () => void =>
|
export const useOnUserScroll = (lineMarks: LineMarkerPosition[] | undefined, renderer: RefObject<HTMLElement>, onScroll: ((newScrollState: ScrollState) => void) | undefined): () => void =>
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
if (!renderer.current || !lineMarks || lineMarks.length === 0 || !onScroll) {
|
if (!renderer.current || !lineMarks || lineMarks.length === 0 || !onScroll) {
|
||||||
return
|
return
|
|
@ -1,10 +1,10 @@
|
||||||
/*
|
/*
|
||||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
import React, { ReactElement, useCallback, useRef, useState } from 'react'
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { ReactElement, useRef, useState } from 'react'
|
|
||||||
import { ShowIf } from '../../common/show-if/show-if'
|
import { ShowIf } from '../../common/show-if/show-if'
|
||||||
import { SplitDivider } from './split-divider/split-divider'
|
import { SplitDivider } from './split-divider/split-divider'
|
||||||
import './splitter.scss'
|
import './splitter.scss'
|
||||||
|
@ -33,28 +33,35 @@ export const Splitter: React.FC<SplitterProps> = ({ containerClassName, left, ri
|
||||||
setSplit(newSize * 100)
|
setSplit(newSize * 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stopResizing = useCallback(() => {
|
||||||
|
setDoResizing(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onMouseMove = useCallback((mouseEvent: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||||
|
if (doResizing) {
|
||||||
|
recalculateSize(mouseEvent.pageX)
|
||||||
|
mouseEvent.preventDefault()
|
||||||
|
}
|
||||||
|
}, [doResizing])
|
||||||
|
|
||||||
|
const onTouchMove = useCallback((touchEvent: React.TouchEvent<HTMLDivElement>) => {
|
||||||
|
if (doResizing) {
|
||||||
|
recalculateSize(touchEvent.touches[0].pageX)
|
||||||
|
touchEvent.preventDefault()
|
||||||
|
}
|
||||||
|
}, [doResizing])
|
||||||
|
|
||||||
|
const onGrab = useCallback(() => setDoResizing(true), [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={splitContainer} className={`flex-fill flex-row d-flex ${containerClassName || ''}`}
|
<div ref={splitContainer} className={`flex-fill flex-row d-flex ${containerClassName || ''}`}
|
||||||
onMouseUp={() => setDoResizing(false)}
|
onMouseUp={stopResizing} onTouchEnd={stopResizing} onMouseMove={onMouseMove} onTouchMove={onTouchMove}>
|
||||||
onTouchEnd={() => setDoResizing(false)}
|
|
||||||
onMouseMove={(mouseEvent) => {
|
|
||||||
if (doResizing) {
|
|
||||||
recalculateSize(mouseEvent.pageX)
|
|
||||||
mouseEvent.preventDefault()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onTouchMove={(touchEvent) => {
|
|
||||||
if (doResizing) {
|
|
||||||
recalculateSize(touchEvent.touches[0].pageX)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={`splitter left ${!showLeft ? 'd-none' : ''}`} style={{ flexBasis: `calc(${realSplit}% - 5px)` }}>
|
<div className={`splitter left ${!showLeft ? 'd-none' : ''}`} style={{ flexBasis: `calc(${realSplit}% - 5px)` }}>
|
||||||
{left}
|
{left}
|
||||||
</div>
|
</div>
|
||||||
<ShowIf condition={showLeft && showRight}>
|
<ShowIf condition={showLeft && showRight}>
|
||||||
<div className='splitter separator'>
|
<div className='splitter separator'>
|
||||||
<SplitDivider onGrab={() => setDoResizing(true)}/>
|
<SplitDivider onGrab={onGrab}/>
|
||||||
</div>
|
</div>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
<div className={`splitter right ${!showRight ? 'd-none' : ''}`}>
|
<div className={`splitter right ${!showRight ? 'd-none' : ''}`}>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/*
|
/*!
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
@ -13,6 +13,7 @@
|
||||||
|
|
||||||
&.sticky {
|
&.sticky {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
> ul > li {
|
> ul > li {
|
||||||
|
|
|
@ -1,26 +1,29 @@
|
||||||
/*
|
/*
|
||||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { TocAst } from 'markdown-it-toc-done-right'
|
||||||
import React, { Fragment, ReactElement, useMemo } from 'react'
|
import React, { Fragment, ReactElement, useMemo } from 'react'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { TocAst } from 'markdown-it-toc-done-right'
|
|
||||||
import { ShowIf } from '../../common/show-if/show-if'
|
import { ShowIf } from '../../common/show-if/show-if'
|
||||||
|
import { createJumpToMarkClickEventHandler } from '../../markdown-renderer/replace-components/link-replacer/link-replacer'
|
||||||
import './table-of-contents.scss'
|
import './table-of-contents.scss'
|
||||||
|
|
||||||
export interface TableOfContentsProps {
|
export interface TableOfContentsProps {
|
||||||
ast: TocAst
|
ast: TocAst
|
||||||
maxDepth?: number
|
maxDepth?: number
|
||||||
className?: string
|
className?: string
|
||||||
|
baseUrl?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const slugify = (content: string): string => {
|
export const slugify = (content: string): string => {
|
||||||
return encodeURIComponent(String(content).trim().toLowerCase().replace(/\s+/g, '-'))
|
return encodeURIComponent(content.trim().toLowerCase().replace(/\s+/g, '-'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const convertLevel = (toc: TocAst, levelsToShowUnderThis: number, headerCounts: Map<string, number>, wrapInListItem: boolean): ReactElement | null => {
|
const convertLevel = (toc: TocAst, levelsToShowUnderThis: number, headerCounts: Map<string, number>,
|
||||||
|
wrapInListItem: boolean, baseUrl?: string): ReactElement | null => {
|
||||||
if (levelsToShowUnderThis < 0) {
|
if (levelsToShowUnderThis < 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -28,19 +31,20 @@ const convertLevel = (toc: TocAst, levelsToShowUnderThis: number, headerCounts:
|
||||||
const rawName = toc.n.trim()
|
const rawName = toc.n.trim()
|
||||||
const nameCount = (headerCounts.get(rawName) ?? -1) + 1
|
const nameCount = (headerCounts.get(rawName) ?? -1) + 1
|
||||||
const slug = `#${slugify(rawName)}${nameCount > 0 ? `-${nameCount}` : ''}`
|
const slug = `#${slugify(rawName)}${nameCount > 0 ? `-${nameCount}` : ''}`
|
||||||
|
const headlineUrl = new URL(slug, baseUrl).toString()
|
||||||
|
|
||||||
headerCounts.set(rawName, nameCount)
|
headerCounts.set(rawName, nameCount)
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<ShowIf condition={toc.l > 0}>
|
<ShowIf condition={toc.l > 0}>
|
||||||
<a href={slug}>{rawName}</a>
|
<a href={headlineUrl} title={rawName} onClick={createJumpToMarkClickEventHandler(slug.substr(1))}>{rawName}</a>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
<ShowIf condition={toc.c.length > 0}>
|
<ShowIf condition={toc.c.length > 0}>
|
||||||
<ul>
|
<ul>
|
||||||
{
|
{
|
||||||
toc.c.map(child =>
|
toc.c.map(child =>
|
||||||
(convertLevel(child, levelsToShowUnderThis - 1, headerCounts, true)))
|
(convertLevel(child, levelsToShowUnderThis - 1, headerCounts, true, baseUrl)))
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
|
@ -49,7 +53,7 @@ const convertLevel = (toc: TocAst, levelsToShowUnderThis: number, headerCounts:
|
||||||
|
|
||||||
if (wrapInListItem) {
|
if (wrapInListItem) {
|
||||||
return (
|
return (
|
||||||
<li key={slug}>
|
<li key={headlineUrl}>
|
||||||
{content}
|
{content}
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
|
@ -58,16 +62,21 @@ const convertLevel = (toc: TocAst, levelsToShowUnderThis: number, headerCounts:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TableOfContents: React.FC<TableOfContentsProps> = ({ ast, maxDepth = 3, className }) => {
|
export const TableOfContents: React.FC<TableOfContentsProps> = ({
|
||||||
|
ast,
|
||||||
|
maxDepth = 3,
|
||||||
|
className,
|
||||||
|
baseUrl
|
||||||
|
}) => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const tocTree = useMemo(() => convertLevel(ast, maxDepth, new Map<string, number>(), false), [ast, maxDepth])
|
const tocTree = useMemo(() => convertLevel(ast, maxDepth, new Map<string, number>(), false, baseUrl), [ast, maxDepth, baseUrl])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`markdown-toc ${className ?? ''}`}>
|
<div className={`markdown-toc ${className ?? ''}`}>
|
||||||
<ShowIf condition={ast.c.length === 0}>
|
<ShowIf condition={ast.c.length === 0}>
|
||||||
<Trans i18nKey={'editor.infoToc'}/>
|
<Trans i18nKey={'editor.infoToc'}/>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
{ tocTree }
|
{tocTree}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -227,5 +227,3 @@ export const HistoryPage: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default HistoryPage
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { TocAst } from 'markdown-it-toc-done-right'
|
import { TocAst } from 'markdown-it-toc-done-right'
|
||||||
import React, { RefObject, useCallback, useMemo, useRef, useState } from 'react'
|
import React, { Ref, useCallback, useMemo, useRef, useState } from 'react'
|
||||||
import { Alert } from 'react-bootstrap'
|
import { Alert } from 'react-bootstrap'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { InternalLink } from '../common/links/internal-link'
|
import { InternalLink } from '../common/links/internal-link'
|
||||||
|
@ -17,6 +17,7 @@ import { usePostMetaDataOnChange } from './hooks/use-post-meta-data-on-change'
|
||||||
import { usePostTocAstOnChange } from './hooks/use-post-toc-ast-on-change'
|
import { usePostTocAstOnChange } from './hooks/use-post-toc-ast-on-change'
|
||||||
import { useReplacerInstanceListCreator } from './hooks/use-replacer-instance-list-creator'
|
import { useReplacerInstanceListCreator } from './hooks/use-replacer-instance-list-creator'
|
||||||
import { FullMarkdownItConfigurator } from './markdown-it-configurator/FullMarkdownItConfigurator'
|
import { FullMarkdownItConfigurator } from './markdown-it-configurator/FullMarkdownItConfigurator'
|
||||||
|
import { ImageClickHandler } from './replace-components/image/image-replacer'
|
||||||
import { LineMarkers } from './replace-components/linemarker/line-number-marker'
|
import { LineMarkers } from './replace-components/linemarker/line-number-marker'
|
||||||
import { AdditionalMarkdownRendererProps, LineMarkerPosition } from './types'
|
import { AdditionalMarkdownRendererProps, LineMarkerPosition } from './types'
|
||||||
import { useCalculateLineMarkerPosition } from './utils/calculate-line-marker-positions'
|
import { useCalculateLineMarkerPosition } from './utils/calculate-line-marker-positions'
|
||||||
|
@ -27,21 +28,26 @@ export interface FullMarkdownRendererProps {
|
||||||
onMetaDataChange?: (yamlMetaData: YAMLMetaData | undefined) => void
|
onMetaDataChange?: (yamlMetaData: YAMLMetaData | undefined) => void
|
||||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
||||||
onTocChange?: (ast: TocAst) => void
|
onTocChange?: (ast: TocAst) => void
|
||||||
rendererRef?: RefObject<HTMLDivElement>
|
rendererRef?: Ref<HTMLDivElement>
|
||||||
|
baseUrl?: string
|
||||||
|
onImageClick?: ImageClickHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & AdditionalMarkdownRendererProps> = ({
|
export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & AdditionalMarkdownRendererProps> = (
|
||||||
onFirstHeadingChange,
|
{
|
||||||
onLineMarkerPositionChanged,
|
onFirstHeadingChange,
|
||||||
onMetaDataChange,
|
onLineMarkerPositionChanged,
|
||||||
onTaskCheckedChange,
|
onMetaDataChange,
|
||||||
onTocChange,
|
onTaskCheckedChange,
|
||||||
content,
|
onTocChange,
|
||||||
className,
|
content,
|
||||||
wide,
|
className,
|
||||||
rendererRef
|
wide,
|
||||||
}) => {
|
rendererRef,
|
||||||
const allReplacers = useReplacerInstanceListCreator(onTaskCheckedChange)
|
baseUrl,
|
||||||
|
onImageClick
|
||||||
|
}) => {
|
||||||
|
const allReplacers = useReplacerInstanceListCreator(onTaskCheckedChange, onImageClick, baseUrl)
|
||||||
useTranslation()
|
useTranslation()
|
||||||
|
|
||||||
const [showYamlError, setShowYamlError] = useState(false)
|
const [showYamlError, setShowYamlError] = useState(false)
|
||||||
|
|
|
@ -16,14 +16,14 @@ export const useConvertMarkdownToReactDom = (
|
||||||
markdownCode: string,
|
markdownCode: string,
|
||||||
markdownIt: MarkdownIt,
|
markdownIt: MarkdownIt,
|
||||||
componentReplacers?: () => ComponentReplacer[],
|
componentReplacers?: () => ComponentReplacer[],
|
||||||
onPreRendering?: () => void,
|
onBeforeRendering?: () => void,
|
||||||
onPostRendering?: () => void): ReactElement[] => {
|
onAfterRendering?: () => void): ReactElement[] => {
|
||||||
const oldMarkdownLineKeys = useRef<LineKeys[]>()
|
const oldMarkdownLineKeys = useRef<LineKeys[]>()
|
||||||
const lastUsedLineId = useRef<number>(0)
|
const lastUsedLineId = useRef<number>(0)
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
if (onPreRendering) {
|
if (onBeforeRendering) {
|
||||||
onPreRendering()
|
onBeforeRendering()
|
||||||
}
|
}
|
||||||
const html = markdownIt.render(markdownCode)
|
const html = markdownIt.render(markdownCode)
|
||||||
const contentLines = markdownCode.split('\n')
|
const contentLines = markdownCode.split('\n')
|
||||||
|
@ -35,9 +35,9 @@ export const useConvertMarkdownToReactDom = (
|
||||||
lastUsedLineId.current = newLastUsedLineId
|
lastUsedLineId.current = newLastUsedLineId
|
||||||
const transformer = componentReplacers ? buildTransformer(newLines, componentReplacers()) : undefined
|
const transformer = componentReplacers ? buildTransformer(newLines, componentReplacers()) : undefined
|
||||||
const rendering = ReactHtmlParser(html, { transform: transformer })
|
const rendering = ReactHtmlParser(html, { transform: transformer })
|
||||||
if (onPostRendering) {
|
if (onAfterRendering) {
|
||||||
onPostRendering()
|
onAfterRendering()
|
||||||
}
|
}
|
||||||
return rendering
|
return rendering
|
||||||
}, [onPreRendering, onPostRendering, markdownCode, markdownIt, componentReplacers])
|
}, [onBeforeRendering, onAfterRendering, markdownCode, markdownIt, componentReplacers])
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,14 +9,14 @@ import { AbcReplacer } from '../replace-components/abc/abc-replacer'
|
||||||
import { AsciinemaReplacer } from '../replace-components/asciinema/asciinema-replacer'
|
import { AsciinemaReplacer } from '../replace-components/asciinema/asciinema-replacer'
|
||||||
import { ComponentReplacer } from '../replace-components/ComponentReplacer'
|
import { ComponentReplacer } from '../replace-components/ComponentReplacer'
|
||||||
import { CsvReplacer } from '../replace-components/csv/csv-replacer'
|
import { CsvReplacer } from '../replace-components/csv/csv-replacer'
|
||||||
import { LinkInNewTabReplacer } from '../replace-components/external-links-in-new-tabs/external-links-in-new-tabs'
|
|
||||||
import { FlowchartReplacer } from '../replace-components/flow/flowchart-replacer'
|
import { FlowchartReplacer } from '../replace-components/flow/flowchart-replacer'
|
||||||
import { GistReplacer } from '../replace-components/gist/gist-replacer'
|
import { GistReplacer } from '../replace-components/gist/gist-replacer'
|
||||||
import { GraphvizReplacer } from '../replace-components/graphviz/graphviz-replacer'
|
import { GraphvizReplacer } from '../replace-components/graphviz/graphviz-replacer'
|
||||||
import { HighlightedCodeReplacer } from '../replace-components/highlighted-fence/highlighted-fence-replacer'
|
import { HighlightedCodeReplacer } from '../replace-components/highlighted-fence/highlighted-fence-replacer'
|
||||||
import { ImageReplacer } from '../replace-components/image/image-replacer'
|
import { ImageClickHandler, ImageReplacer } from '../replace-components/image/image-replacer'
|
||||||
import { KatexReplacer } from '../replace-components/katex/katex-replacer'
|
import { KatexReplacer } from '../replace-components/katex/katex-replacer'
|
||||||
import { LinemarkerReplacer } from '../replace-components/linemarker/linemarker-replacer'
|
import { LinemarkerReplacer } from '../replace-components/linemarker/linemarker-replacer'
|
||||||
|
import { LinkReplacer } from '../replace-components/link-replacer/link-replacer'
|
||||||
import { MarkmapReplacer } from '../replace-components/markmap/markmap-replacer'
|
import { MarkmapReplacer } from '../replace-components/markmap/markmap-replacer'
|
||||||
import { MermaidReplacer } from '../replace-components/mermaid/mermaid-replacer'
|
import { MermaidReplacer } from '../replace-components/mermaid/mermaid-replacer'
|
||||||
import { PdfReplacer } from '../replace-components/pdf/pdf-replacer'
|
import { PdfReplacer } from '../replace-components/pdf/pdf-replacer'
|
||||||
|
@ -28,9 +28,10 @@ import { VegaReplacer } from '../replace-components/vega-lite/vega-replacer'
|
||||||
import { VimeoReplacer } from '../replace-components/vimeo/vimeo-replacer'
|
import { VimeoReplacer } from '../replace-components/vimeo/vimeo-replacer'
|
||||||
import { YoutubeReplacer } from '../replace-components/youtube/youtube-replacer'
|
import { YoutubeReplacer } from '../replace-components/youtube/youtube-replacer'
|
||||||
|
|
||||||
export const useReplacerInstanceListCreator = (onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void): () => ComponentReplacer[] => {
|
export const useReplacerInstanceListCreator = (onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void,
|
||||||
return useMemo(() => () => [
|
onImageClick?: ImageClickHandler, baseUrl?: string): () => ComponentReplacer[] => useMemo(() =>
|
||||||
new LinkInNewTabReplacer(),
|
() => [
|
||||||
|
new LinkReplacer(baseUrl),
|
||||||
new LinemarkerReplacer(),
|
new LinemarkerReplacer(),
|
||||||
new PossibleWiderReplacer(),
|
new PossibleWiderReplacer(),
|
||||||
new GistReplacer(),
|
new GistReplacer(),
|
||||||
|
@ -39,7 +40,7 @@ export const useReplacerInstanceListCreator = (onTaskCheckedChange?: (lineInMark
|
||||||
new AsciinemaReplacer(),
|
new AsciinemaReplacer(),
|
||||||
new AbcReplacer(),
|
new AbcReplacer(),
|
||||||
new PdfReplacer(),
|
new PdfReplacer(),
|
||||||
new ImageReplacer(),
|
new ImageReplacer(onImageClick),
|
||||||
new SequenceDiagramReplacer(),
|
new SequenceDiagramReplacer(),
|
||||||
new CsvReplacer(),
|
new CsvReplacer(),
|
||||||
new FlowchartReplacer(),
|
new FlowchartReplacer(),
|
||||||
|
@ -51,5 +52,4 @@ export const useReplacerInstanceListCreator = (onTaskCheckedChange?: (lineInMark
|
||||||
new QuoteOptionsReplacer(),
|
new QuoteOptionsReplacer(),
|
||||||
new KatexReplacer(),
|
new KatexReplacer(),
|
||||||
new TaskListReplacer(onTaskCheckedChange)
|
new TaskListReplacer(onTaskCheckedChange)
|
||||||
], [onTaskCheckedChange])
|
], [onImageClick, onTaskCheckedChange, baseUrl])
|
||||||
}
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
/*
|
/*
|
||||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import MarkdownIt from 'markdown-it'
|
import MarkdownIt from 'markdown-it'
|
||||||
import { TocAst } from 'markdown-it-toc-done-right'
|
import { TocAst } from 'markdown-it-toc-done-right'
|
||||||
|
@ -45,9 +45,9 @@ export class FullMarkdownItConfigurator extends BasicMarkdownItConfigurator {
|
||||||
!this.useFrontmatter
|
!this.useFrontmatter
|
||||||
? undefined
|
? undefined
|
||||||
: {
|
: {
|
||||||
onYamlError: (hasError: boolean) => this.passYamlErrorState(hasError),
|
onYamlError: (hasError: boolean) => this.passYamlErrorState(hasError),
|
||||||
onRawMeta: (rawMeta: RawYAMLMetadata) => this.onRawMeta(rawMeta)
|
onRawMeta: (rawMeta: RawYAMLMetadata) => this.onRawMeta(rawMeta)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
headlineAnchors,
|
headlineAnchors,
|
||||||
KatexReplacer.markdownItPlugin,
|
KatexReplacer.markdownItPlugin,
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import { DomElement } from 'domhandler'
|
|
||||||
import { ReactElement } from 'react'
|
|
||||||
import { ComponentReplacer, SubNodeTransform } from '../ComponentReplacer'
|
|
||||||
|
|
||||||
export class LinkInNewTabReplacer extends ComponentReplacer {
|
|
||||||
public getReplacement (node: DomElement, subNodeTransform: SubNodeTransform): (ReactElement | null | undefined) {
|
|
||||||
const isJumpMark = node.attribs?.href?.substr(0, 1) === '#'
|
|
||||||
|
|
||||||
if (node.name !== 'a' || isJumpMark) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
return <a className={node.attribs?.class} title={node.attribs?.title} href={node.attribs?.href} rel='noopener noreferrer' target='_blank'>
|
|
||||||
{
|
|
||||||
node.children?.map((child, index) => subNodeTransform(child, index))
|
|
||||||
}
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +1,8 @@
|
||||||
/*
|
/*
|
||||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { Fragment, ReactElement, useEffect, useState } from 'react'
|
import React, { Fragment, ReactElement, useEffect, useState } from 'react'
|
||||||
import ReactHtmlParser from 'react-html-parser'
|
import ReactHtmlParser from 'react-html-parser'
|
||||||
|
@ -59,7 +59,7 @@ export const HighlightedCode: React.FC<HighlightedCodeProps> = ({ code, language
|
||||||
{ dom }
|
{ dom }
|
||||||
</code>
|
</code>
|
||||||
<div className={'text-right button-inside'}>
|
<div className={'text-right button-inside'}>
|
||||||
<CopyToClipboardButton content={code}/>
|
<CopyToClipboardButton content={code} data-cy="copy-code-button"/>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>)
|
</Fragment>)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import React from 'react'
|
||||||
import { Modal } from 'react-bootstrap'
|
import { Modal } from 'react-bootstrap'
|
||||||
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
|
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
|
||||||
import "./lightbox.scss"
|
import "./lightbox.scss"
|
||||||
|
import { ProxyImageFrame } from './proxy-image-frame'
|
||||||
|
|
||||||
export interface ImageLightboxModalProps {
|
export interface ImageLightboxModalProps {
|
||||||
show: boolean
|
show: boolean
|
||||||
|
@ -33,7 +34,7 @@ export const ImageLightboxModal: React.FC<ImageLightboxModalProps> = ({ show, on
|
||||||
<span>{alt ?? title ?? ''}</span>
|
<span>{alt ?? title ?? ''}</span>
|
||||||
</Modal.Title>
|
</Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<img alt={alt} src={src} title={title} className={'w-100 cursor-zoom-out'} onClick={onHide}/>
|
<ProxyImageFrame alt={alt} src={src} title={title} className={'w-100 cursor-zoom-out'} onClick={onHide}/>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,17 +9,27 @@ import React from 'react'
|
||||||
import { ComponentReplacer } from '../ComponentReplacer'
|
import { ComponentReplacer } from '../ComponentReplacer'
|
||||||
import { ProxyImageFrame } from './proxy-image-frame'
|
import { ProxyImageFrame } from './proxy-image-frame'
|
||||||
|
|
||||||
|
export type ImageClickHandler = (event: React.MouseEvent<HTMLImageElement, MouseEvent>) => void;
|
||||||
|
|
||||||
export class ImageReplacer extends ComponentReplacer {
|
export class ImageReplacer extends ComponentReplacer {
|
||||||
|
private readonly clickHandler?: ImageClickHandler
|
||||||
|
|
||||||
|
constructor (clickHandler?: ImageClickHandler) {
|
||||||
|
super()
|
||||||
|
this.clickHandler = clickHandler
|
||||||
|
}
|
||||||
|
|
||||||
public getReplacement (node: DomElement): React.ReactElement | undefined {
|
public getReplacement (node: DomElement): React.ReactElement | undefined {
|
||||||
if (node.name === 'img' && node.attribs) {
|
if (node.name === 'img' && node.attribs) {
|
||||||
return <ProxyImageFrame
|
return <ProxyImageFrame
|
||||||
id={node.attribs.id}
|
id={node.attribs.id}
|
||||||
className={node.attribs.class}
|
className={`${node.attribs.class} cursor-zoom-in`}
|
||||||
src={node.attribs.src}
|
src={node.attribs.src}
|
||||||
alt={node.attribs.alt}
|
alt={node.attribs.alt}
|
||||||
title={node.attribs.title}
|
title={node.attribs.title}
|
||||||
width={node.attribs.width}
|
width={node.attribs.width}
|
||||||
height={node.attribs.height}
|
height={node.attribs.height}
|
||||||
|
onClick={this.clickHandler}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { Fragment, useState } from 'react'
|
|
||||||
import { ImageLightboxModal } from './image-lightbox-modal'
|
|
||||||
import "./lightbox.scss"
|
|
||||||
|
|
||||||
export const LightboxImageFrame: React.FC<React.ImgHTMLAttributes<HTMLImageElement>> = (
|
|
||||||
{
|
|
||||||
alt,
|
|
||||||
title,
|
|
||||||
src,
|
|
||||||
...props
|
|
||||||
}) => {
|
|
||||||
const [showFullscreenImage, setShowFullscreenImage] = useState(false)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<img alt={alt} src={src} title={title} {...props} className={'cursor-zoom-in'}
|
|
||||||
onClick={() => setShowFullscreenImage(true)}/>
|
|
||||||
<ImageLightboxModal
|
|
||||||
show={showFullscreenImage}
|
|
||||||
onHide={() => setShowFullscreenImage(false)} title={title} src={src} alt={alt}/>
|
|
||||||
</Fragment>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -8,7 +8,6 @@ import React, { useEffect, useState } from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { getProxiedUrl } from '../../../../api/media'
|
import { getProxiedUrl } from '../../../../api/media'
|
||||||
import { ApplicationState } from '../../../../redux'
|
import { ApplicationState } from '../../../../redux'
|
||||||
import { LightboxImageFrame } from './lightbox-image-frame'
|
|
||||||
|
|
||||||
export const ProxyImageFrame: React.FC<React.ImgHTMLAttributes<HTMLImageElement>> = (
|
export const ProxyImageFrame: React.FC<React.ImgHTMLAttributes<HTMLImageElement>> = (
|
||||||
{
|
{
|
||||||
|
@ -29,13 +28,6 @@ export const ProxyImageFrame: React.FC<React.ImgHTMLAttributes<HTMLImageElement>
|
||||||
.catch(err => console.error(err))
|
.catch(err => console.error(err))
|
||||||
}, [imageProxyEnabled, src])
|
}, [imageProxyEnabled, src])
|
||||||
|
|
||||||
if (imageProxyEnabled) {
|
return <img src={imageProxyEnabled ? imageUrl : (src ?? '')} title={title ?? alt ?? ''} alt={alt} {...props}/>
|
||||||
return (
|
|
||||||
<LightboxImageFrame src={imageUrl} title={title ?? alt ?? ''} alt={alt} {...props}/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LightboxImageFrame src={src ?? ''} title={title ?? alt ?? ''} alt={alt} {...props}/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { DomElement } from 'domhandler'
|
||||||
|
import React, { ReactElement } from 'react'
|
||||||
|
import { ComponentReplacer, NativeRenderer, SubNodeTransform } from '../ComponentReplacer'
|
||||||
|
|
||||||
|
export const createJumpToMarkClickEventHandler = (id: string) => {
|
||||||
|
return (event: React.MouseEvent<HTMLElement, MouseEvent>): void => {
|
||||||
|
document.getElementById(id)?.scrollIntoView()
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LinkReplacer extends ComponentReplacer {
|
||||||
|
constructor (private baseUrl?: string) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
public getReplacement (node: DomElement, subNodeTransform: SubNodeTransform, nativeRenderer: NativeRenderer): (ReactElement | null | undefined) {
|
||||||
|
if (node.name !== 'a' || !node.attribs || !node.attribs.href) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = node.attribs.href
|
||||||
|
const isJumpMark = url.substr(0, 1) === '#'
|
||||||
|
|
||||||
|
const id = url.substr(1)
|
||||||
|
|
||||||
|
try {
|
||||||
|
node.attribs.href = new URL(url, this.baseUrl).toString()
|
||||||
|
} catch (e) {
|
||||||
|
node.attribs.href = url
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isJumpMark) {
|
||||||
|
return <span onClick={createJumpToMarkClickEventHandler(id)}>
|
||||||
|
{nativeRenderer()}
|
||||||
|
</span>
|
||||||
|
} else {
|
||||||
|
node.attribs.rel = "noreferer noopener"
|
||||||
|
node.attribs.target = "_blank"
|
||||||
|
return nativeRenderer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,7 +15,7 @@ export interface TextDifferenceResult {
|
||||||
lastUsedLineId: number
|
lastUsedLineId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const calculateKeyFromLineMarker = (node: DomElement, lineKeys?: LineKeys[]): string|undefined => {
|
export const calculateKeyFromLineMarker = (node: DomElement, lineKeys?: LineKeys[]): string | undefined => {
|
||||||
if (!node.attribs || lineKeys === undefined) {
|
if (!node.attribs || lineKeys === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@ export const renderNativeNode = (node: DomElement, key: string, transform: Trans
|
||||||
return convertNodeToElement(node, key as unknown as number, transform)
|
return convertNodeToElement(node, key as unknown as number, transform)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildTransformer = (lineKeys: (LineKeys[] | undefined), allReplacers: ComponentReplacer[]):Transform => {
|
export const buildTransformer = (lineKeys: (LineKeys[] | undefined), allReplacers: ComponentReplacer[]): Transform => {
|
||||||
const transform: Transform = (node, index) => {
|
const transform: Transform = (node, index) => {
|
||||||
const nativeRenderer: NativeRenderer = () => renderNativeNode(node, key, transform)
|
const nativeRenderer: NativeRenderer = () => renderNativeNode(node, key, transform)
|
||||||
const subNodeTransform: SubNodeTransform = (subNode, subIndex) => transform(subNode, subIndex, transform)
|
const subNodeTransform: SubNodeTransform = (subNode, subIndex) => transform(subNode, subIndex, transform)
|
||||||
|
|
|
@ -1,22 +1,24 @@
|
||||||
/*
|
/*
|
||||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { Alert } from 'react-bootstrap'
|
import { Alert } from 'react-bootstrap'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
import { useParams } from 'react-router'
|
import { useParams } from 'react-router'
|
||||||
import { getNote, Note } from '../../api/notes'
|
import { getNote, Note } from '../../api/notes'
|
||||||
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
|
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
|
||||||
import { useDocumentTitle } from '../../hooks/common/use-document-title'
|
import { useDocumentTitle } from '../../hooks/common/use-document-title'
|
||||||
|
import { ApplicationState } from '../../redux'
|
||||||
import { setDocumentContent, setDocumentMetadata } from '../../redux/document-content/methods'
|
import { setDocumentContent, setDocumentMetadata } from '../../redux/document-content/methods'
|
||||||
import { extractNoteTitle } from '../common/document-title/note-title-extractor'
|
import { extractNoteTitle } from '../common/document-title/note-title-extractor'
|
||||||
import { MotdBanner } from '../common/motd-banner/motd-banner'
|
import { MotdBanner } from '../common/motd-banner/motd-banner'
|
||||||
import { ShowIf } from '../common/show-if/show-if'
|
import { ShowIf } from '../common/show-if/show-if'
|
||||||
import { AppBar, AppBarMode } from '../editor/app-bar/app-bar'
|
import { AppBar, AppBarMode } from '../editor/app-bar/app-bar'
|
||||||
import { DocumentRenderPane } from '../editor/document-renderer-pane/document-render-pane'
|
import { DocumentIframe } from '../editor/document-renderer-pane/document-iframe'
|
||||||
import { EditorPathParams } from '../editor/editor'
|
import { EditorPathParams } from '../editor/editor'
|
||||||
import { YAMLMetaData } from '../editor/yaml-metadata/yaml-metadata'
|
import { YAMLMetaData } from '../editor/yaml-metadata/yaml-metadata'
|
||||||
import { DocumentInfobar } from './document-infobar'
|
import { DocumentInfobar } from './document-infobar'
|
||||||
|
@ -60,6 +62,7 @@ export const PadViewOnly: React.FC = () => {
|
||||||
|
|
||||||
useApplyDarkMode()
|
useApplyDarkMode()
|
||||||
useDocumentTitle(documentTitle)
|
useDocumentTitle(documentTitle)
|
||||||
|
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'d-flex flex-column mvh-100 bg-light'}>
|
<div className={'d-flex flex-column mvh-100 bg-light'}>
|
||||||
|
@ -80,7 +83,7 @@ export const PadViewOnly: React.FC = () => {
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
</div>
|
</div>
|
||||||
<ShowIf condition={!error && !loading}>
|
<ShowIf condition={!error && !loading}>
|
||||||
{ /* TODO set editable and created author properly */ }
|
{ /* TODO set editable and created author properly */}
|
||||||
<DocumentInfobar
|
<DocumentInfobar
|
||||||
changedAuthor={noteData?.lastChange.userId ?? ''}
|
changedAuthor={noteData?.lastChange.userId ?? ''}
|
||||||
changedTime={noteData?.lastChange.timestamp ?? 0}
|
changedTime={noteData?.lastChange.timestamp ?? 0}
|
||||||
|
@ -90,11 +93,10 @@ export const PadViewOnly: React.FC = () => {
|
||||||
noteId={id}
|
noteId={id}
|
||||||
viewCount={noteData?.viewcount ?? 0}
|
viewCount={noteData?.viewcount ?? 0}
|
||||||
/>
|
/>
|
||||||
<DocumentRenderPane
|
<DocumentIframe extraClasses={"flex-fill"}
|
||||||
onFirstHeadingChange={onFirstHeadingChange}
|
markdownContent={markdownContent}
|
||||||
onMetadataChange={onMetadataChange}
|
onFirstHeadingChange={onFirstHeadingChange}
|
||||||
onTaskCheckedChange={() => false}
|
onMetadataChange={onMetadataChange}/>
|
||||||
/>
|
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
43
src/components/render-page/iframe-communicator.ts
Normal file
43
src/components/render-page/iframe-communicator.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export abstract class IframeCommunicator<SEND, RECEIVE> {
|
||||||
|
protected otherSide?: Window
|
||||||
|
protected otherOrigin?: string
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
window.addEventListener("message", this.handleEvent.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
public unregisterEventListener (): void {
|
||||||
|
window.removeEventListener("message", this.handleEvent.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
public setOtherSide (otherSide: Window, otherOrigin: string): void {
|
||||||
|
this.otherSide = otherSide
|
||||||
|
this.otherOrigin = otherOrigin
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsetOtherSide (): void {
|
||||||
|
this.otherSide = undefined
|
||||||
|
this.otherOrigin = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
public getOtherSide (): Window | undefined {
|
||||||
|
return this.otherSide
|
||||||
|
}
|
||||||
|
|
||||||
|
protected sendMessageToOtherSide (message: SEND): void {
|
||||||
|
if (this.otherSide === undefined || this.otherOrigin === undefined) {
|
||||||
|
console.error("Can't send message because otherSide is null", message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.otherSide.postMessage(message, this.otherOrigin)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract handleEvent (event: MessageEvent<RECEIVE>): void;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ScrollState } from "../editor/scroll/scroll-props"
|
||||||
|
import { YAMLMetaData } from "../editor/yaml-metadata/yaml-metadata"
|
||||||
|
import { IframeCommunicator } from "./iframe-communicator"
|
||||||
|
import {
|
||||||
|
EditorToRendererIframeMessage,
|
||||||
|
ImageDetails,
|
||||||
|
RendererToEditorIframeMessage,
|
||||||
|
RenderIframeMessageType
|
||||||
|
} from "./rendering-message"
|
||||||
|
|
||||||
|
export class IframeEditorToRendererCommunicator extends IframeCommunicator<EditorToRendererIframeMessage, RendererToEditorIframeMessage> {
|
||||||
|
private onSetScrollSourceToRendererHandler?: () => void
|
||||||
|
private onTaskCheckboxChangeHandler?: (lineInMarkdown: number, checked: boolean) => void
|
||||||
|
private onFirstHeadingChangeHandler?: (heading?: string) => void
|
||||||
|
private onMetaDataChangeHandler?: (metaData?: YAMLMetaData) => void
|
||||||
|
private onSetScrollStateHandler?: (scrollState: ScrollState) => void
|
||||||
|
private onRendererReadyHandler?: () => void
|
||||||
|
private onImageClickedHandler?: (details: ImageDetails) => void
|
||||||
|
|
||||||
|
protected handleEvent (event: MessageEvent<RendererToEditorIframeMessage>): boolean | undefined {
|
||||||
|
const renderMessage = event.data
|
||||||
|
switch (renderMessage.type) {
|
||||||
|
case RenderIframeMessageType.RENDERER_READY:
|
||||||
|
this.onRendererReadyHandler?.()
|
||||||
|
return false
|
||||||
|
case RenderIframeMessageType.SET_SCROLL_SOURCE_TO_RENDERER:
|
||||||
|
this.onSetScrollSourceToRendererHandler?.()
|
||||||
|
return false
|
||||||
|
case RenderIframeMessageType.SET_SCROLL_STATE:
|
||||||
|
this.onSetScrollStateHandler?.(renderMessage.scrollState)
|
||||||
|
return false
|
||||||
|
case RenderIframeMessageType.ON_FIRST_HEADING_CHANGE:
|
||||||
|
this.onFirstHeadingChangeHandler?.(renderMessage.firstHeading)
|
||||||
|
return false
|
||||||
|
case RenderIframeMessageType.ON_TASK_CHECKBOX_CHANGE:
|
||||||
|
this.onTaskCheckboxChangeHandler?.(renderMessage.lineInMarkdown, renderMessage.checked)
|
||||||
|
return false
|
||||||
|
case RenderIframeMessageType.ON_SET_META_DATA:
|
||||||
|
this.onMetaDataChangeHandler?.(renderMessage.metaData)
|
||||||
|
return false
|
||||||
|
case RenderIframeMessageType.IMAGE_CLICKED:
|
||||||
|
this.onImageClickedHandler?.(renderMessage.details)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public onImageClicked (handler?: (details: ImageDetails) => void): void {
|
||||||
|
this.onImageClickedHandler = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
public onRendererReady (handler?: () => void): void {
|
||||||
|
this.onRendererReadyHandler = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
public onSetScrollSourceToRenderer (handler?: () => void): void {
|
||||||
|
this.onSetScrollSourceToRendererHandler = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
public onTaskCheckboxChange (handler?: (lineInMarkdown: number, checked: boolean) => void): void {
|
||||||
|
this.onTaskCheckboxChangeHandler = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
public onFirstHeadingChange (handler?: (heading?: string) => void): void {
|
||||||
|
this.onFirstHeadingChangeHandler = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
public onMetaDataChange (handler?: (metaData?: YAMLMetaData) => void): void {
|
||||||
|
this.onMetaDataChangeHandler = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
public onSetScrollState (handler?: (scrollState: ScrollState) => void): void {
|
||||||
|
this.onSetScrollStateHandler = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendSetBaseUrl (baseUrl: string): void {
|
||||||
|
this.sendMessageToOtherSide({
|
||||||
|
type: RenderIframeMessageType.SET_BASE_URL,
|
||||||
|
baseUrl
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendSetMarkdownContent (markdownContent: string): void {
|
||||||
|
this.sendMessageToOtherSide({
|
||||||
|
type: RenderIframeMessageType.SET_MARKDOWN_CONTENT,
|
||||||
|
content: markdownContent
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendSetDarkmode (darkModeActivated: boolean): void {
|
||||||
|
this.sendMessageToOtherSide({
|
||||||
|
type: RenderIframeMessageType.SET_DARKMODE,
|
||||||
|
activated: darkModeActivated
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendSetWide (isWide: boolean): void {
|
||||||
|
this.sendMessageToOtherSide({
|
||||||
|
type: RenderIframeMessageType.SET_WIDE,
|
||||||
|
activated: isWide
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendScrollState (scrollState?: ScrollState): void {
|
||||||
|
if (!scrollState) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.sendMessageToOtherSide({
|
||||||
|
type: RenderIframeMessageType.SET_SCROLL_STATE,
|
||||||
|
scrollState
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ScrollState } from "../editor/scroll/scroll-props"
|
||||||
|
import { YAMLMetaData } from "../editor/yaml-metadata/yaml-metadata"
|
||||||
|
import { IframeCommunicator } from "./iframe-communicator"
|
||||||
|
import {
|
||||||
|
EditorToRendererIframeMessage,
|
||||||
|
ImageDetails,
|
||||||
|
RendererToEditorIframeMessage,
|
||||||
|
RenderIframeMessageType
|
||||||
|
} from "./rendering-message"
|
||||||
|
|
||||||
|
export class IframeRendererToEditorCommunicator extends IframeCommunicator<RendererToEditorIframeMessage, EditorToRendererIframeMessage> {
|
||||||
|
private onSetMarkdownContentHandler?: ((markdownContent: string) => void)
|
||||||
|
private onSetDarkModeHandler?: ((darkModeActivated: boolean) => void)
|
||||||
|
private onSetWideHandler?: ((wide: boolean) => void)
|
||||||
|
private onSetScrollStateHandler?: ((scrollState: ScrollState) => void)
|
||||||
|
private onSetBaseUrlHandler?: ((baseUrl: string) => void)
|
||||||
|
|
||||||
|
public onSetBaseUrl (handler?: (baseUrl: string) => void): void {
|
||||||
|
this.onSetBaseUrlHandler = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
public onSetMarkdownContent (handler?: (markdownContent: string) => void): void {
|
||||||
|
this.onSetMarkdownContentHandler = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
public onSetDarkMode (handler?: (darkModeActivated: boolean) => void): void {
|
||||||
|
this.onSetDarkModeHandler = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
public onSetWide (handler?: (wide: boolean) => void): void {
|
||||||
|
this.onSetWideHandler = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
public onSetScrollState (handler?: (scrollState: ScrollState) => void): void {
|
||||||
|
this.onSetScrollStateHandler = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendRendererReady (): void {
|
||||||
|
this.sendMessageToOtherSide({
|
||||||
|
type: RenderIframeMessageType.RENDERER_READY
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendTaskCheckBoxChange (lineInMarkdown: number, checked: boolean): void {
|
||||||
|
this.sendMessageToOtherSide({
|
||||||
|
type: RenderIframeMessageType.ON_TASK_CHECKBOX_CHANGE,
|
||||||
|
checked,
|
||||||
|
lineInMarkdown
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendFirstHeadingChanged (firstHeading: string | undefined): void {
|
||||||
|
this.sendMessageToOtherSide({
|
||||||
|
type: RenderIframeMessageType.ON_FIRST_HEADING_CHANGE,
|
||||||
|
firstHeading
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendSetScrollSourceToRenderer (): void {
|
||||||
|
this.sendMessageToOtherSide({
|
||||||
|
type: RenderIframeMessageType.SET_SCROLL_SOURCE_TO_RENDERER
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendSetMetaData (metaData: YAMLMetaData | undefined): void {
|
||||||
|
this.sendMessageToOtherSide({
|
||||||
|
type: RenderIframeMessageType.ON_SET_META_DATA,
|
||||||
|
metaData
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendSetScrollState (scrollState: ScrollState): void {
|
||||||
|
this.sendMessageToOtherSide({
|
||||||
|
type: RenderIframeMessageType.SET_SCROLL_STATE,
|
||||||
|
scrollState
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected handleEvent (event: MessageEvent<EditorToRendererIframeMessage>): boolean | undefined {
|
||||||
|
const renderMessage = event.data
|
||||||
|
switch (renderMessage.type) {
|
||||||
|
case RenderIframeMessageType.SET_MARKDOWN_CONTENT:
|
||||||
|
this.onSetMarkdownContentHandler?.(renderMessage.content)
|
||||||
|
return false
|
||||||
|
case RenderIframeMessageType.SET_DARKMODE:
|
||||||
|
this.onSetDarkModeHandler?.(renderMessage.activated)
|
||||||
|
return false
|
||||||
|
case RenderIframeMessageType.SET_WIDE:
|
||||||
|
this.onSetWideHandler?.(renderMessage.activated)
|
||||||
|
return false
|
||||||
|
case RenderIframeMessageType.SET_SCROLL_STATE:
|
||||||
|
this.onSetScrollStateHandler?.(renderMessage.scrollState)
|
||||||
|
return false
|
||||||
|
case RenderIframeMessageType.SET_BASE_URL:
|
||||||
|
this.onSetBaseUrlHandler?.(renderMessage.baseUrl)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendClickedImageUrl (details: ImageDetails): void {
|
||||||
|
this.sendMessageToOtherSide({
|
||||||
|
type: RenderIframeMessageType.IMAGE_CLICKED,
|
||||||
|
details: details
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
105
src/components/render-page/render-page.tsx
Normal file
105
src/components/render-page/render-page.tsx
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import equal from "fast-deep-equal"
|
||||||
|
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
|
||||||
|
import { ApplicationState } from '../../redux'
|
||||||
|
import { setDarkMode } from '../../redux/dark-mode/methods'
|
||||||
|
import { setDocumentMetadata } from '../../redux/document-content/methods'
|
||||||
|
import { ScrollingDocumentRenderPane } from '../editor/document-renderer-pane/scrolling-document-render-pane'
|
||||||
|
import { ScrollState } from '../editor/scroll/scroll-props'
|
||||||
|
import { YAMLMetaData } from '../editor/yaml-metadata/yaml-metadata'
|
||||||
|
import { ImageClickHandler } from '../markdown-renderer/replace-components/image/image-replacer'
|
||||||
|
import { IframeRendererToEditorCommunicator } from './iframe-renderer-to-editor-communicator'
|
||||||
|
|
||||||
|
export const RenderPage: React.FC = () => {
|
||||||
|
useApplyDarkMode()
|
||||||
|
|
||||||
|
const [markdownContent, setMarkdownContent] = useState('')
|
||||||
|
const [isWide, setWide] = useState(false)
|
||||||
|
const [scrollState, setScrollState] = useState<ScrollState>({ firstLineInView: 1, scrolledPercentage: 0 })
|
||||||
|
const [baseUrl, setBaseUrl] = useState<string>()
|
||||||
|
|
||||||
|
const editorOrigin = useSelector((state: ApplicationState) => state.config.iframeCommunication.editorOrigin)
|
||||||
|
|
||||||
|
const iframeCommunicator = useMemo(() => {
|
||||||
|
const newCommunicator = new IframeRendererToEditorCommunicator()
|
||||||
|
newCommunicator.setOtherSide(window.parent, editorOrigin)
|
||||||
|
return newCommunicator
|
||||||
|
}, [editorOrigin])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
iframeCommunicator.sendRendererReady()
|
||||||
|
return () => iframeCommunicator.unregisterEventListener()
|
||||||
|
}, [iframeCommunicator])
|
||||||
|
|
||||||
|
useEffect(() => iframeCommunicator.onSetBaseUrl(setBaseUrl), [iframeCommunicator])
|
||||||
|
useEffect(() => iframeCommunicator.onSetMarkdownContent(setMarkdownContent), [iframeCommunicator])
|
||||||
|
useEffect(() => iframeCommunicator.onSetDarkMode(setDarkMode), [iframeCommunicator])
|
||||||
|
useEffect(() => iframeCommunicator.onSetWide(setWide), [iframeCommunicator])
|
||||||
|
useEffect(() => iframeCommunicator.onSetScrollState((newScrollState) => {
|
||||||
|
if (!equal(scrollState, newScrollState)) {
|
||||||
|
setScrollState(newScrollState)
|
||||||
|
}
|
||||||
|
}), [iframeCommunicator, scrollState])
|
||||||
|
|
||||||
|
const onTaskCheckedChange = useCallback((lineInMarkdown: number, checked: boolean) => {
|
||||||
|
iframeCommunicator.sendTaskCheckBoxChange(lineInMarkdown, checked)
|
||||||
|
}, [iframeCommunicator])
|
||||||
|
|
||||||
|
const onFirstHeadingChange = useCallback((firstHeading?: string) => {
|
||||||
|
iframeCommunicator.sendFirstHeadingChanged(firstHeading)
|
||||||
|
}, [iframeCommunicator])
|
||||||
|
|
||||||
|
const onMakeScrollSource = useCallback(() => {
|
||||||
|
iframeCommunicator.sendSetScrollSourceToRenderer()
|
||||||
|
}, [iframeCommunicator])
|
||||||
|
|
||||||
|
const onMetaDataChange = useCallback((metaData?: YAMLMetaData) => {
|
||||||
|
setDocumentMetadata(metaData)
|
||||||
|
iframeCommunicator.sendSetMetaData(metaData)
|
||||||
|
}, [iframeCommunicator])
|
||||||
|
|
||||||
|
const onScroll = useCallback((scrollState: ScrollState) => {
|
||||||
|
iframeCommunicator.sendSetScrollState(scrollState)
|
||||||
|
}, [iframeCommunicator])
|
||||||
|
|
||||||
|
const onImageClick: ImageClickHandler = useCallback((event) => {
|
||||||
|
const image = event.target as HTMLImageElement
|
||||||
|
if (image.src === '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
iframeCommunicator.sendClickedImageUrl({
|
||||||
|
src: image.src,
|
||||||
|
alt: image.alt,
|
||||||
|
title: image.title
|
||||||
|
})
|
||||||
|
}, [iframeCommunicator])
|
||||||
|
|
||||||
|
if (!baseUrl) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={"vh-100 w-100"}>
|
||||||
|
<ScrollingDocumentRenderPane
|
||||||
|
extraClasses={'w-100'}
|
||||||
|
markdownContent={markdownContent}
|
||||||
|
wide={isWide}
|
||||||
|
onTaskCheckedChange={onTaskCheckedChange}
|
||||||
|
onFirstHeadingChange={onFirstHeadingChange}
|
||||||
|
onMakeScrollSource={onMakeScrollSource}
|
||||||
|
onMetadataChange={onMetaDataChange}
|
||||||
|
scrollState={scrollState}
|
||||||
|
onScroll={onScroll}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
onImageClick={onImageClick}/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RenderPage
|
92
src/components/render-page/rendering-message.ts
Normal file
92
src/components/render-page/rendering-message.ts
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { ScrollState } from '../editor/scroll/scroll-props'
|
||||||
|
import { YAMLMetaData } from '../editor/yaml-metadata/yaml-metadata'
|
||||||
|
|
||||||
|
export enum RenderIframeMessageType {
|
||||||
|
SET_MARKDOWN_CONTENT = 'SET_MARKDOWN_CONTENT',
|
||||||
|
RENDERER_READY = 'RENDERER_READY',
|
||||||
|
SET_DARKMODE = 'SET_DARKMODE',
|
||||||
|
SET_WIDE = 'SET_WIDE',
|
||||||
|
ON_TASK_CHECKBOX_CHANGE = 'ON_TASK_CHECKBOX_CHANGE',
|
||||||
|
ON_FIRST_HEADING_CHANGE = 'ON_FIRST_HEADING_CHANGE',
|
||||||
|
SET_SCROLL_SOURCE_TO_RENDERER = 'SET_SCROLL_SOURCE_TO_RENDERER',
|
||||||
|
SET_SCROLL_STATE = 'SET_SCROLL_STATE',
|
||||||
|
ON_SET_META_DATA = 'ON_SET_META_DATA',
|
||||||
|
IMAGE_CLICKED = 'IMAGE_CLICKED',
|
||||||
|
SET_BASE_URL = 'SET_BASE_URL'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RendererToEditorSimpleMessage {
|
||||||
|
type: RenderIframeMessageType.RENDERER_READY | RenderIframeMessageType.SET_SCROLL_SOURCE_TO_RENDERER
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetDarkModeMessage {
|
||||||
|
type: RenderIframeMessageType.SET_DARKMODE,
|
||||||
|
activated: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImageDetails {
|
||||||
|
alt?: string
|
||||||
|
src: string
|
||||||
|
title?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetBaseUrlMessage {
|
||||||
|
type: RenderIframeMessageType.SET_BASE_URL,
|
||||||
|
baseUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImageClickedMessage {
|
||||||
|
type: RenderIframeMessageType.IMAGE_CLICKED,
|
||||||
|
details: ImageDetails
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetWideMessage {
|
||||||
|
type: RenderIframeMessageType.SET_WIDE,
|
||||||
|
activated: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetMarkdownContentMessage {
|
||||||
|
type: RenderIframeMessageType.SET_MARKDOWN_CONTENT,
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetScrollStateMessage {
|
||||||
|
type: RenderIframeMessageType.SET_SCROLL_STATE,
|
||||||
|
scrollState: ScrollState
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OnTaskCheckboxChangeMessage {
|
||||||
|
type: RenderIframeMessageType.ON_TASK_CHECKBOX_CHANGE,
|
||||||
|
lineInMarkdown: number,
|
||||||
|
checked: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OnFirstHeadingChangeMessage {
|
||||||
|
type: RenderIframeMessageType.ON_FIRST_HEADING_CHANGE,
|
||||||
|
firstHeading: string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OnMetadataChangeMessage {
|
||||||
|
type: RenderIframeMessageType.ON_SET_META_DATA,
|
||||||
|
metaData: YAMLMetaData | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EditorToRendererIframeMessage =
|
||||||
|
SetMarkdownContentMessage |
|
||||||
|
SetDarkModeMessage |
|
||||||
|
SetWideMessage |
|
||||||
|
SetScrollStateMessage |
|
||||||
|
SetBaseUrlMessage
|
||||||
|
|
||||||
|
export type RendererToEditorIframeMessage =
|
||||||
|
RendererToEditorSimpleMessage |
|
||||||
|
OnFirstHeadingChangeMessage |
|
||||||
|
OnTaskCheckboxChangeMessage |
|
||||||
|
OnMetadataChangeMessage |
|
||||||
|
SetScrollStateMessage |
|
||||||
|
ImageClickedMessage
|
|
@ -26,6 +26,7 @@ import './style/index.scss'
|
||||||
import { isTestMode } from './utils/is-test-mode'
|
import { isTestMode } from './utils/is-test-mode'
|
||||||
|
|
||||||
const Editor = React.lazy(() => import(/* webpackPrefetch: true */ './components/editor/editor'))
|
const Editor = React.lazy(() => import(/* webpackPrefetch: true */ './components/editor/editor'))
|
||||||
|
const RenderPage = React.lazy(() => import (/* webpackPrefetch: true */ './components/render-page/render-page'))
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
|
@ -58,6 +59,9 @@ ReactDOM.render(
|
||||||
<ProfilePage/>
|
<ProfilePage/>
|
||||||
</LandingLayout>
|
</LandingLayout>
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="/render">
|
||||||
|
<RenderPage/>
|
||||||
|
</Route>
|
||||||
<Route path="/n/:id">
|
<Route path="/n/:id">
|
||||||
<Editor/>
|
<Editor/>
|
||||||
</Route>
|
</Route>
|
||||||
|
@ -82,10 +86,11 @@ ReactDOM.render(
|
||||||
)
|
)
|
||||||
|
|
||||||
if (isTestMode()) {
|
if (isTestMode()) {
|
||||||
console.log("This build runs in test mode. This means:\n - No default content in the editor")
|
console.log("This build runs in test mode. This means:\n - No default content in the editor\n - no sandboxed iframe")
|
||||||
}
|
}
|
||||||
|
|
||||||
// If you want your app to work offline and load faster, you can change
|
// If you want your app to work offline and load faster, you can change
|
||||||
// unregister() to register() below. Note this comes with some pitfalls.
|
// unregister() to register() below. Note this comes with some pitfalls.
|
||||||
// Learn more about service workers: https://bit.ly/CRA-PWA
|
// Learn more about service workers: https://bit.ly/CRA-PWA
|
||||||
serviceWorkerRegistration.unregister()
|
serviceWorkerRegistration.unregister()
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,10 @@ export const initialState: Config = {
|
||||||
version: '',
|
version: '',
|
||||||
sourceCodeUrl: '',
|
sourceCodeUrl: '',
|
||||||
issueTrackerUrl: ''
|
issueTrackerUrl: ''
|
||||||
|
},
|
||||||
|
iframeCommunication: {
|
||||||
|
editorOrigin: '',
|
||||||
|
rendererOrigin: ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,10 @@
|
||||||
color: $black;
|
color: $black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: darken($dark, 8%);
|
background-color: darken($dark, 8%);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue