Skip to content

Commit

Permalink
Merge pull request #2841 from nextcloud/feature/heading-anchors
Browse files Browse the repository at this point in the history
Feature: Add page anchors (inter page links)
  • Loading branch information
Vinicius Reis authored Sep 1, 2022
2 parents 9973d1f + 4c87a0f commit 0f2c48e
Show file tree
Hide file tree
Showing 20 changed files with 307 additions and 99 deletions.
4 changes: 1 addition & 3 deletions css/prosemirror.scss
Original file line number Diff line number Diff line change
Expand Up @@ -102,20 +102,18 @@ div.ProseMirror {
h5,
h6 {
font-weight: 600;
line-height: 120%;
line-height: 1.1em;
margin-top: 24px;
margin-bottom: 12px;
color: var(--color-main-text);
}

h1 {
font-size: 36px;
margin-top: 48px;
}

h2 {
font-size: 30px;
margin-top: 48px;
}

h3 {
Expand Down
74 changes: 0 additions & 74 deletions cypress/e2e/outline.spec.js

This file was deleted.

124 changes: 124 additions & 0 deletions cypress/e2e/sections.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { initUserAndFiles, randHash } from '../utils/index.js'

const currentUser = randHash()
const fileName = 'test.md'

const refresh = () => cy.get('.files-controls .crumb:not(.hidden) a')
.last()
.click({ force: true })

const clickOutline = () => {
cy.getActionEntry('headings')
.click()

cy.get('.v-popper__wrapper .open').getActionEntry('outline')
.click()
}

describe('Content Sections', () => {
before(function() {
initUserAndFiles(currentUser, fileName)
})

beforeEach(function() {
cy.login(currentUser, 'password', {
onBeforeLoad(win) {
cy.stub(win, 'open')
.as('winOpen')
},
})

cy.openFile(fileName)
.then(() => cy.clearContent())
})

describe('Heading anchors', () => {
beforeEach(() => cy.clearContent())

it('Anchor exists', () => {
cy.getContent()
.type('# Heading\nText\n## Heading 2\nText\n## Heading 2')
.then(() => {
cy.getContent()
.find('a.anchor-link')
.should(($anchor) => {
expect($anchor).to.have.length(3)
expect($anchor.eq(0)).to.have.attr('href').and.equal('#heading')
expect($anchor.eq(1)).to.have.attr('href').and.equal('#heading-2')
expect($anchor.eq(2)).to.have.attr('href').and.equal('#heading-2--1')
})
})
})

it('Anchor scrolls into view', () => {
// Create link to top heading
cy.getContent()
.type('{selectAll}{backspace}move top\n{selectAll}')
.get('.menububble button[data-text-bubble-action="add-link"]')
.click({ force: true })
.then(() => {
cy.get('.menububble .menububble__input')
.type('{shift}')
.type('#top{enter}', { force: true })
})
// Insert content above link
cy.getContent()
.type('{moveToStart}\n{moveToStart}# top \n')
.type('lorem ipsum \n'.repeat(25))
.type('{moveToEnd}\n')
.find('h1#top')
.should('not.be.inViewport')
// Click link and test view moved to anchor
cy.getContent()
.find('a:not(.anchor-link)')
.click()
.then(() => {
cy.getContent()
.get('h1[id="top"]')
.should('be.inViewport')
})
})
})

describe('Table of Contents', () => {
beforeEach(() => cy.clearContent())

it('sidebar toc', () => {
cy.getContent()
.type('# T1 \n## T2 \n### T3 \n#### T4 \n##### T5 \n###### T6\n')
.then(refresh)
.then(() => cy.openFile(fileName, { force: true }))
.then(clickOutline)

cy.getOutline()
.find('header')
.should('exist')

cy.getTOC()
.find('ul li')
.should('have.length', 6)
cy.getTOC()
.find('ul li')
.each((el, index) => {
cy.wrap(el)
.should('have.attr', 'data-toc-level')
.and('equal', String(index + 1))

cy.wrap(el)
.find('a')
.should('have.attr', 'href')
.and('equal', `#t${index + 1}`)
})
})

it('empty toc', () => {
refresh()
.then(() => cy.openFile(fileName, { force: true }))
.then(clickOutline)

cy.getOutline()
.find('ul')
.should('be.empty')
})
})
})
16 changes: 16 additions & 0 deletions cypress/support/chai.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export default _chai => {
_chai.Assertion.addMethod('inViewport', function() {
const subject = this._obj

const height = Cypress.$(cy.state('window')).height()
const width = Cypress.$(cy.state('window')).width()
const rect = subject[0].getBoundingClientRect()

this.assert(
rect.top < height && rect.bottom > 0 && rect.right <= width && rect.left >= 0,
'expected #{this} to be in the viewport',
'expected #{this} to not be in the viewport',
this._obj
)
})
}
4 changes: 2 additions & 2 deletions cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,8 @@ Cypress.Commands.add('getTOC', () => {

Cypress.Commands.add('clearContent', () => {
return cy.getContent()
.type('{selectall}')
.type('{del}')
.scrollIntoView()
.type('{selectAll}{backspace}', { force: true })
})

Cypress.Commands.add('openWorkspace', (subject, name) => {
Expand Down
5 changes: 5 additions & 0 deletions cypress/support/e2e.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
// This file is loaded before all e2e tests

import './commands.js'
import chaiExtension from './chai.js'

before(() => {
chai.use(chaiExtension)
})
4 changes: 2 additions & 2 deletions js/editor.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/editor.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions js/text-files.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/text-files.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions js/text-public.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/text-public.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions js/text-text.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/text-text.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions js/text-viewer.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/text-viewer.js.map

Large diffs are not rendered by default.

19 changes: 15 additions & 4 deletions src/helpers/links.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ const domHref = function(node) {
if (ref.match(/^[a-zA-Z]*:/)) {
return ref
}
if (ref.startsWith('#')) {
return ref
}
const match = ref.match(/^([^?]*)\?fileId=(\d+)/)
if (match) {
const [, relPath, id] = match
Expand All @@ -82,9 +85,10 @@ const openLink = function(event, _attrs) {
const linkElement = event.target.closest('a')
const htmlHref = linkElement.href
const query = OC.parseQueryString(htmlHref)
const fragment = OC.parseQueryString(htmlHref.split('#').pop())
if (query.dir && fragment.relPath) {
const filename = fragment.relPath.split('/').pop()
const fragment = htmlHref.split('#').pop()
const fragmentQuery = OC.parseQueryString(fragment)
if (query?.dir && fragmentQuery?.relPath) {
const filename = fragmentQuery.relPath.split('/').pop()
const path = `${query.dir}/${filename}`
document.title = `${filename} - ${OC.theme.title}`
if (window.location.pathname.match(/apps\/files\/$/)) {
Expand All @@ -95,7 +99,7 @@ const openLink = function(event, _attrs) {
OCA.Viewer.open({ path })
return
}
if (query.fileId) {
if (query?.fileId) {
// open the direct file link
window.open(generateUrl(`/f/${query.fileId}`))
return
Expand All @@ -104,6 +108,13 @@ const openLink = function(event, _attrs) {
console.error('Invalid link', htmlHref)
return false
}
if (fragment) {
const el = document.getElementById(fragment)
if (el) {
el.scrollIntoView()
return
}
}
window.open(htmlHref)
return true
}
Expand Down
Loading

0 comments on commit 0f2c48e

Please sign in to comment.