mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-30 09:27:13 +00:00
feat(vue-app): support app/router.scrollBehavior.js
and deprecate scrollBehavior
(#6055)
This commit is contained in:
parent
ac00f7a627
commit
f7cb3dae0f
@ -405,42 +405,47 @@ export default class Builder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async resolveCustomTemplates (templateContext) {
|
async resolveCustomTemplates (templateContext) {
|
||||||
// Resolve template files
|
// Sanitize custom template files
|
||||||
|
this.options.build.templates = this.options.build.templates.map((t) => {
|
||||||
|
const src = t.src || t
|
||||||
|
|
||||||
|
return Object.assign(
|
||||||
|
{
|
||||||
|
src: r(this.options.srcDir, src),
|
||||||
|
dst: t.dst || path.basename(src),
|
||||||
|
custom: true
|
||||||
|
},
|
||||||
|
typeof t === 'object' ? t : undefined
|
||||||
|
)
|
||||||
|
})
|
||||||
const customTemplateFiles = this.options.build.templates.map(
|
const customTemplateFiles = this.options.build.templates.map(
|
||||||
t => t.dst || path.basename(t.src || t)
|
t => t.dst || path.basename(t.src || t)
|
||||||
)
|
)
|
||||||
|
const templatePaths = uniq([
|
||||||
|
// Modules & user provided templates
|
||||||
|
// first custom to keep their index
|
||||||
|
...customTemplateFiles,
|
||||||
|
// @nuxt/vue-app templates
|
||||||
|
...templateContext.templateFiles
|
||||||
|
])
|
||||||
|
|
||||||
const templateFiles = await Promise.all(templateContext.templateFiles.map(async (file) => {
|
templateContext.templateFiles = await Promise.all(templatePaths.map(async (file) => {
|
||||||
// Skip if custom file was already provided in build.templates[]
|
// Use custom file if provided in build.templates[]
|
||||||
if (customTemplateFiles.includes(file)) {
|
const customTemplateIndex = customTemplateFiles.indexOf(file)
|
||||||
return
|
const customTemplate = customTemplateIndex !== -1 ? this.options.build.templates[customTemplateIndex] : null
|
||||||
}
|
let src = customTemplate ? (customTemplate.src || customTemplate) : r(this.template.dir, file)
|
||||||
// Allow override templates using a file with same name in ${srcDir}/app
|
// Allow override templates using a file with same name in ${srcDir}/app
|
||||||
const customPath = r(this.options.srcDir, 'app', file)
|
const customPath = r(this.options.srcDir, this.options.dir.app, file)
|
||||||
const customFileExists = await fsExtra.exists(customPath)
|
const customFileExists = await fsExtra.exists(customPath)
|
||||||
|
src = customFileExists ? customPath : src
|
||||||
|
|
||||||
return {
|
return {
|
||||||
src: customFileExists ? customPath : r(this.template.dir, file),
|
src,
|
||||||
dst: file,
|
dst: file,
|
||||||
custom: customFileExists
|
custom: Boolean(customFileExists || customTemplate),
|
||||||
|
options: (customTemplate && customTemplate.options) || {}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
templateContext.templateFiles = templateFiles
|
|
||||||
.filter(Boolean)
|
|
||||||
// Add custom template files
|
|
||||||
.concat(
|
|
||||||
this.options.build.templates.map((t) => {
|
|
||||||
return Object.assign(
|
|
||||||
{
|
|
||||||
src: r(this.options.srcDir, t.src || t),
|
|
||||||
dst: t.dst || path.basename(t.src || t),
|
|
||||||
custom: true
|
|
||||||
},
|
|
||||||
typeof t === 'object' ? t : undefined
|
|
||||||
)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async resolveLoadingIndicator ({ templateFiles }) {
|
async resolveLoadingIndicator ({ templateFiles }) {
|
||||||
@ -628,6 +633,9 @@ export default class Builder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.createFileWatcher(customPatterns, ['change'], refreshFiles, this.assignWatcher('custom'))
|
this.createFileWatcher(customPatterns, ['change'], refreshFiles, this.assignWatcher('custom'))
|
||||||
|
|
||||||
|
// Watch for app/ files
|
||||||
|
this.createFileWatcher([r(this.options.srcDir, this.options.dir.app)], ['add', 'change', 'unlink'], refreshFiles, this.assignWatcher('app'))
|
||||||
}
|
}
|
||||||
|
|
||||||
getServerMiddlewarePaths () {
|
getServerMiddlewarePaths () {
|
||||||
|
@ -3,7 +3,10 @@ export const createNuxt = () => ({
|
|||||||
globalName: 'global_name',
|
globalName: 'global_name',
|
||||||
globals: [],
|
globals: [],
|
||||||
build: {},
|
build: {},
|
||||||
router: {}
|
router: {},
|
||||||
|
dir: {
|
||||||
|
app: 'app'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
ready: jest.fn(),
|
ready: jest.fn(),
|
||||||
hook: jest.fn(),
|
hook: jest.fn(),
|
||||||
|
@ -209,12 +209,12 @@ describe('builder: builder generate', () => {
|
|||||||
await builder.resolveCustomTemplates(templateContext)
|
await builder.resolveCustomTemplates(templateContext)
|
||||||
|
|
||||||
expect(templateContext.templateFiles).toEqual([
|
expect(templateContext.templateFiles).toEqual([
|
||||||
{ custom: true, dst: 'router.js', src: 'r(/var/nuxt/src, app, router.js)' },
|
{ custom: true, dst: 'foo.js', src: 'r(/var/nuxt/src, app, foo.js)', options: {} },
|
||||||
{ custom: undefined, dst: 'store.js', src: 'r(/var/nuxt/templates, store.js)' },
|
{ custom: true, dst: 'bar.js', src: '/var/nuxt/templates/bar.js', options: {} },
|
||||||
{ custom: undefined, dst: 'middleware.js', src: 'r(/var/nuxt/templates, middleware.js)' },
|
{ custom: true, dst: 'baz.js', src: '/var/nuxt/templates/baz.js', options: {} },
|
||||||
{ custom: true, dst: 'foo.js', src: 'r(/var/nuxt/src, /var/nuxt/templates/foo.js)' },
|
{ custom: false, dst: 'router.js', src: 'r(/var/nuxt/templates, router.js)', options: {} },
|
||||||
{ custom: true, dst: 'bar.js', src: '/var/nuxt/templates/bar.js' },
|
{ custom: false, dst: 'store.js', src: 'r(/var/nuxt/templates, store.js)', options: {} },
|
||||||
{ custom: true, dst: 'baz.js', src: '/var/nuxt/templates/baz.js' }
|
{ custom: false, dst: 'middleware.js', src: 'r(/var/nuxt/templates, middleware.js)', options: {} }
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -189,9 +189,9 @@ describe('builder: builder watch', () => {
|
|||||||
'/var/nuxt/src/style'
|
'/var/nuxt/src/style'
|
||||||
]
|
]
|
||||||
|
|
||||||
expect(builder.createFileWatcher).toBeCalledTimes(2)
|
expect(builder.createFileWatcher).toBeCalledTimes(3)
|
||||||
expect(builder.createFileWatcher).toBeCalledWith(patterns, ['change'], expect.any(Function), expect.any(Function))
|
expect(builder.createFileWatcher).toBeCalledWith(patterns, ['change'], expect.any(Function), expect.any(Function))
|
||||||
expect(builder.assignWatcher).toBeCalledTimes(2)
|
expect(builder.assignWatcher).toBeCalledTimes(3)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should invoke chokidar to create watcher', () => {
|
test('should invoke chokidar to create watcher', () => {
|
||||||
|
@ -35,6 +35,7 @@ export default () => ({
|
|||||||
],
|
],
|
||||||
dir: {
|
dir: {
|
||||||
assets: 'assets',
|
assets: 'assets',
|
||||||
|
app: 'app',
|
||||||
layouts: 'layouts',
|
layouts: 'layouts',
|
||||||
middleware: 'middleware',
|
middleware: 'middleware',
|
||||||
pages: 'pages',
|
pages: 'pages',
|
||||||
|
@ -35,6 +35,12 @@ export function getNuxtConfig (_options) {
|
|||||||
options._routerBaseSpecified = true
|
options._routerBaseSpecified = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Remove for Nuxt 3
|
||||||
|
// router.scrollBehavior -> app/router.scrollBehavior.js
|
||||||
|
if (options.router && typeof options.router.scrollBehavior !== 'undefined') {
|
||||||
|
consola.warn('`router.scrollBehavior` property is deprecated in favor of using `~/app/router.scrollBehavior.js` file, learn more: https://nuxtjs.org/api/configuration-router#scrollbehavior')
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Remove for Nuxt 3
|
// TODO: Remove for Nuxt 3
|
||||||
// transition -> pageTransition
|
// transition -> pageTransition
|
||||||
if (typeof options.transition !== 'undefined') {
|
if (typeof options.transition !== 'undefined') {
|
||||||
|
@ -151,6 +151,7 @@ Object {
|
|||||||
"dev": false,
|
"dev": false,
|
||||||
"devModules": Array [],
|
"devModules": Array [],
|
||||||
"dir": Object {
|
"dir": Object {
|
||||||
|
"app": "app",
|
||||||
"assets": "assets",
|
"assets": "assets",
|
||||||
"layouts": "layouts",
|
"layouts": "layouts",
|
||||||
"middleware": "middleware",
|
"middleware": "middleware",
|
||||||
|
@ -131,6 +131,7 @@ Object {
|
|||||||
"dev": false,
|
"dev": false,
|
||||||
"devModules": Array [],
|
"devModules": Array [],
|
||||||
"dir": Object {
|
"dir": Object {
|
||||||
|
"app": "app",
|
||||||
"assets": "assets",
|
"assets": "assets",
|
||||||
"layouts": "layouts",
|
"layouts": "layouts",
|
||||||
"middleware": "middleware",
|
"middleware": "middleware",
|
||||||
@ -457,6 +458,7 @@ Object {
|
|||||||
"dev": false,
|
"dev": false,
|
||||||
"devModules": Array [],
|
"devModules": Array [],
|
||||||
"dir": Object {
|
"dir": Object {
|
||||||
|
"app": "app",
|
||||||
"assets": "assets",
|
"assets": "assets",
|
||||||
"layouts": "layouts",
|
"layouts": "layouts",
|
||||||
"middleware": "middleware",
|
"middleware": "middleware",
|
||||||
|
@ -26,9 +26,9 @@ describe('util: serialize', () => {
|
|||||||
|
|
||||||
test('should serialize normal function', () => {
|
test('should serialize normal function', () => {
|
||||||
const obj = {
|
const obj = {
|
||||||
fn () {}
|
fn: function () {} // eslint-disable-line object-shorthand
|
||||||
}
|
}
|
||||||
expect(serializeFunction(obj.fn)).toEqual('function() {}')
|
expect(serializeFunction(obj.fn)).toEqual('function () {}')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should serialize shorthand function', () => {
|
test('should serialize shorthand function', () => {
|
||||||
|
@ -10,6 +10,7 @@ export const template = {
|
|||||||
'index.js',
|
'index.js',
|
||||||
'middleware.js',
|
'middleware.js',
|
||||||
'router.js',
|
'router.js',
|
||||||
|
'router.scrollBehavior.js',
|
||||||
'server.js',
|
'server.js',
|
||||||
'utils.js',
|
'utils.js',
|
||||||
'empty.js',
|
'empty.js',
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import Router from 'vue-router'
|
import Router from 'vue-router'
|
||||||
import { interopDefault } from './utils'<%= isTest ? '// eslint-disable-line no-unused-vars' : '' %>
|
import { interopDefault } from './utils'<%= isTest ? '// eslint-disable-line no-unused-vars' : '' %>
|
||||||
|
import scrollBehavior from './router.scrollBehavior.js'
|
||||||
|
|
||||||
<% function recursiveRoutes(routes, tab, components, indentCount) {
|
<% function recursiveRoutes(routes, tab, components, indentCount) {
|
||||||
let res = ''
|
let res = ''
|
||||||
@ -83,81 +84,6 @@ const _routes = recursiveRoutes(router.routes, ' ', _components, 2)
|
|||||||
|
|
||||||
Vue.use(Router)
|
Vue.use(Router)
|
||||||
|
|
||||||
<% if (router.scrollBehavior) { %>
|
|
||||||
const scrollBehavior = <%= serializeFunction(router.scrollBehavior) %>
|
|
||||||
<% } else { %>
|
|
||||||
if (process.client) {
|
|
||||||
if ('scrollRestoration' in window.history) {
|
|
||||||
window.history.scrollRestoration = 'manual'
|
|
||||||
|
|
||||||
// reset scrollRestoration to auto when leaving page, allowing page reload
|
|
||||||
// and back-navigation from other pages to use the browser to restore the
|
|
||||||
// scrolling position.
|
|
||||||
window.addEventListener('beforeunload', () => {
|
|
||||||
window.history.scrollRestoration = 'auto'
|
|
||||||
})
|
|
||||||
|
|
||||||
// Setting scrollRestoration to manual again when returning to this page.
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
window.history.scrollRestoration = 'manual'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const scrollBehavior = function (to, from, savedPosition) {
|
|
||||||
// if the returned position is falsy or an empty object,
|
|
||||||
// will retain current scroll position.
|
|
||||||
let position = false
|
|
||||||
|
|
||||||
// if no children detected and scrollToTop is not explicitly disabled
|
|
||||||
if (
|
|
||||||
to.matched.length < 2 &&
|
|
||||||
to.matched.every(r => r.components.default.options.scrollToTop !== false)
|
|
||||||
) {
|
|
||||||
// scroll to the top of the page
|
|
||||||
position = { x: 0, y: 0 }
|
|
||||||
} else if (to.matched.some(r => r.components.default.options.scrollToTop)) {
|
|
||||||
// if one of the children has scrollToTop option set to true
|
|
||||||
position = { x: 0, y: 0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
// savedPosition is only available for popstate navigations (back button)
|
|
||||||
if (savedPosition) {
|
|
||||||
position = savedPosition
|
|
||||||
}
|
|
||||||
|
|
||||||
const nuxt = window.<%= globals.nuxt %>
|
|
||||||
|
|
||||||
// triggerScroll is only fired when a new component is loaded
|
|
||||||
if (to.path === from.path && to.hash !== from.hash) {
|
|
||||||
nuxt.$nextTick(() => nuxt.$emit('triggerScroll'))
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
// wait for the out transition to complete (if necessary)
|
|
||||||
nuxt.$once('triggerScroll', () => {
|
|
||||||
// coords will be used if no selector is provided,
|
|
||||||
// or if the selector didn't match any element.
|
|
||||||
if (to.hash) {
|
|
||||||
let hash = to.hash
|
|
||||||
// CSS.escape() is not supported with IE and Edge.
|
|
||||||
if (typeof window.CSS !== 'undefined' && typeof window.CSS.escape !== 'undefined') {
|
|
||||||
hash = '#' + window.CSS.escape(hash.substr(1))
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if (document.querySelector(hash)) {
|
|
||||||
// scroll to anchor by returning the selector
|
|
||||||
position = { selector: hash }
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Failed to save scroll position. Please add CSS.escape() polyfill (https://github.com/mathiasbynens/CSS.escape).')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resolve(position)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
export function createRouter() {
|
export function createRouter() {
|
||||||
return new Router({
|
return new Router({
|
||||||
mode: '<%= router.mode %>',
|
mode: '<%= router.mode %>',
|
||||||
|
77
packages/vue-app/template/router.scrollBehavior.js
Normal file
77
packages/vue-app/template/router.scrollBehavior.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<% if (router.scrollBehavior) { %>
|
||||||
|
export default <%= serializeFunction(router.scrollBehavior) %>
|
||||||
|
<% } else { %>import { getMatchedComponents } from './utils'
|
||||||
|
|
||||||
|
if (process.client) {
|
||||||
|
if ('scrollRestoration' in window.history) {
|
||||||
|
window.history.scrollRestoration = 'manual'
|
||||||
|
|
||||||
|
// reset scrollRestoration to auto when leaving page, allowing page reload
|
||||||
|
// and back-navigation from other pages to use the browser to restore the
|
||||||
|
// scrolling position.
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
window.history.scrollRestoration = 'auto'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Setting scrollRestoration to manual again when returning to this page.
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
window.history.scrollRestoration = 'manual'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (to, from, savedPosition) {
|
||||||
|
// if the returned position is falsy or an empty object,
|
||||||
|
// will retain current scroll position.
|
||||||
|
let position = false
|
||||||
|
|
||||||
|
// if no children detected and scrollToTop is not explicitly disabled
|
||||||
|
const Pages = getMatchedComponents(to)
|
||||||
|
if (
|
||||||
|
Pages.length < 2 &&
|
||||||
|
Pages.every(Page => Page.options.scrollToTop !== false)
|
||||||
|
) {
|
||||||
|
// scroll to the top of the page
|
||||||
|
position = { x: 0, y: 0 }
|
||||||
|
} else if (Pages.some(Page => Page.options.scrollToTop)) {
|
||||||
|
// if one of the children has scrollToTop option set to true
|
||||||
|
position = { x: 0, y: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// savedPosition is only available for popstate navigations (back button)
|
||||||
|
if (savedPosition) {
|
||||||
|
position = savedPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
const nuxt = window.<%= globals.nuxt %>
|
||||||
|
|
||||||
|
// triggerScroll is only fired when a new component is loaded
|
||||||
|
if (to.path === from.path && to.hash !== from.hash) {
|
||||||
|
nuxt.$nextTick(() => nuxt.$emit('triggerScroll'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// wait for the out transition to complete (if necessary)
|
||||||
|
nuxt.$once('triggerScroll', () => {
|
||||||
|
// coords will be used if no selector is provided,
|
||||||
|
// or if the selector didn't match any element.
|
||||||
|
if (to.hash) {
|
||||||
|
let hash = to.hash
|
||||||
|
// CSS.escape() is not supported with IE and Edge.
|
||||||
|
if (typeof window.CSS !== 'undefined' && typeof window.CSS.escape !== 'undefined') {
|
||||||
|
hash = '#' + window.CSS.escape(hash.substr(1))
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (document.querySelector(hash)) {
|
||||||
|
// scroll to anchor by returning the selector
|
||||||
|
position = { selector: hash }
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to save scroll position. Please add CSS.escape() polyfill (https://github.com/mathiasbynens/CSS.escape).')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve(position)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
<% } %>
|
@ -14,9 +14,10 @@ const hooks = [
|
|||||||
|
|
||||||
describe('with-config', () => {
|
describe('with-config', () => {
|
||||||
buildFixture('with-config', () => {
|
buildFixture('with-config', () => {
|
||||||
expect(consola.warn).toHaveBeenCalledTimes(6)
|
expect(consola.warn).toHaveBeenCalledTimes(7)
|
||||||
expect(consola.fatal).toHaveBeenCalledTimes(0)
|
expect(consola.fatal).toHaveBeenCalledTimes(0)
|
||||||
expect(consola.warn.mock.calls).toMatchObject([
|
expect(consola.warn.mock.calls).toMatchObject([
|
||||||
|
['`router.scrollBehavior` property is deprecated in favor of using `~/app/router.scrollBehavior.js` file, learn more: https://nuxtjs.org/api/configuration-router#scrollbehavior'],
|
||||||
['Unknown mode: unknown. Falling back to universal'],
|
['Unknown mode: unknown. Falling back to universal'],
|
||||||
['Invalid plugin mode (server/client/all): \'abc\'. Falling back to \'all\''],
|
['Invalid plugin mode (server/client/all): \'abc\'. Falling back to \'all\''],
|
||||||
[{
|
[{
|
||||||
|
Loading…
Reference in New Issue
Block a user