mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-29 03:04:21 -05: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.
|
||||
- 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).
|
||||
- Improved security by wrapping the markdown rendering into an iframe
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
|
@ -26,7 +26,8 @@ describe('Autocompletion', () => {
|
|||
.should('have.text', '```abnf')
|
||||
cy.get('.CodeMirror-code > div:nth-of-type(3) > .CodeMirror-line > span span')
|
||||
.should('have.text', '```')
|
||||
cy.get('.markdown-body > pre > code')
|
||||
cy.getMarkdownBody()
|
||||
.find('pre > code')
|
||||
.should('exist')
|
||||
})
|
||||
it('via doubleclick', () => {
|
||||
|
@ -40,7 +41,8 @@ describe('Autocompletion', () => {
|
|||
.should('have.text', '```abnf')
|
||||
cy.get('.CodeMirror-code > div:nth-of-type(3) > .CodeMirror-line > span span')
|
||||
.should('have.text', '```')
|
||||
cy.get('.markdown-body > pre > code')
|
||||
cy.getMarkdownBody()
|
||||
.find('pre > code')
|
||||
.should('exist')
|
||||
})
|
||||
})
|
||||
|
@ -58,7 +60,8 @@ describe('Autocompletion', () => {
|
|||
.should('have.text', ':::success')
|
||||
cy.get('.CodeMirror-code > div:nth-of-type(3) > .CodeMirror-line > span span')
|
||||
.should('have.text', '::: ')
|
||||
cy.get('.markdown-body > div.alert')
|
||||
cy.getMarkdownBody()
|
||||
.find('div.alert')
|
||||
.should('exist')
|
||||
})
|
||||
it('via doubleclick', () => {
|
||||
|
@ -72,7 +75,8 @@ describe('Autocompletion', () => {
|
|||
.should('have.text', ':::success')
|
||||
cy.get('.CodeMirror-code > div:nth-of-type(3) > .CodeMirror-line > span span')
|
||||
.should('have.text', '::: ')
|
||||
cy.get('.markdown-body > div.alert')
|
||||
cy.getMarkdownBody()
|
||||
.find('div.alert')
|
||||
.should('exist')
|
||||
})
|
||||
})
|
||||
|
@ -80,8 +84,7 @@ describe('Autocompletion', () => {
|
|||
describe('emoji', () => {
|
||||
describe('normal emoji', () => {
|
||||
it('via Enter', () => {
|
||||
cy.get('@codeinput')
|
||||
.fill(':hedg')
|
||||
cy.codemirrorFill(':hedg')
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('exist')
|
||||
cy.get('@codeinput')
|
||||
|
@ -90,12 +93,11 @@ describe('Autocompletion', () => {
|
|||
.should('not.exist')
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', ':hedgehog:')
|
||||
cy.get('.markdown-body')
|
||||
cy.getMarkdownBody()
|
||||
.should('have.text', '🦔')
|
||||
})
|
||||
it('via doubleclick', () => {
|
||||
cy.get('@codeinput')
|
||||
.fill(':hedg')
|
||||
cy.codemirrorFill(':hedg')
|
||||
cy.get('.CodeMirror-hints > li')
|
||||
.first()
|
||||
.dblclick()
|
||||
|
@ -103,15 +105,14 @@ describe('Autocompletion', () => {
|
|||
.should('not.exist')
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', ':hedgehog:')
|
||||
cy.get('.markdown-body')
|
||||
cy.getMarkdownBody()
|
||||
.should('have.text', '🦔')
|
||||
})
|
||||
})
|
||||
|
||||
describe('fork-awesome-icon', () => {
|
||||
it('via Enter', () => {
|
||||
cy.get('@codeinput')
|
||||
.fill(':fa-face')
|
||||
cy.codemirrorFill(':fa-face')
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('exist')
|
||||
cy.get('@codeinput')
|
||||
|
@ -120,12 +121,12 @@ describe('Autocompletion', () => {
|
|||
.should('not.exist')
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', ':fa-facebook:')
|
||||
cy.get('.markdown-body > p > i.fa.fa-facebook')
|
||||
cy.getMarkdownBody()
|
||||
.find('p > i.fa.fa-facebook')
|
||||
.should('exist')
|
||||
})
|
||||
it('via doubleclick', () => {
|
||||
cy.get('@codeinput')
|
||||
.fill(':fa-face')
|
||||
cy.codemirrorFill(':fa-face')
|
||||
cy.get('.CodeMirror-hints > li')
|
||||
.first()
|
||||
.dblclick()
|
||||
|
@ -133,7 +134,8 @@ describe('Autocompletion', () => {
|
|||
.should('not.exist')
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', ':fa-facebook:')
|
||||
cy.get('.markdown-body > p > i.fa.fa-facebook')
|
||||
cy.getMarkdownBody()
|
||||
.find('p > i.fa.fa-facebook')
|
||||
.should('exist')
|
||||
})
|
||||
})
|
||||
|
@ -150,7 +152,8 @@ describe('Autocompletion', () => {
|
|||
.should('not.exist')
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', '# ')
|
||||
cy.get('.markdown-body > h1 ')
|
||||
cy.getMarkdownBody()
|
||||
.find('h1 ')
|
||||
.should('have.text', ' ')
|
||||
})
|
||||
it('via doubleclick', () => {
|
||||
|
@ -162,7 +165,8 @@ describe('Autocompletion', () => {
|
|||
.should('not.exist')
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', '# ')
|
||||
cy.get('.markdown-body > h1')
|
||||
cy.getMarkdownBody()
|
||||
.find('h1')
|
||||
.should('have.text', ' ')
|
||||
})
|
||||
})
|
||||
|
@ -178,7 +182,8 @@ describe('Autocompletion', () => {
|
|||
.should('not.exist')
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', '![image alt](https:// "title")')
|
||||
cy.get('.markdown-body > p > img')
|
||||
cy.getMarkdownBody()
|
||||
.find('p > img')
|
||||
.should('have.attr', 'alt', 'image alt')
|
||||
.should('have.attr', 'src', 'https://')
|
||||
.should('have.attr', 'title', 'title')
|
||||
|
@ -192,7 +197,8 @@ describe('Autocompletion', () => {
|
|||
.should('not.exist')
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', '![image alt](https:// "title")')
|
||||
cy.get('.markdown-body > p > img')
|
||||
cy.getMarkdownBody()
|
||||
.find('p > img')
|
||||
.should('have.attr', 'alt', 'image alt')
|
||||
.should('have.attr', 'src', 'https://')
|
||||
.should('have.attr', 'title', 'title')
|
||||
|
@ -210,7 +216,8 @@ describe('Autocompletion', () => {
|
|||
.should('not.exist')
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.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.attr', 'href', 'https://')
|
||||
.should('have.attr', 'title', 'title')
|
||||
|
@ -224,7 +231,8 @@ describe('Autocompletion', () => {
|
|||
.should('not.exist')
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.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.attr', 'href', 'https://')
|
||||
.should('have.attr', 'title', 'title')
|
||||
|
@ -242,7 +250,8 @@ describe('Autocompletion', () => {
|
|||
.should('not.exist')
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', '{%pdf https:// %}')
|
||||
cy.get('.markdown-body > p')
|
||||
cy.getMarkdownBody()
|
||||
.find('p')
|
||||
.should('exist')
|
||||
})
|
||||
it('via doubleclick', () => {
|
||||
|
@ -254,7 +263,8 @@ describe('Autocompletion', () => {
|
|||
.should('not.exist')
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', '{%pdf https:// %}')
|
||||
cy.get('.markdown-body > p')
|
||||
cy.getMarkdownBody()
|
||||
.find('p')
|
||||
.should('exist')
|
||||
})
|
||||
})
|
||||
|
@ -270,7 +280,8 @@ describe('Autocompletion', () => {
|
|||
.should('not.exist')
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.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')
|
||||
})
|
||||
it('via doubleclick', () => {
|
||||
|
@ -282,7 +293,8 @@ describe('Autocompletion', () => {
|
|||
.should('not.exist')
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', '</details>')
|
||||
cy.get('.markdown-body > details')
|
||||
cy.getMarkdownBody()
|
||||
.find('details')
|
||||
.should('exist')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -87,7 +87,8 @@ describe('Document Title', () => {
|
|||
|
||||
it('katex code looks right', () => {
|
||||
cy.codemirrorFill(`# $\\alpha$-foo`)
|
||||
cy.get('.markdown-body > h1')
|
||||
cy.getMarkdownRenderer()
|
||||
.find('h1')
|
||||
.should('contain', 'α')
|
||||
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.
|
||||
|
|
|
@ -4,33 +4,37 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
const findHljsCodeBlock = () => {
|
||||
return cy.getMarkdownBody()
|
||||
.find('pre > code.hljs')
|
||||
.should('be.visible')
|
||||
}
|
||||
|
||||
describe('Code', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/n/test', {
|
||||
onBeforeLoad (win: Window): void {
|
||||
cy.spy(win.navigator.clipboard, 'writeText').as('copy')
|
||||
}
|
||||
})
|
||||
cy.visit('/n/test')
|
||||
})
|
||||
|
||||
describe('with just the language', () => {
|
||||
it('doesn\'t show a gutter', () => {
|
||||
cy.codemirrorFill('```javascript \nlet x = 0\n```')
|
||||
cy.get('.markdown-body > pre > code.hljs')
|
||||
.should('be.visible')
|
||||
findHljsCodeBlock()
|
||||
.should('not.have.class', 'showGutter')
|
||||
cy.get('.markdown-body > pre > code.hljs > .linenumber')
|
||||
|
||||
findHljsCodeBlock()
|
||||
.find('.linenumber')
|
||||
.should('not.be.visible')
|
||||
})
|
||||
|
||||
describe('and line wrapping', () => {
|
||||
it('doesn\'t show a gutter', () => {
|
||||
cy.codemirrorFill('```javascript! \nlet x = 0\n```')
|
||||
cy.get('.markdown-body > pre > code.hljs')
|
||||
.should('be.visible')
|
||||
findHljsCodeBlock()
|
||||
.should('not.have.class', 'showGutter')
|
||||
.should('have.class', 'wrapLines')
|
||||
cy.get('.markdown-body > pre > code.hljs > .linenumber')
|
||||
|
||||
findHljsCodeBlock()
|
||||
.find('.linenumber')
|
||||
.should('not.be.visible')
|
||||
})
|
||||
})
|
||||
|
@ -39,10 +43,11 @@ describe('Code', () => {
|
|||
describe('with the language and show gutter', () => {
|
||||
it('shows the correct line number', () => {
|
||||
cy.codemirrorFill('```javascript= \nlet x = 0\n```')
|
||||
cy.get('.markdown-body > pre > code.hljs')
|
||||
.should('be.visible')
|
||||
findHljsCodeBlock()
|
||||
.should('have.class', 'showGutter')
|
||||
cy.get('.markdown-body > pre > code.hljs > .linenumber')
|
||||
|
||||
findHljsCodeBlock()
|
||||
.find('.linenumber')
|
||||
.should('be.visible')
|
||||
.text()
|
||||
.should('eq', '1')
|
||||
|
@ -51,11 +56,12 @@ describe('Code', () => {
|
|||
describe('and line wrapping', () => {
|
||||
it('shows the correct line number', () => {
|
||||
cy.codemirrorFill('```javascript=! \nlet x = 0\n```')
|
||||
cy.get('.markdown-body > pre > code.hljs')
|
||||
.should('be.visible')
|
||||
findHljsCodeBlock()
|
||||
.should('have.class', 'showGutter')
|
||||
.should('have.class', 'wrapLines')
|
||||
cy.get('.markdown-body > pre > code.hljs > .linenumber')
|
||||
|
||||
findHljsCodeBlock()
|
||||
.find('.linenumber')
|
||||
.should('be.visible')
|
||||
.text()
|
||||
.should('eq', '1')
|
||||
|
@ -66,10 +72,11 @@ describe('Code', () => {
|
|||
describe('with the language, show gutter with a start number', () => {
|
||||
it('shows the correct line number', () => {
|
||||
cy.codemirrorFill('```javascript=100 \nlet x = 0\n```')
|
||||
cy.get('.markdown-body > pre > code.hljs')
|
||||
.should('be.visible')
|
||||
findHljsCodeBlock()
|
||||
.should('have.class', 'showGutter')
|
||||
cy.get('.markdown-body > pre > code.hljs > .linenumber')
|
||||
|
||||
findHljsCodeBlock()
|
||||
.find('.linenumber')
|
||||
.should('be.visible')
|
||||
.text()
|
||||
.should('eq', '100')
|
||||
|
@ -77,8 +84,7 @@ describe('Code', () => {
|
|||
|
||||
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.get('.markdown-body > pre > code.hljs')
|
||||
.should('be.visible')
|
||||
findHljsCodeBlock()
|
||||
.should('have.class', 'showGutter')
|
||||
.first()
|
||||
.find('.linenumber')
|
||||
|
@ -86,14 +92,14 @@ describe('Code', () => {
|
|||
.should('be.visible')
|
||||
.text()
|
||||
.should('eq', '100')
|
||||
cy.get('.markdown-body > pre > code.hljs')
|
||||
findHljsCodeBlock()
|
||||
.first()
|
||||
.find('.linenumber')
|
||||
.last()
|
||||
.should('be.visible')
|
||||
.text()
|
||||
.should('eq', '101')
|
||||
cy.get('.markdown-body > pre > code.hljs')
|
||||
findHljsCodeBlock()
|
||||
.last()
|
||||
.find('.linenumber')
|
||||
.first()
|
||||
|
@ -105,11 +111,11 @@ describe('Code', () => {
|
|||
describe('and line wrapping', () => {
|
||||
it('shows the correct line number', () => {
|
||||
cy.codemirrorFill('```javascript=100! \nlet x = 0\n```')
|
||||
cy.get('.markdown-body > pre > code.hljs')
|
||||
.should('be.visible')
|
||||
findHljsCodeBlock()
|
||||
.should('have.class', 'showGutter')
|
||||
.should('have.class', 'wrapLines')
|
||||
cy.get('.markdown-body > pre > code.hljs > .linenumber')
|
||||
findHljsCodeBlock()
|
||||
.find('.linenumber')
|
||||
.should('be.visible')
|
||||
.text()
|
||||
.should('eq', '100')
|
||||
|
@ -117,8 +123,7 @@ describe('Code', () => {
|
|||
|
||||
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.get('.markdown-body > pre > code.hljs')
|
||||
.should('be.visible')
|
||||
findHljsCodeBlock()
|
||||
.should('have.class', 'showGutter')
|
||||
.should('have.class', 'wrapLines')
|
||||
.first()
|
||||
|
@ -127,14 +132,14 @@ describe('Code', () => {
|
|||
.should('be.visible')
|
||||
.text()
|
||||
.should('eq', '100')
|
||||
cy.get('.markdown-body > pre > code.hljs')
|
||||
findHljsCodeBlock()
|
||||
.first()
|
||||
.find('.linenumber')
|
||||
.last()
|
||||
.should('be.visible')
|
||||
.text()
|
||||
.should('eq', '101')
|
||||
cy.get('.markdown-body > pre > code.hljs')
|
||||
findHljsCodeBlock()
|
||||
.last()
|
||||
.find('.linenumber')
|
||||
.first()
|
||||
|
@ -147,9 +152,22 @@ describe('Code', () => {
|
|||
|
||||
it('has a working copy button', () => {
|
||||
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()
|
||||
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.get('[data-cy="limitReachedModal"]')
|
||||
.should('be.visible')
|
||||
cy.get('[data-cy="limitReachedMessage"]')
|
||||
cy.getMarkdownRenderer()
|
||||
.find('[data-cy="limitReachedMessage"]')
|
||||
.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', () => {
|
||||
cy.codemirrorFill('---\ntags: a, b, c\n---')
|
||||
cy.get('[data-cy="yamlArrayDeprecationAlert"]')
|
||||
cy.getMarkdownRenderer()
|
||||
.find('[data-cy="yamlArrayDeprecationAlert"]')
|
||||
.should('be.visible')
|
||||
})
|
||||
|
||||
it('isn\'t shown when using inline yaml-array', () => {
|
||||
cy.codemirrorFill('---\ntags: [\'a\', \'b\', \'c\']\n---')
|
||||
cy.get('[data-cy="yamlArrayDeprecationAlert"]')
|
||||
cy.getMarkdownRenderer()
|
||||
.find('[data-cy="yamlArrayDeprecationAlert"]')
|
||||
.should('not.exist')
|
||||
})
|
||||
|
||||
it('isn\'t shown when using multi line yaml-array', () => {
|
||||
cy.codemirrorFill('---\ntags:\n - a\n - b\n - c\n---')
|
||||
cy.get('[data-cy="yamlArrayDeprecationAlert"]')
|
||||
cy.getMarkdownRenderer()
|
||||
.find('[data-cy="yamlArrayDeprecationAlert"]')
|
||||
.should('not.exist')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -49,6 +49,10 @@ beforeEach(() => {
|
|||
version: 'mock',
|
||||
sourceCodeUrl: '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 './config'
|
||||
import './fill'
|
||||
import './getMarkdownRenderer'
|
||||
import './login'
|
||||
|
|
|
@ -39,5 +39,9 @@
|
|||
"version": "mock",
|
||||
"sourceCodeUrl": "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,
|
||||
plantumlServer: string | null,
|
||||
maxDocumentLength: number,
|
||||
iframeCommunication: iframeCommunicationConfig
|
||||
}
|
||||
|
||||
export interface iframeCommunicationConfig {
|
||||
editorOrigin: string,
|
||||
rendererOrigin: string
|
||||
}
|
||||
|
||||
export interface BrandingConfig {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useRef } from 'react'
|
||||
import { Button } from 'react-bootstrap'
|
||||
|
@ -15,15 +15,22 @@ export interface CopyToClipboardButtonProps {
|
|||
content: string
|
||||
size?: 'sm' | 'lg'
|
||||
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 button = useRef<HTMLButtonElement>(null)
|
||||
|
||||
return (
|
||||
<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'/>
|
||||
</Button>
|
||||
<CopyOverlay content={content} clickComponent={button}/>
|
||||
|
|
|
@ -6,8 +6,12 @@
|
|||
|
||||
export const download = (data: BlobPart, fileName: string, mimeType: string): void => {
|
||||
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')
|
||||
helperElement.href = URL.createObjectURL(file)
|
||||
helperElement.href = url
|
||||
helperElement.download = fileName
|
||||
document.body.appendChild(helperElement)
|
||||
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 React, { RefObject, useRef, useState } from 'react'
|
||||
import React, { MutableRefObject, useCallback, useRef, useState } from 'react'
|
||||
import { Dropdown } from 'react-bootstrap'
|
||||
import { useSelector } from 'react-redux'
|
||||
import useResizeObserver from 'use-resize-observer'
|
||||
import { ApplicationState } from '../../../redux'
|
||||
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
||||
import { ShowIf } from '../../common/show-if/show-if'
|
||||
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 { TableOfContents } from '../table-of-contents/table-of-contents'
|
||||
import { YAMLMetaData } from '../yaml-metadata/yaml-metadata'
|
||||
|
@ -21,14 +20,17 @@ import { YamlArrayDeprecationAlert } from './yaml-array-deprecation-alert'
|
|||
|
||||
export interface DocumentRenderPaneProps {
|
||||
extraClasses?: string
|
||||
onFirstHeadingChange: (firstHeading: string | undefined) => void
|
||||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
||||
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
|
||||
onMetadataChange: (metaData: YAMLMetaData | undefined) => void
|
||||
onMetadataChange?: (metaData: YAMLMetaData | undefined) => void
|
||||
onMouseEnterRenderer?: () => void
|
||||
onScrollRenderer?: () => void
|
||||
onTaskCheckedChange: (lineInMarkdown: number, checked: boolean) => void
|
||||
documentRenderPaneRef?: RefObject<HTMLDivElement>
|
||||
wide?: boolean
|
||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
||||
documentRenderPaneRef?: MutableRefObject<HTMLDivElement | null>
|
||||
wide?: boolean,
|
||||
markdownContent: string,
|
||||
baseUrl?: string
|
||||
onImageClick?: ImageClickHandler
|
||||
}
|
||||
|
||||
export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = (
|
||||
|
@ -41,25 +43,34 @@ export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = (
|
|||
onScrollRenderer,
|
||||
onTaskCheckedChange,
|
||||
documentRenderPaneRef,
|
||||
wide
|
||||
wide,
|
||||
baseUrl,
|
||||
markdownContent,
|
||||
onImageClick
|
||||
}) => {
|
||||
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 rendererRef = useRef<HTMLDivElement | null>(null)
|
||||
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
|
||||
const changeLineMarker = useAdaptedLineMarkerCallback(documentRenderPaneRef, rendererRef, onLineMarkerPositionChanged)
|
||||
const setContainerReference = useCallback((instance: HTMLDivElement | null) => {
|
||||
if (documentRenderPaneRef) {
|
||||
documentRenderPaneRef.current = instance || null
|
||||
}
|
||||
internalDocumentRenderPaneRef.current = instance || undefined
|
||||
}, [documentRenderPaneRef])
|
||||
|
||||
return (
|
||||
<div className={`bg-light flex-fill pb-5 flex-row d-flex w-100 h-100 ${extraClasses ?? ''}`}
|
||||
ref={documentRenderPaneRef} onScroll={onScrollRenderer} onMouseEnter={onMouseEnterRenderer}>
|
||||
<div className={'col-md'}/>
|
||||
<div className={'bg-light flex-fill'}>
|
||||
<div className={`bg-light m-0 pb-5 row ${extraClasses ?? ''}`}
|
||||
ref={setContainerReference} onScroll={onScrollRenderer} onMouseEnter={onMouseEnterRenderer}>
|
||||
<div className={'col-md d-none d-md-block'}/>
|
||||
<div className={'bg-light col'}>
|
||||
<YamlArrayDeprecationAlert/>
|
||||
<div>
|
||||
<FullMarkdownRenderer
|
||||
rendererRef={rendererRef}
|
||||
className={'flex-fill mb-3'}
|
||||
className={'flex-fill pt-4 mb-3'}
|
||||
content={markdownContent}
|
||||
onFirstHeadingChange={onFirstHeadingChange}
|
||||
onLineMarkerPositionChanged={changeLineMarker}
|
||||
|
@ -67,13 +78,14 @@ export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = (
|
|||
onTaskCheckedChange={onTaskCheckedChange}
|
||||
onTocChange={(tocAst) => setTocAst(tocAst)}
|
||||
wide={wide}
|
||||
/>
|
||||
baseUrl={baseUrl}
|
||||
onImageClick={onImageClick}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={'col-md'}>
|
||||
<div className={'col-md pt-4'}>
|
||||
<ShowIf condition={realWidth >= 1280 && !!tocAst}>
|
||||
<TableOfContents ast={tocAst as TocAst} className={'position-fixed'}/>
|
||||
<TableOfContents ast={tocAst as TocAst} className={'sticky'} baseUrl={baseUrl}/>
|
||||
</ShowIf>
|
||||
<ShowIf condition={realWidth < 1280 && !!tocAst}>
|
||||
<div className={'markdown-toc-sidebar-button'}>
|
||||
|
@ -83,7 +95,7 @@ export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = (
|
|||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<div className={'p-2'}>
|
||||
<TableOfContents ast={tocAst as TocAst}/>
|
||||
<TableOfContents ast={tocAst as TocAst} baseUrl={baseUrl}/>
|
||||
</div>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
|
|
|
@ -1,38 +1,48 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { useMemo, useRef, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { ApplicationState } from '../../../redux'
|
||||
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 { useUserScroll } from '../scroll/hooks/use-user-scroll'
|
||||
import { ScrollProps } from '../scroll/scroll-props'
|
||||
import { DocumentRenderPane, DocumentRenderPaneProps } from './document-render-pane'
|
||||
|
||||
export const ScrollingDocumentRenderPane: React.FC<DocumentRenderPaneProps & ScrollProps> = ({
|
||||
scrollState,
|
||||
wide,
|
||||
onFirstHeadingChange,
|
||||
onMakeScrollSource,
|
||||
onMetadataChange,
|
||||
onScroll,
|
||||
onTaskCheckedChange
|
||||
}) => {
|
||||
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
|
||||
type ImplementedProps =
|
||||
'onLineMarkerPositionChanged'
|
||||
| 'onScrollRenderer'
|
||||
| 'rendererReference'
|
||||
| 'onMouseEnterRenderer'
|
||||
|
||||
export type ScrollingDocumentRenderPaneProps = Omit<(DocumentRenderPaneProps & ScrollProps), ImplementedProps>
|
||||
|
||||
export const ScrollingDocumentRenderPane: React.FC<ScrollingDocumentRenderPaneProps> = (
|
||||
{
|
||||
scrollState,
|
||||
wide,
|
||||
onFirstHeadingChange,
|
||||
onMakeScrollSource,
|
||||
onMetadataChange,
|
||||
onScroll,
|
||||
onTaskCheckedChange,
|
||||
markdownContent,
|
||||
extraClasses,
|
||||
baseUrl,
|
||||
onImageClick
|
||||
}) => {
|
||||
const renderer = useRef<HTMLDivElement>(null)
|
||||
const [lineMarks, setLineMarks] = useState<LineMarkerPosition[]>()
|
||||
|
||||
const contentLineCount = useMemo(() => markdownContent.split('\n').length, [markdownContent])
|
||||
useScrollToLineMark(scrollState, lineMarks, contentLineCount, renderer)
|
||||
const userScroll = useUserScroll(lineMarks, renderer, onScroll)
|
||||
const userScroll = useOnUserScroll(lineMarks, renderer, onScroll)
|
||||
|
||||
return (
|
||||
<DocumentRenderPane
|
||||
extraClasses={'overflow-y-scroll'}
|
||||
extraClasses={`overflow-y-scroll h-100 ${extraClasses || ''}`}
|
||||
documentRenderPaneRef={renderer}
|
||||
wide={wide}
|
||||
onFirstHeadingChange={onFirstHeadingChange}
|
||||
|
@ -41,6 +51,9 @@ export const ScrollingDocumentRenderPane: React.FC<DocumentRenderPaneProps & Scr
|
|||
onMouseEnterRenderer={onMakeScrollSource}
|
||||
onScrollRenderer={userScroll}
|
||||
onTaskCheckedChange={onTaskCheckedChange}
|
||||
markdownContent={markdownContent}
|
||||
baseUrl={baseUrl}
|
||||
onImageClick={onImageClick}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -8,14 +8,14 @@ import { RefObject, useCallback } from 'react'
|
|||
import { LineMarkerPosition } from '../../markdown-renderer/types'
|
||||
|
||||
export const useAdaptedLineMarkerCallback = (documentRenderPaneRef: RefObject<HTMLDivElement> | undefined,
|
||||
rendererRef: RefObject<HTMLDivElement | null>,
|
||||
rendererRef: RefObject<HTMLDivElement>,
|
||||
onLineMarkerPositionChanged: ((lineMarkerPosition: LineMarkerPosition[]) => void) | undefined): ((lineMarkerPosition: LineMarkerPosition[]) => void) => {
|
||||
return useCallback((linkMarkerPositions) => {
|
||||
if (!onLineMarkerPositionChanged) {
|
||||
if (!onLineMarkerPositionChanged || !documentRenderPaneRef || !documentRenderPaneRef.current || !rendererRef.current) {
|
||||
return
|
||||
}
|
||||
const documentRenderPaneTop = (documentRenderPaneRef?.current?.offsetTop ?? 0)
|
||||
const rendererTop = (rendererRef.current?.offsetTop ?? 0)
|
||||
const documentRenderPaneTop = (documentRenderPaneRef.current.offsetTop ?? 0)
|
||||
const rendererTop = (rendererRef.current.offsetTop ?? 0)
|
||||
const offset = rendererTop - documentRenderPaneTop
|
||||
onLineMarkerPositionChanged(linkMarkerPositions.map(oldMarker => ({
|
||||
line: oldMarker.line,
|
||||
|
|
|
@ -18,9 +18,11 @@ export const YamlArrayDeprecationAlert: React.FC = () => {
|
|||
const yamlDeprecatedTags = useSelector((state: ApplicationState) => state.documentContent.metadata.deprecatedTagsSyntax)
|
||||
|
||||
return <ShowIf condition={yamlDeprecatedTags}>
|
||||
<Alert data-cy={'yamlArrayDeprecationAlert'} variant='warning' dir='auto'>
|
||||
<span className={'text-wrap'}>
|
||||
<Trans i18nKey='editor.deprecatedTags'/>
|
||||
<Alert data-cy={'yamlArrayDeprecationAlert'} className={'text-wrap'} variant='warning' dir='auto'>
|
||||
<span className={'text-wrap'}>
|
||||
<span className={'text-wrap'}>
|
||||
<Trans i18nKey='editor.deprecatedTags' />
|
||||
</span>
|
||||
</span>
|
||||
<br/>
|
||||
<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 { EditorMode } from './app-bar/editor-view-mode'
|
||||
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 { editorTestContent } from './editorTestContent'
|
||||
import { useViewModeShortcuts } from './hooks/useViewModeShortcuts'
|
||||
|
@ -121,6 +121,10 @@ export const Editor: React.FC = () => {
|
|||
useApplyDarkMode()
|
||||
useDocumentTitle(documentTitle)
|
||||
|
||||
const setRendererToScrollSource = useCallback(() => {
|
||||
scrollSource.current = ScrollSource.RENDERER
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<MotdBanner/>
|
||||
|
@ -140,16 +144,14 @@ export const Editor: React.FC = () => {
|
|||
}
|
||||
showRight={editorMode === EditorMode.PREVIEW || (editorMode === EditorMode.BOTH)}
|
||||
right={
|
||||
<ScrollingDocumentRenderPane
|
||||
onFirstHeadingChange={onFirstHeadingChange}
|
||||
onMakeScrollSource={() => {
|
||||
scrollSource.current = ScrollSource.RENDERER
|
||||
}}
|
||||
onMetadataChange={onMetadataChange}
|
||||
onScroll={onMarkdownRendererScroll}
|
||||
onTaskCheckedChange={onTaskCheckedChange}
|
||||
scrollState={scrollState.rendererScrollState}
|
||||
wide={editorMode === EditorMode.PREVIEW}
|
||||
<DocumentIframe markdownContent={markdownContent}
|
||||
onMakeScrollSource={setRendererToScrollSource}
|
||||
onFirstHeadingChange={onFirstHeadingChange}
|
||||
onTaskCheckedChange={onTaskCheckedChange}
|
||||
onMetadataChange={onMetadataChange}
|
||||
onScroll={onMarkdownRendererScroll}
|
||||
wide={editorMode === EditorMode.PREVIEW}
|
||||
scrollState={scrollState.rendererScrollState}
|
||||
/>
|
||||
}
|
||||
containerClassName={'overflow-hidden'}/>
|
||||
|
@ -157,5 +159,4 @@ export const Editor: React.FC = () => {
|
|||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export default Editor
|
||||
|
|
|
@ -8,7 +8,7 @@ import { RefObject, useCallback } from 'react'
|
|||
import { LineMarkerPosition } from '../../../markdown-renderer/types'
|
||||
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(() => {
|
||||
if (!renderer.current || !lineMarks || lineMarks.length === 0 || !onScroll) {
|
||||
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, useRef, useState } from 'react'
|
||||
import React, { ReactElement, useCallback, useRef, useState } from 'react'
|
||||
import { ShowIf } from '../../common/show-if/show-if'
|
||||
import { SplitDivider } from './split-divider/split-divider'
|
||||
import './splitter.scss'
|
||||
|
@ -33,28 +33,35 @@ export const Splitter: React.FC<SplitterProps> = ({ containerClassName, left, ri
|
|||
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 (
|
||||
<div ref={splitContainer} className={`flex-fill flex-row d-flex ${containerClassName || ''}`}
|
||||
onMouseUp={() => setDoResizing(false)}
|
||||
onTouchEnd={() => setDoResizing(false)}
|
||||
onMouseMove={(mouseEvent) => {
|
||||
if (doResizing) {
|
||||
recalculateSize(mouseEvent.pageX)
|
||||
mouseEvent.preventDefault()
|
||||
}
|
||||
}}
|
||||
onTouchMove={(touchEvent) => {
|
||||
if (doResizing) {
|
||||
recalculateSize(touchEvent.touches[0].pageX)
|
||||
}
|
||||
}}
|
||||
>
|
||||
onMouseUp={stopResizing} onTouchEnd={stopResizing} onMouseMove={onMouseMove} onTouchMove={onTouchMove}>
|
||||
<div className={`splitter left ${!showLeft ? 'd-none' : ''}`} style={{ flexBasis: `calc(${realSplit}% - 5px)` }}>
|
||||
{left}
|
||||
</div>
|
||||
<ShowIf condition={showLeft && showRight}>
|
||||
<div className='splitter separator'>
|
||||
<SplitDivider onGrab={() => setDoResizing(true)}/>
|
||||
<SplitDivider onGrab={onGrab}/>
|
||||
</div>
|
||||
</ShowIf>
|
||||
<div className={`splitter right ${!showRight ? 'd-none' : ''}`}>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
@ -13,6 +13,7 @@
|
|||
|
||||
&.sticky {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
> ul > li {
|
||||
|
|
|
@ -1,26 +1,29 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { TocAst } from 'markdown-it-toc-done-right'
|
||||
import React, { Fragment, ReactElement, useMemo } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { TocAst } from 'markdown-it-toc-done-right'
|
||||
import { ShowIf } from '../../common/show-if/show-if'
|
||||
import { createJumpToMarkClickEventHandler } from '../../markdown-renderer/replace-components/link-replacer/link-replacer'
|
||||
import './table-of-contents.scss'
|
||||
|
||||
export interface TableOfContentsProps {
|
||||
ast: TocAst
|
||||
maxDepth?: number
|
||||
className?: string
|
||||
baseUrl?: 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) {
|
||||
return null
|
||||
}
|
||||
|
@ -28,19 +31,20 @@ const convertLevel = (toc: TocAst, levelsToShowUnderThis: number, headerCounts:
|
|||
const rawName = toc.n.trim()
|
||||
const nameCount = (headerCounts.get(rawName) ?? -1) + 1
|
||||
const slug = `#${slugify(rawName)}${nameCount > 0 ? `-${nameCount}` : ''}`
|
||||
const headlineUrl = new URL(slug, baseUrl).toString()
|
||||
|
||||
headerCounts.set(rawName, nameCount)
|
||||
|
||||
const content = (
|
||||
<Fragment>
|
||||
<ShowIf condition={toc.l > 0}>
|
||||
<a href={slug}>{rawName}</a>
|
||||
<a href={headlineUrl} title={rawName} onClick={createJumpToMarkClickEventHandler(slug.substr(1))}>{rawName}</a>
|
||||
</ShowIf>
|
||||
<ShowIf condition={toc.c.length > 0}>
|
||||
<ul>
|
||||
{
|
||||
toc.c.map(child =>
|
||||
(convertLevel(child, levelsToShowUnderThis - 1, headerCounts, true)))
|
||||
(convertLevel(child, levelsToShowUnderThis - 1, headerCounts, true, baseUrl)))
|
||||
}
|
||||
</ul>
|
||||
</ShowIf>
|
||||
|
@ -49,7 +53,7 @@ const convertLevel = (toc: TocAst, levelsToShowUnderThis: number, headerCounts:
|
|||
|
||||
if (wrapInListItem) {
|
||||
return (
|
||||
<li key={slug}>
|
||||
<li key={headlineUrl}>
|
||||
{content}
|
||||
</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()
|
||||
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 (
|
||||
<div className={`markdown-toc ${className ?? ''}`}>
|
||||
<ShowIf condition={ast.c.length === 0}>
|
||||
<Trans i18nKey={'editor.infoToc'}/>
|
||||
</ShowIf>
|
||||
{ tocTree }
|
||||
{tocTree}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -227,5 +227,3 @@ export const HistoryPage: React.FC = () => {
|
|||
/>
|
||||
</Fragment>
|
||||
}
|
||||
|
||||
export default HistoryPage
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
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 { Trans, useTranslation } from 'react-i18next'
|
||||
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 { useReplacerInstanceListCreator } from './hooks/use-replacer-instance-list-creator'
|
||||
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 { AdditionalMarkdownRendererProps, LineMarkerPosition } from './types'
|
||||
import { useCalculateLineMarkerPosition } from './utils/calculate-line-marker-positions'
|
||||
|
@ -27,21 +28,26 @@ export interface FullMarkdownRendererProps {
|
|||
onMetaDataChange?: (yamlMetaData: YAMLMetaData | undefined) => void
|
||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
||||
onTocChange?: (ast: TocAst) => void
|
||||
rendererRef?: RefObject<HTMLDivElement>
|
||||
rendererRef?: Ref<HTMLDivElement>
|
||||
baseUrl?: string
|
||||
onImageClick?: ImageClickHandler
|
||||
}
|
||||
|
||||
export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & AdditionalMarkdownRendererProps> = ({
|
||||
onFirstHeadingChange,
|
||||
onLineMarkerPositionChanged,
|
||||
onMetaDataChange,
|
||||
onTaskCheckedChange,
|
||||
onTocChange,
|
||||
content,
|
||||
className,
|
||||
wide,
|
||||
rendererRef
|
||||
}) => {
|
||||
const allReplacers = useReplacerInstanceListCreator(onTaskCheckedChange)
|
||||
export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & AdditionalMarkdownRendererProps> = (
|
||||
{
|
||||
onFirstHeadingChange,
|
||||
onLineMarkerPositionChanged,
|
||||
onMetaDataChange,
|
||||
onTaskCheckedChange,
|
||||
onTocChange,
|
||||
content,
|
||||
className,
|
||||
wide,
|
||||
rendererRef,
|
||||
baseUrl,
|
||||
onImageClick
|
||||
}) => {
|
||||
const allReplacers = useReplacerInstanceListCreator(onTaskCheckedChange, onImageClick, baseUrl)
|
||||
useTranslation()
|
||||
|
||||
const [showYamlError, setShowYamlError] = useState(false)
|
||||
|
|
|
@ -16,14 +16,14 @@ export const useConvertMarkdownToReactDom = (
|
|||
markdownCode: string,
|
||||
markdownIt: MarkdownIt,
|
||||
componentReplacers?: () => ComponentReplacer[],
|
||||
onPreRendering?: () => void,
|
||||
onPostRendering?: () => void): ReactElement[] => {
|
||||
onBeforeRendering?: () => void,
|
||||
onAfterRendering?: () => void): ReactElement[] => {
|
||||
const oldMarkdownLineKeys = useRef<LineKeys[]>()
|
||||
const lastUsedLineId = useRef<number>(0)
|
||||
|
||||
return useMemo(() => {
|
||||
if (onPreRendering) {
|
||||
onPreRendering()
|
||||
if (onBeforeRendering) {
|
||||
onBeforeRendering()
|
||||
}
|
||||
const html = markdownIt.render(markdownCode)
|
||||
const contentLines = markdownCode.split('\n')
|
||||
|
@ -35,9 +35,9 @@ export const useConvertMarkdownToReactDom = (
|
|||
lastUsedLineId.current = newLastUsedLineId
|
||||
const transformer = componentReplacers ? buildTransformer(newLines, componentReplacers()) : undefined
|
||||
const rendering = ReactHtmlParser(html, { transform: transformer })
|
||||
if (onPostRendering) {
|
||||
onPostRendering()
|
||||
if (onAfterRendering) {
|
||||
onAfterRendering()
|
||||
}
|
||||
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 { ComponentReplacer } from '../replace-components/ComponentReplacer'
|
||||
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 { GistReplacer } from '../replace-components/gist/gist-replacer'
|
||||
import { GraphvizReplacer } from '../replace-components/graphviz/graphviz-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 { 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 { MermaidReplacer } from '../replace-components/mermaid/mermaid-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 { YoutubeReplacer } from '../replace-components/youtube/youtube-replacer'
|
||||
|
||||
export const useReplacerInstanceListCreator = (onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void): () => ComponentReplacer[] => {
|
||||
return useMemo(() => () => [
|
||||
new LinkInNewTabReplacer(),
|
||||
export const useReplacerInstanceListCreator = (onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void,
|
||||
onImageClick?: ImageClickHandler, baseUrl?: string): () => ComponentReplacer[] => useMemo(() =>
|
||||
() => [
|
||||
new LinkReplacer(baseUrl),
|
||||
new LinemarkerReplacer(),
|
||||
new PossibleWiderReplacer(),
|
||||
new GistReplacer(),
|
||||
|
@ -39,7 +40,7 @@ export const useReplacerInstanceListCreator = (onTaskCheckedChange?: (lineInMark
|
|||
new AsciinemaReplacer(),
|
||||
new AbcReplacer(),
|
||||
new PdfReplacer(),
|
||||
new ImageReplacer(),
|
||||
new ImageReplacer(onImageClick),
|
||||
new SequenceDiagramReplacer(),
|
||||
new CsvReplacer(),
|
||||
new FlowchartReplacer(),
|
||||
|
@ -51,5 +52,4 @@ export const useReplacerInstanceListCreator = (onTaskCheckedChange?: (lineInMark
|
|||
new QuoteOptionsReplacer(),
|
||||
new KatexReplacer(),
|
||||
new TaskListReplacer(onTaskCheckedChange)
|
||||
], [onTaskCheckedChange])
|
||||
}
|
||||
], [onImageClick, onTaskCheckedChange, baseUrl])
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { TocAst } from 'markdown-it-toc-done-right'
|
||||
|
@ -45,9 +45,9 @@ export class FullMarkdownItConfigurator extends BasicMarkdownItConfigurator {
|
|||
!this.useFrontmatter
|
||||
? undefined
|
||||
: {
|
||||
onYamlError: (hasError: boolean) => this.passYamlErrorState(hasError),
|
||||
onRawMeta: (rawMeta: RawYAMLMetadata) => this.onRawMeta(rawMeta)
|
||||
})
|
||||
onYamlError: (hasError: boolean) => this.passYamlErrorState(hasError),
|
||||
onRawMeta: (rawMeta: RawYAMLMetadata) => this.onRawMeta(rawMeta)
|
||||
})
|
||||
},
|
||||
headlineAnchors,
|
||||
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-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, ReactElement, useEffect, useState } from 'react'
|
||||
import ReactHtmlParser from 'react-html-parser'
|
||||
|
@ -59,7 +59,7 @@ export const HighlightedCode: React.FC<HighlightedCodeProps> = ({ code, language
|
|||
{ dom }
|
||||
</code>
|
||||
<div className={'text-right button-inside'}>
|
||||
<CopyToClipboardButton content={code}/>
|
||||
<CopyToClipboardButton content={code} data-cy="copy-code-button"/>
|
||||
</div>
|
||||
</Fragment>)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import React from 'react'
|
|||
import { Modal } from 'react-bootstrap'
|
||||
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
|
||||
import "./lightbox.scss"
|
||||
import { ProxyImageFrame } from './proxy-image-frame'
|
||||
|
||||
export interface ImageLightboxModalProps {
|
||||
show: boolean
|
||||
|
@ -33,7 +34,7 @@ export const ImageLightboxModal: React.FC<ImageLightboxModalProps> = ({ show, on
|
|||
<span>{alt ?? title ?? ''}</span>
|
||||
</Modal.Title>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -9,17 +9,27 @@ import React from 'react'
|
|||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import { ProxyImageFrame } from './proxy-image-frame'
|
||||
|
||||
export type ImageClickHandler = (event: React.MouseEvent<HTMLImageElement, MouseEvent>) => void;
|
||||
|
||||
export class ImageReplacer extends ComponentReplacer {
|
||||
private readonly clickHandler?: ImageClickHandler
|
||||
|
||||
constructor (clickHandler?: ImageClickHandler) {
|
||||
super()
|
||||
this.clickHandler = clickHandler
|
||||
}
|
||||
|
||||
public getReplacement (node: DomElement): React.ReactElement | undefined {
|
||||
if (node.name === 'img' && node.attribs) {
|
||||
return <ProxyImageFrame
|
||||
id={node.attribs.id}
|
||||
className={node.attribs.class}
|
||||
className={`${node.attribs.class} cursor-zoom-in`}
|
||||
src={node.attribs.src}
|
||||
alt={node.attribs.alt}
|
||||
title={node.attribs.title}
|
||||
width={node.attribs.width}
|
||||
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 { getProxiedUrl } from '../../../../api/media'
|
||||
import { ApplicationState } from '../../../../redux'
|
||||
import { LightboxImageFrame } from './lightbox-image-frame'
|
||||
|
||||
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))
|
||||
}, [imageProxyEnabled, src])
|
||||
|
||||
if (imageProxyEnabled) {
|
||||
return (
|
||||
<LightboxImageFrame src={imageUrl} title={title ?? alt ?? ''} alt={alt} {...props}/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<LightboxImageFrame src={src ?? ''} title={title ?? alt ?? ''} alt={alt} {...props}/>
|
||||
)
|
||||
return <img src={imageProxyEnabled ? imageUrl : (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
|
||||
}
|
||||
|
||||
export const calculateKeyFromLineMarker = (node: DomElement, lineKeys?: LineKeys[]): string|undefined => {
|
||||
export const calculateKeyFromLineMarker = (node: DomElement, lineKeys?: LineKeys[]): string | undefined => {
|
||||
if (!node.attribs || lineKeys === undefined) {
|
||||
return
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ export const renderNativeNode = (node: DomElement, key: string, transform: Trans
|
|||
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 nativeRenderer: NativeRenderer = () => renderNativeNode(node, key, transform)
|
||||
const subNodeTransform: SubNodeTransform = (subNode, subIndex) => transform(subNode, subIndex, transform)
|
||||
|
|
|
@ -1,22 +1,24 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Alert } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useParams } from 'react-router'
|
||||
import { getNote, Note } from '../../api/notes'
|
||||
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
|
||||
import { useDocumentTitle } from '../../hooks/common/use-document-title'
|
||||
import { ApplicationState } from '../../redux'
|
||||
import { setDocumentContent, setDocumentMetadata } from '../../redux/document-content/methods'
|
||||
import { extractNoteTitle } from '../common/document-title/note-title-extractor'
|
||||
import { MotdBanner } from '../common/motd-banner/motd-banner'
|
||||
import { ShowIf } from '../common/show-if/show-if'
|
||||
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 { YAMLMetaData } from '../editor/yaml-metadata/yaml-metadata'
|
||||
import { DocumentInfobar } from './document-infobar'
|
||||
|
@ -60,6 +62,7 @@ export const PadViewOnly: React.FC = () => {
|
|||
|
||||
useApplyDarkMode()
|
||||
useDocumentTitle(documentTitle)
|
||||
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
|
||||
|
||||
return (
|
||||
<div className={'d-flex flex-column mvh-100 bg-light'}>
|
||||
|
@ -80,7 +83,7 @@ export const PadViewOnly: React.FC = () => {
|
|||
</ShowIf>
|
||||
</div>
|
||||
<ShowIf condition={!error && !loading}>
|
||||
{ /* TODO set editable and created author properly */ }
|
||||
{ /* TODO set editable and created author properly */}
|
||||
<DocumentInfobar
|
||||
changedAuthor={noteData?.lastChange.userId ?? ''}
|
||||
changedTime={noteData?.lastChange.timestamp ?? 0}
|
||||
|
@ -90,11 +93,10 @@ export const PadViewOnly: React.FC = () => {
|
|||
noteId={id}
|
||||
viewCount={noteData?.viewcount ?? 0}
|
||||
/>
|
||||
<DocumentRenderPane
|
||||
onFirstHeadingChange={onFirstHeadingChange}
|
||||
onMetadataChange={onMetadataChange}
|
||||
onTaskCheckedChange={() => false}
|
||||
/>
|
||||
<DocumentIframe extraClasses={"flex-fill"}
|
||||
markdownContent={markdownContent}
|
||||
onFirstHeadingChange={onFirstHeadingChange}
|
||||
onMetadataChange={onMetadataChange}/>
|
||||
</ShowIf>
|
||||
</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'
|
||||
|
||||
const Editor = React.lazy(() => import(/* webpackPrefetch: true */ './components/editor/editor'))
|
||||
const RenderPage = React.lazy(() => import (/* webpackPrefetch: true */ './components/render-page/render-page'))
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
|
@ -58,6 +59,9 @@ ReactDOM.render(
|
|||
<ProfilePage/>
|
||||
</LandingLayout>
|
||||
</Route>
|
||||
<Route path="/render">
|
||||
<RenderPage/>
|
||||
</Route>
|
||||
<Route path="/n/:id">
|
||||
<Editor/>
|
||||
</Route>
|
||||
|
@ -82,10 +86,11 @@ ReactDOM.render(
|
|||
)
|
||||
|
||||
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
|
||||
// unregister() to register() below. Note this comes with some pitfalls.
|
||||
// Learn more about service workers: https://bit.ly/CRA-PWA
|
||||
serviceWorkerRegistration.unregister()
|
||||
|
||||
|
|
|
@ -49,6 +49,10 @@ export const initialState: Config = {
|
|||
version: '',
|
||||
sourceCodeUrl: '',
|
||||
issueTrackerUrl: ''
|
||||
},
|
||||
iframeCommunication: {
|
||||
editorOrigin: '',
|
||||
rendererOrigin: ''
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,10 @@
|
|||
color: $black;
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: darken($dark, 8%);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue