From b84e721e47a957aba84b078518314f8e1deef1da Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 11 Jun 2024 10:13:29 +0200 Subject: [PATCH 1/4] refactor(heading): move css to prosemirror.scss Signed-off-by: Max --- src/css/prosemirror.scss | 25 +++++++++++++++++++++++++ src/nodes/Heading/HeadingView.vue | 31 ------------------------------- 2 files changed, 25 insertions(+), 31 deletions(-) diff --git a/src/css/prosemirror.scss b/src/css/prosemirror.scss index cd0f05ee362..c958aa3bd55 100644 --- a/src/css/prosemirror.scss +++ b/src/css/prosemirror.scss @@ -87,9 +87,34 @@ div.ProseMirror { } > h1,h2,h3,h4,h5,h6 { + position: relative; + &:first-child { margin-top: 0; } + + .heading-anchor[contenteditable="false"] { + // Shrink clickable area of anchor permalinks to not overlay the heading + width: 1em; + opacity: 0; + padding: 0; + left: -1em; + bottom: 0; + font-size: max(1em, 16px); + position: absolute; + text-decoration: none; + transition-duration: .15s; + transition-property: opacity; + transition-timing-function: cubic-bezier(.4,0,.2,1); + } + + &:hover .heading-anchor { + opacity: 0.5!important; + } + + &:focus-visible { + outline: none; + } } a { diff --git a/src/nodes/Heading/HeadingView.vue b/src/nodes/Heading/HeadingView.vue index d47c7d21a8e..804ef334032 100644 --- a/src/nodes/Heading/HeadingView.vue +++ b/src/nodes/Heading/HeadingView.vue @@ -81,34 +81,3 @@ export default { }, } - - From a4a686e448c5ee5ce43a1042581d934283df46e8 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 13 Jun 2024 14:29:22 +0200 Subject: [PATCH 2/4] test(cy): test headings without node-view-content Signed-off-by: Max --- cypress/e2e/SmartPicker.spec.js | 4 ++-- cypress/e2e/versions.spec.js | 30 +++++++++++++++--------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/cypress/e2e/SmartPicker.spec.js b/cypress/e2e/SmartPicker.spec.js index 8a9fbab8355..2f7671b96c0 100644 --- a/cypress/e2e/SmartPicker.spec.js +++ b/cypress/e2e/SmartPicker.spec.js @@ -37,8 +37,8 @@ describe('Smart picker', () => { .type('Heading{enter}Hello World{enter}') cy.getContent() - .find('h1 [data-node-view-content]') - .should('have.text', 'Hello World') + .find('h1') + .should('contain.text', 'Hello World') }) it('Insert a link with the smart picker', () => { diff --git a/cypress/e2e/versions.spec.js b/cypress/e2e/versions.spec.js index 5da12bc6358..5241761d0f2 100644 --- a/cypress/e2e/versions.spec.js +++ b/cypress/e2e/versions.spec.js @@ -30,18 +30,18 @@ describe('Versions', () => { cy.get('[data-files-versions-versions-list] li a').eq(1).click() cy.get('.viewer__content #read-only-editor') - .find('h1 [data-node-view-content]') - .should('have.text', 'V2') + .find('h1') + .should('contain.text', 'V2') cy.get('[data-files-versions-versions-list] li a').eq(2).click() cy.get('.viewer__content #read-only-editor') - .find('h1 [data-node-view-content]') - .should('have.text', 'V1') + .find('h1') + .should('contain.text', 'V1') cy.get('[data-files-versions-versions-list] li a').eq(0).click() cy.getContent() - .find('h1 [data-node-view-content]') - .should('have.text', 'V3') + .find('h1') + .should('contain.text', 'V3') }) }) @@ -64,18 +64,18 @@ describe('Versions', () => { cy.get('[data-files-versions-versions-list] li a').eq(1).click() cy.get('.viewer__content #read-only-editor') - .find('h1 [data-node-view-content]') - .should('have.text', 'V2') + .find('h1') + .should('contain.text', 'V2') cy.get('[data-files-versions-versions-list] li a').eq(2).click() cy.get('.viewer__content #read-only-editor') - .find('h1 [data-node-view-content]') - .should('have.text', 'V1') + .find('h1') + .should('contain.text', 'V1') cy.get('[data-files-versions-versions-list] li a').eq(0).click() cy.getContent() - .find('h1 [data-node-view-content]') - .should('have.text', 'V3') + .find('h1') + .should('contain.text', 'V3') cy.getContent() .type('Hello') @@ -109,11 +109,11 @@ describe('Versions', () => { .click() cy.get('.viewer__content #read-only-editor') - .find('h1 [data-node-view-content]') - .should('have.text', 'V1') + .find('h1') + .should('contain.text', '#V1') cy.get('.viewer__content .viewer__file--active .ProseMirror') - .find('h1 [data-node-view-content]') + .find('h1') .should('contain.text', 'V3') }) }) From 79345b062862180074260b37c0e7662bb65ef480 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 17 Jun 2024 12:15:59 +0200 Subject: [PATCH 3/4] fix(editor): only setEditable when needed This will trigger an update event on the editor which in turn will trigger all update handlers. Avoid this when not necessary. Signed-off-by: Max --- src/components/Editor.vue | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/Editor.vue b/src/components/Editor.vue index 5157ddde4e6..db83ddf172c 100644 --- a/src/components/Editor.vue +++ b/src/components/Editor.vue @@ -558,7 +558,10 @@ export default { this.document = document this.syncError = null - this.$editor.setEditable(!this.readOnly) + const editable = !this.readOnly + if (this.$editor.isEditable !== editable) { + this.$editor.setEditable(editable) + } }, onSync({ steps, document }) { From d48eb8d78c5c57a63deff09a4e07ce4613beff37 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 18 Jun 2024 09:51:09 +0200 Subject: [PATCH 4/4] test(cy): do not wait for push after initialization The toc does not trigger a transaction anymore and therefore there is no push to wait for either. Also use cypress aliases and avoid deep nesting of cypress calls when possible. Signed-off-by: Max [skip ci] --- cypress/e2e/initial.spec.js | 176 +++++++++++++++++------------------- 1 file changed, 82 insertions(+), 94 deletions(-) diff --git a/cypress/e2e/initial.spec.js b/cypress/e2e/initial.spec.js index c9372c22ff7..81801490b4d 100644 --- a/cypress/e2e/initial.spec.js +++ b/cypress/e2e/initial.spec.js @@ -39,14 +39,11 @@ describe('Test state loading of documents', function() { it('Initial content can not be undone', function() { cy.shareFile('/test.md', { edit: true }) - .then((token) => { - cy.visit(`/s/${token}`) - }) - .then(() => { - cy.getEditor().should('be.visible') - cy.getContent() - .should('contain', 'Hello world') - .find('h2').should('contain', 'Hello world') + .then(token => cy.visit(`/s/${token}`)) + cy.getEditor().should('be.visible') + cy.getContent() + .should('contain', 'Hello world') + .find('h2').should('contain', 'Hello world') cy.getMenu().should('be.visible') cy.getActionEntry('undo').should('be.visible').click() @@ -57,104 +54,95 @@ describe('Test state loading of documents', function() { }) it('Consecutive sessions work properly', function() { - let readToken = null - let writeToken = null cy.interceptCreate() cy.shareFile('/test2.md') - .then((token) => { - readToken = token - cy.logout() - cy.visit(`/s/${readToken}`) - cy.wait('@create') - }) - .then(() => { - // Open read only for the first time - cy.getEditor().should('be.visible') - cy.getContent() - .should('contain', 'Hello world') - .find('h2').should('contain', 'Hello world') - cy.closeInterceptedSession(readToken) + .as('readToken') + cy.logout() + cy.get('@readToken') + .then(token => cy.visit(`/s/${token}`)) + cy.wait('@create') + // Open read only for the first time + cy.getEditor().should('be.visible') + cy.getContent() + .should('contain', 'Hello world') + .find('h2').should('contain', 'Hello world') + cy.get('@readToken') + .then(cy.closeInterceptedSession) - // Open read only for the second time - cy.reload() - cy.getEditor().should('be.visible') - cy.getContent() - .should('contain', 'Hello world') - .find('h2').should('contain', 'Hello world') - cy.closeInterceptedSession(readToken) + // Open read only for the second time + cy.reload() + cy.getEditor().should('be.visible') + cy.getContent() + .should('contain', 'Hello world') + .find('h2').should('contain', 'Hello world') + cy.get('@readToken') + .then(cy.closeInterceptedSession) - cy.login(user) - cy.shareFile('/test2.md', { edit: true }) - .then((token) => { - writeToken = token - // Open write link and edit something - cy.visit(`/s/${writeToken}`) - cy.getEditor().should('be.visible') - cy.getContent() - .should('contain', 'Hello world') - .find('h2').should('contain', 'Hello world') - cy.getContent() - .type('Something new {end}') - cy.intercept({ method: 'POST', url: '**/session/*/push' }).as('push') - cy.intercept({ method: 'POST', url: '**/session/*/sync' }).as('sync') - cy.wait('@push') - cy.wait('@sync') - cy.closeInterceptedSession(writeToken) + cy.login(user) + cy.shareFile('/test2.md', { edit: true }) + .as('writeToken') + // Open write link and edit something + cy.get('@writeToken') + .then(token => cy.visit(`/s/${token}`)) + cy.getEditor().should('be.visible') + cy.getContent() + .should('contain', 'Hello world') + .find('h2').should('contain', 'Hello world') + cy.getContent() + .type('Something new {end}') + cy.intercept({ method: 'POST', url: '**/session/*/sync' }).as('sync') + cy.wait('@sync') + cy.get('@writeToken') + .then(cy.closeInterceptedSession) - // Reopen read only link and check if changes are there - cy.visit(`/s/${readToken}`) - cy.getEditor().should('be.visible') - cy.getContent() - .find('h2').should('contain', 'Something new Hello world') - }) - }) + // Reopen read only link and check if changes are there + cy.get('@readToken') + .then(token => cy.visit(`/s/${token}`)) + cy.getEditor().should('be.visible') + cy.getContent() + .find('h2').should('contain', 'Something new Hello world') }) it('Load after state has been saved', function() { - let readToken = null - let writeToken = null cy.interceptCreate() cy.shareFile('/test3.md', { edit: true }) - .then((token) => { - writeToken = token - cy.logout() - cy.visit(`/s/${writeToken}`) - }) - .then(() => { - // Open a file, write and save - cy.getEditor().should('be.visible') - cy.getContent() - .should('contain', 'Hello world') - .find('h2').should('contain', 'Hello world') - cy.getContent() - .type('Something new {end}') - cy.intercept({ method: 'POST', url: '**/session/*/save' }).as('save') - cy.get('.save-status button').click() - cy.wait('@save', { timeout: 10000 }) - cy.closeInterceptedSession(writeToken) + .as('writeToken') + cy.logout() + cy.get('@writeToken') + .then(token => cy.visit(`/s/${token}`)) - // Open writable file again and assert the content - cy.reload() - cy.getEditor().should('be.visible') - cy.getContent() - .should('contain', 'Hello world') - .find('h2').should('contain', 'Something new Hello world') + // Open a file, write and save + cy.getEditor().should('be.visible') + cy.getContent() + .should('contain', 'Hello world') + .find('h2').should('contain', 'Hello world') + cy.getContent() + .type('Something new {end}') + cy.intercept({ method: 'POST', url: '**/session/*/save' }).as('save') + cy.get('.save-status button').click() + cy.wait('@save', { timeout: 10000 }) + cy.get('@writeToken') + .then(cy.closeInterceptedSession) - cy.login(user) - cy.shareFile('/test3.md') - .then((token) => { - readToken = token - cy.logout() - cy.visit(`/s/${readToken}`) - }) - .then(() => { - // Open read only file again and assert the content - cy.getEditor().should('be.visible') - cy.getContent() - .should('contain', 'Hello world') - .find('h2').should('contain', 'Something new Hello world') - }) - }) + // Open writable file again and assert the content + cy.reload() + cy.getEditor().should('be.visible') + cy.getContent() + .should('contain', 'Hello world') + .find('h2').should('contain', 'Something new Hello world') + + cy.login(user) + cy.shareFile('/test3.md') + .as('readToken') + cy.logout() + cy.get('@readToken') + .then(token => cy.visit(`/s/${token}`)) + + // Open read only file again and assert the content + cy.getEditor().should('be.visible') + cy.getContent() + .should('contain', 'Hello world') + .find('h2').should('contain', 'Something new Hello world') }) })