mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-19 19:57:22 +08:00
fix(vue): improve path matching when going back (#22275)
resolves #22258
This commit is contained in:
@ -13,12 +13,12 @@ describe('View Stacks', () => {
|
|||||||
const item = viewStacks.createViewItem(
|
const item = viewStacks.createViewItem(
|
||||||
'1',
|
'1',
|
||||||
() => {},
|
() => {},
|
||||||
'mockMatchedRoute',
|
{ path: '/mockMatchedRoute' },
|
||||||
{ pathname: '/home' }
|
{ pathname: '/home' }
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(item.outletId).toEqual('1');
|
expect(item.outletId).toEqual('1');
|
||||||
expect(item.matchedRoute).toEqual('mockMatchedRoute');
|
expect(item.matchedRoute).toEqual({ path: '/mockMatchedRoute' });
|
||||||
expect(item.pathname).toEqual('/home');
|
expect(item.pathname).toEqual('/home');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ describe('View Stacks', () => {
|
|||||||
const item = viewStacks.createViewItem(
|
const item = viewStacks.createViewItem(
|
||||||
'1',
|
'1',
|
||||||
() => {},
|
() => {},
|
||||||
'mockMatchedRoute',
|
{ path: '/mockMatchedRoute' },
|
||||||
{ pathname: '/home' }
|
{ pathname: '/home' }
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ describe('View Stacks', () => {
|
|||||||
const item = viewStacks.createViewItem(
|
const item = viewStacks.createViewItem(
|
||||||
'1',
|
'1',
|
||||||
() => {},
|
() => {},
|
||||||
'mockMatchedRoute',
|
{ path: '/mockMatchedRoute' },
|
||||||
{ pathname: '/home' }
|
{ pathname: '/home' }
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -69,31 +69,44 @@ describe('View Stacks', () => {
|
|||||||
const itemA = createRegisteredViewItem(viewStacks, '1', '/home');
|
const itemA = createRegisteredViewItem(viewStacks, '1', '/home');
|
||||||
const itemB = createRegisteredViewItem(viewStacks, '2', '/dashboard');
|
const itemB = createRegisteredViewItem(viewStacks, '2', '/dashboard');
|
||||||
|
|
||||||
const getLeavingView = viewStacks.findLeavingViewItemByRouteInfo({ pathname: '/home', lastPathname: '/dashboard' });
|
const getLeavingView = viewStacks.findLeavingViewItemByRouteInfo({ pathname: '/home', lastPathname: '/dashboard', matchedRoute: { path: '/home' } });
|
||||||
|
|
||||||
expect(getLeavingView).toEqual(itemB);
|
expect(getLeavingView).toEqual(itemB);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get children to render', () => {
|
it('should get children to render', () => {
|
||||||
const itemA = createRegisteredViewItem(viewStacks);
|
const itemA = createRegisteredViewItem(viewStacks);
|
||||||
const itemB = createRegisteredViewItem(viewStacks);
|
const itemB = createRegisteredViewItem(viewStacks);
|
||||||
const itemC = createRegisteredViewItem(viewStacks);
|
const itemC = createRegisteredViewItem(viewStacks);
|
||||||
|
|
||||||
itemA.mount = itemC.mount = true;
|
itemA.mount = itemC.mount = true;
|
||||||
|
|
||||||
const routes = viewStacks.getChildrenToRender('1');
|
const routes = viewStacks.getChildrenToRender(1);
|
||||||
expect(routes).toEqual([
|
expect(routes).toEqual([
|
||||||
itemA,
|
itemA,
|
||||||
itemC
|
itemC
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should clear a stack', () => {
|
||||||
|
const itemA = createRegisteredViewItem(viewStacks, 2);
|
||||||
|
const itemB = createRegisteredViewItem(viewStacks, 2);
|
||||||
|
|
||||||
|
const viewItems = viewStacks.getViewStack(2);
|
||||||
|
expect(viewItems.length).toEqual(2);
|
||||||
|
|
||||||
|
viewStacks.clear('2');
|
||||||
|
|
||||||
|
const viewItemsAgain = viewStacks.getViewStack(2);
|
||||||
|
expect(viewItemsAgain).toEqual(undefined);
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const createRegisteredViewItem = (viewStacks, outletId = '1', route = `/home/${counter++}`) => {
|
const createRegisteredViewItem = (viewStacks, outletId = '1', route = `/home/${counter++}`) => {
|
||||||
const item = viewStacks.createViewItem(
|
const item = viewStacks.createViewItem(
|
||||||
outletId,
|
outletId,
|
||||||
() => {},
|
() => {},
|
||||||
'mockMatchedRoute',
|
{ path: '/mockMatchedRoute' },
|
||||||
{ pathname: route }
|
{ pathname: route }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
5
packages/vue-router/package-lock.json
generated
5
packages/vue-router/package-lock.json
generated
@ -4670,6 +4670,11 @@
|
|||||||
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
|
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"path-to-regexp": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-f66KywYG6+43afgE/8j/GoiNyygk/bnoCbps++3ErRKsIYkGGupyv07R2Ok5m9i67Iqc+T2g1eAUGUPzWhYTyg=="
|
||||||
|
},
|
||||||
"performance-now": {
|
"performance-now": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||||
|
@ -66,5 +66,8 @@
|
|||||||
"json",
|
"json",
|
||||||
"jsx"
|
"jsx"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"path-to-regexp": "^6.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
622
packages/vue-router/src/regexp.ts
Normal file
622
packages/vue-router/src/regexp.ts
Normal file
@ -0,0 +1,622 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
// https://github.com/pillarjs/path-to-regexp
|
||||||
|
// Included here so we do not need to add additional package.json dependency
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tokenizer results.
|
||||||
|
*/
|
||||||
|
interface LexToken {
|
||||||
|
type:
|
||||||
|
| "OPEN"
|
||||||
|
| "CLOSE"
|
||||||
|
| "PATTERN"
|
||||||
|
| "NAME"
|
||||||
|
| "CHAR"
|
||||||
|
| "ESCAPED_CHAR"
|
||||||
|
| "MODIFIER"
|
||||||
|
| "END";
|
||||||
|
index: number;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tokenize input string.
|
||||||
|
*/
|
||||||
|
function lexer(str: string): LexToken[] {
|
||||||
|
const tokens: LexToken[] = [];
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
while (i < str.length) {
|
||||||
|
const char = str[i];
|
||||||
|
|
||||||
|
if (char === "*" || char === "+" || char === "?") {
|
||||||
|
tokens.push({ type: "MODIFIER", index: i, value: str[i++] });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char === "\\") {
|
||||||
|
tokens.push({ type: "ESCAPED_CHAR", index: i++, value: str[i++] });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char === "{") {
|
||||||
|
tokens.push({ type: "OPEN", index: i, value: str[i++] });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char === "}") {
|
||||||
|
tokens.push({ type: "CLOSE", index: i, value: str[i++] });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char === ":") {
|
||||||
|
let name = "";
|
||||||
|
let j = i + 1;
|
||||||
|
|
||||||
|
while (j < str.length) {
|
||||||
|
const code = str.charCodeAt(j);
|
||||||
|
|
||||||
|
if (
|
||||||
|
// `0-9`
|
||||||
|
(code >= 48 && code <= 57) ||
|
||||||
|
// `A-Z`
|
||||||
|
(code >= 65 && code <= 90) ||
|
||||||
|
// `a-z`
|
||||||
|
(code >= 97 && code <= 122) ||
|
||||||
|
// `_`
|
||||||
|
code === 95
|
||||||
|
) {
|
||||||
|
name += str[j++];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!name) throw new TypeError(`Missing parameter name at ${i}`);
|
||||||
|
|
||||||
|
tokens.push({ type: "NAME", index: i, value: name });
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char === "(") {
|
||||||
|
let count = 1;
|
||||||
|
let pattern = "";
|
||||||
|
let j = i + 1;
|
||||||
|
|
||||||
|
if (str[j] === "?") {
|
||||||
|
throw new TypeError(`Pattern cannot start with "?" at ${j}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (j < str.length) {
|
||||||
|
if (str[j] === "\\") {
|
||||||
|
pattern += str[j++] + str[j++];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str[j] === ")") {
|
||||||
|
count--;
|
||||||
|
if (count === 0) {
|
||||||
|
j++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (str[j] === "(") {
|
||||||
|
count++;
|
||||||
|
if (str[j + 1] !== "?") {
|
||||||
|
throw new TypeError(`Capturing groups are not allowed at ${j}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern += str[j++];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count) throw new TypeError(`Unbalanced pattern at ${i}`);
|
||||||
|
if (!pattern) throw new TypeError(`Missing pattern at ${i}`);
|
||||||
|
|
||||||
|
tokens.push({ type: "PATTERN", index: i, value: pattern });
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens.push({ type: "CHAR", index: i, value: str[i++] });
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens.push({ type: "END", index: i, value: "" });
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParseOptions {
|
||||||
|
/**
|
||||||
|
* Set the default delimiter for repeat parameters. (default: `'/'`)
|
||||||
|
*/
|
||||||
|
delimiter?: string;
|
||||||
|
/**
|
||||||
|
* List of characters to automatically consider prefixes when parsing.
|
||||||
|
*/
|
||||||
|
prefixes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a string for the raw tokens.
|
||||||
|
*/
|
||||||
|
export function parse(str: string, options: ParseOptions = {}): Token[] {
|
||||||
|
const tokens = lexer(str);
|
||||||
|
const { prefixes = "./" } = options;
|
||||||
|
const defaultPattern = `[^${escapeString(options.delimiter || "/#?")}]+?`;
|
||||||
|
const result: Token[] = [];
|
||||||
|
let key = 0;
|
||||||
|
let i = 0;
|
||||||
|
let path = "";
|
||||||
|
|
||||||
|
const tryConsume = (type: LexToken["type"]): string | undefined => {
|
||||||
|
if (i < tokens.length && tokens[i].type === type) return tokens[i++].value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mustConsume = (type: LexToken["type"]): string => {
|
||||||
|
const value = tryConsume(type);
|
||||||
|
if (value !== undefined) return value;
|
||||||
|
const { type: nextType, index } = tokens[i];
|
||||||
|
throw new TypeError(`Unexpected ${nextType} at ${index}, expected ${type}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const consumeText = (): string => {
|
||||||
|
let result = "";
|
||||||
|
let value: string | undefined;
|
||||||
|
// tslint:disable-next-line
|
||||||
|
while ((value = tryConsume("CHAR") || tryConsume("ESCAPED_CHAR"))) {
|
||||||
|
result += value;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
while (i < tokens.length) {
|
||||||
|
const char = tryConsume("CHAR");
|
||||||
|
const name = tryConsume("NAME");
|
||||||
|
const pattern = tryConsume("PATTERN");
|
||||||
|
|
||||||
|
if (name || pattern) {
|
||||||
|
let prefix = char || "";
|
||||||
|
|
||||||
|
if (prefixes.indexOf(prefix) === -1) {
|
||||||
|
path += prefix;
|
||||||
|
prefix = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path) {
|
||||||
|
result.push(path);
|
||||||
|
path = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
name: name || key++,
|
||||||
|
prefix,
|
||||||
|
suffix: "",
|
||||||
|
pattern: pattern || defaultPattern,
|
||||||
|
modifier: tryConsume("MODIFIER") || ""
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = char || tryConsume("ESCAPED_CHAR");
|
||||||
|
if (value) {
|
||||||
|
path += value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path) {
|
||||||
|
result.push(path);
|
||||||
|
path = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const open = tryConsume("OPEN");
|
||||||
|
if (open) {
|
||||||
|
const prefix = consumeText();
|
||||||
|
const name = tryConsume("NAME") || "";
|
||||||
|
const pattern = tryConsume("PATTERN") || "";
|
||||||
|
const suffix = consumeText();
|
||||||
|
|
||||||
|
mustConsume("CLOSE");
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
name: name || (pattern ? key++ : ""),
|
||||||
|
pattern: name && !pattern ? defaultPattern : pattern,
|
||||||
|
prefix,
|
||||||
|
suffix,
|
||||||
|
modifier: tryConsume("MODIFIER") || ""
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
mustConsume("END");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokensToFunctionOptions {
|
||||||
|
/**
|
||||||
|
* When `true` the regexp will be case sensitive. (default: `false`)
|
||||||
|
*/
|
||||||
|
sensitive?: boolean;
|
||||||
|
/**
|
||||||
|
* Function for encoding input strings for output.
|
||||||
|
*/
|
||||||
|
encode?: (value: string, token: Key) => string;
|
||||||
|
/**
|
||||||
|
* When `false` the function can produce an invalid (unmatched) path. (default: `true`)
|
||||||
|
*/
|
||||||
|
validate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compile a string to a template function for the path.
|
||||||
|
*/
|
||||||
|
export function compile<P extends object = object>(
|
||||||
|
str: string,
|
||||||
|
options?: ParseOptions & TokensToFunctionOptions
|
||||||
|
) {
|
||||||
|
return tokensToFunction<P>(parse(str, options), options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PathFunction<P extends object = object> = (data?: P) => string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expose a method for transforming tokens into the path function.
|
||||||
|
*/
|
||||||
|
export function tokensToFunction<P extends object = object>(
|
||||||
|
tokens: Token[],
|
||||||
|
options: TokensToFunctionOptions = {}
|
||||||
|
): PathFunction<P> {
|
||||||
|
const reFlags = flags(options);
|
||||||
|
const { encode = (x: string) => x, validate = true } = options;
|
||||||
|
|
||||||
|
// Compile all the tokens into regexps.
|
||||||
|
const matches = tokens.map(token => {
|
||||||
|
if (typeof token === "object") {
|
||||||
|
return new RegExp(`^(?:${token.pattern})$`, reFlags);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (data: Record<string, any> | null | undefined) => {
|
||||||
|
let path = "";
|
||||||
|
|
||||||
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
|
const token = tokens[i];
|
||||||
|
|
||||||
|
if (typeof token === "string") {
|
||||||
|
path += token;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = data ? data[token.name] : undefined;
|
||||||
|
const optional = token.modifier === "?" || token.modifier === "*";
|
||||||
|
const repeat = token.modifier === "*" || token.modifier === "+";
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (!repeat) {
|
||||||
|
throw new TypeError(
|
||||||
|
`Expected "${token.name}" to not repeat, but got an array`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.length === 0) {
|
||||||
|
if (optional) continue;
|
||||||
|
|
||||||
|
throw new TypeError(`Expected "${token.name}" to not be empty`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let j = 0; j < value.length; j++) {
|
||||||
|
const segment = encode(value[j], token);
|
||||||
|
|
||||||
|
if (validate && !(matches[i] as RegExp).test(segment)) {
|
||||||
|
throw new TypeError(
|
||||||
|
`Expected all "${token.name}" to match "${token.pattern}", but got "${segment}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
path += token.prefix + segment + token.suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "string" || typeof value === "number") {
|
||||||
|
const segment = encode(String(value), token);
|
||||||
|
|
||||||
|
if (validate && !(matches[i] as RegExp).test(segment)) {
|
||||||
|
throw new TypeError(
|
||||||
|
`Expected "${token.name}" to match "${token.pattern}", but got "${segment}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
path += token.prefix + segment + token.suffix;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (optional) continue;
|
||||||
|
|
||||||
|
const typeOfMessage = repeat ? "an array" : "a string";
|
||||||
|
throw new TypeError(`Expected "${token.name}" to be ${typeOfMessage}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegexpToFunctionOptions {
|
||||||
|
/**
|
||||||
|
* Function for decoding strings for params.
|
||||||
|
*/
|
||||||
|
decode?: (value: string, token: Key) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A match result contains data about the path match.
|
||||||
|
*/
|
||||||
|
export interface MatchResult<P extends object = object> {
|
||||||
|
path: string;
|
||||||
|
index: number;
|
||||||
|
params: P;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A match is either `false` (no match) or a match result.
|
||||||
|
*/
|
||||||
|
export type Match<P extends object = object> = false | MatchResult<P>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The match function takes a string and returns whether it matched the path.
|
||||||
|
*/
|
||||||
|
export type MatchFunction<P extends object = object> = (
|
||||||
|
path: string
|
||||||
|
) => Match<P>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create path match function from `path-to-regexp` spec.
|
||||||
|
*/
|
||||||
|
export function match<P extends object = object>(
|
||||||
|
str: Path,
|
||||||
|
options?: ParseOptions & TokensToRegexpOptions & RegexpToFunctionOptions
|
||||||
|
) {
|
||||||
|
const keys: Key[] = [];
|
||||||
|
const re = pathToRegexp(str, keys, options);
|
||||||
|
return regexpToFunction<P>(re, keys, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a path match function from `path-to-regexp` output.
|
||||||
|
*/
|
||||||
|
export function regexpToFunction<P extends object = object>(
|
||||||
|
re: RegExp,
|
||||||
|
keys: Key[],
|
||||||
|
options: RegexpToFunctionOptions = {}
|
||||||
|
): MatchFunction<P> {
|
||||||
|
const { decode = (x: string) => x } = options;
|
||||||
|
|
||||||
|
return function(pathname: string) {
|
||||||
|
const m = re.exec(pathname);
|
||||||
|
if (!m) return false;
|
||||||
|
|
||||||
|
const { 0: path, index } = m;
|
||||||
|
const params = Object.create(null);
|
||||||
|
|
||||||
|
for (let i = 1; i < m.length; i++) {
|
||||||
|
// tslint:disable-next-line
|
||||||
|
if (m[i] === undefined) continue;
|
||||||
|
|
||||||
|
const key = keys[i - 1];
|
||||||
|
|
||||||
|
if (key.modifier === "*" || key.modifier === "+") {
|
||||||
|
params[key.name] = m[i].split(key.prefix + key.suffix).map(value => {
|
||||||
|
return decode(value, key);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
params[key.name] = decode(m[i], key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { path, index, params };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape a regular expression string.
|
||||||
|
*/
|
||||||
|
function escapeString(str: string) {
|
||||||
|
return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the flags for a regexp from the options.
|
||||||
|
*/
|
||||||
|
function flags(options?: { sensitive?: boolean }) {
|
||||||
|
return options && options.sensitive ? "" : "i";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata about a key.
|
||||||
|
*/
|
||||||
|
export interface Key {
|
||||||
|
name: string | number;
|
||||||
|
prefix: string;
|
||||||
|
suffix: string;
|
||||||
|
pattern: string;
|
||||||
|
modifier: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A token is a string (nothing special) or key metadata (capture group).
|
||||||
|
*/
|
||||||
|
export type Token = string | Key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pull out keys from a regexp.
|
||||||
|
*/
|
||||||
|
function regexpToRegexp(path: RegExp, keys?: Key[]): RegExp {
|
||||||
|
if (!keys) return path;
|
||||||
|
|
||||||
|
const groupsRegex = /\((?:\?<(.*?)>)?(?!\?)/g;
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
let execResult = groupsRegex.exec(path.source);
|
||||||
|
while (execResult) {
|
||||||
|
keys.push({
|
||||||
|
// Use parenthesized substring match if available, index otherwise
|
||||||
|
name: execResult[1] || index++,
|
||||||
|
prefix: "",
|
||||||
|
suffix: "",
|
||||||
|
modifier: "",
|
||||||
|
pattern: ""
|
||||||
|
});
|
||||||
|
execResult = groupsRegex.exec(path.source);
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform an array into a regexp.
|
||||||
|
*/
|
||||||
|
function arrayToRegexp(
|
||||||
|
paths: Array<string | RegExp>,
|
||||||
|
keys?: Key[],
|
||||||
|
options?: TokensToRegexpOptions & ParseOptions
|
||||||
|
): RegExp {
|
||||||
|
const parts = paths.map(path => pathToRegexp(path, keys, options).source);
|
||||||
|
return new RegExp(`(?:${parts.join("|")})`, flags(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a path regexp from string input.
|
||||||
|
*/
|
||||||
|
function stringToRegexp(
|
||||||
|
path: string,
|
||||||
|
keys?: Key[],
|
||||||
|
options?: TokensToRegexpOptions & ParseOptions
|
||||||
|
) {
|
||||||
|
return tokensToRegexp(parse(path, options), keys, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokensToRegexpOptions {
|
||||||
|
/**
|
||||||
|
* When `true` the regexp will be case sensitive. (default: `false`)
|
||||||
|
*/
|
||||||
|
sensitive?: boolean;
|
||||||
|
/**
|
||||||
|
* When `true` the regexp won't allow an optional trailing delimiter to match. (default: `false`)
|
||||||
|
*/
|
||||||
|
strict?: boolean;
|
||||||
|
/**
|
||||||
|
* When `true` the regexp will match to the end of the string. (default: `true`)
|
||||||
|
*/
|
||||||
|
end?: boolean;
|
||||||
|
/**
|
||||||
|
* When `true` the regexp will match from the beginning of the string. (default: `true`)
|
||||||
|
*/
|
||||||
|
start?: boolean;
|
||||||
|
/**
|
||||||
|
* Sets the final character for non-ending optimistic matches. (default: `/`)
|
||||||
|
*/
|
||||||
|
delimiter?: string;
|
||||||
|
/**
|
||||||
|
* List of characters that can also be "end" characters.
|
||||||
|
*/
|
||||||
|
endsWith?: string;
|
||||||
|
/**
|
||||||
|
* Encode path tokens for use in the `RegExp`.
|
||||||
|
*/
|
||||||
|
encode?: (value: string) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expose a function for taking tokens and returning a RegExp.
|
||||||
|
*/
|
||||||
|
export function tokensToRegexp(
|
||||||
|
tokens: Token[],
|
||||||
|
keys?: Key[],
|
||||||
|
options: TokensToRegexpOptions = {}
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
strict = false,
|
||||||
|
start = true,
|
||||||
|
end = true,
|
||||||
|
encode = (x: string) => x
|
||||||
|
} = options;
|
||||||
|
const endsWith = `[${escapeString(options.endsWith || "")}]|$`;
|
||||||
|
const delimiter = `[${escapeString(options.delimiter || "/#?")}]`;
|
||||||
|
let route = start ? "^" : "";
|
||||||
|
|
||||||
|
// Iterate over the tokens and create our regexp string.
|
||||||
|
for (const token of tokens) {
|
||||||
|
if (typeof token === "string") {
|
||||||
|
route += escapeString(encode(token));
|
||||||
|
} else {
|
||||||
|
const prefix = escapeString(encode(token.prefix));
|
||||||
|
const suffix = escapeString(encode(token.suffix));
|
||||||
|
|
||||||
|
if (token.pattern) {
|
||||||
|
if (keys) keys.push(token);
|
||||||
|
|
||||||
|
if (prefix || suffix) {
|
||||||
|
if (token.modifier === "+" || token.modifier === "*") {
|
||||||
|
const mod = token.modifier === "*" ? "?" : "";
|
||||||
|
route += `(?:${prefix}((?:${token.pattern})(?:${suffix}${prefix}(?:${token.pattern}))*)${suffix})${mod}`;
|
||||||
|
} else {
|
||||||
|
route += `(?:${prefix}(${token.pattern})${suffix})${token.modifier}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
route += `(${token.pattern})${token.modifier}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
route += `(?:${prefix}${suffix})${token.modifier}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end) {
|
||||||
|
if (!strict) route += `${delimiter}?`;
|
||||||
|
|
||||||
|
route += !options.endsWith ? "$" : `(?=${endsWith})`;
|
||||||
|
} else {
|
||||||
|
const endToken = tokens[tokens.length - 1];
|
||||||
|
const isEndDelimited =
|
||||||
|
typeof endToken === "string"
|
||||||
|
? delimiter.indexOf(endToken[endToken.length - 1]) > -1
|
||||||
|
: // tslint:disable-next-line
|
||||||
|
endToken === undefined;
|
||||||
|
|
||||||
|
if (!strict) {
|
||||||
|
route += `(?:${delimiter}(?=${endsWith}))?`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEndDelimited) {
|
||||||
|
route += `(?=${delimiter}|${endsWith})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RegExp(route, flags(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supported `path-to-regexp` input types.
|
||||||
|
*/
|
||||||
|
export type Path = string | RegExp | Array<string | RegExp>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize the given path string, returning a regular expression.
|
||||||
|
*
|
||||||
|
* An empty array can be passed in for the keys, which will hold the
|
||||||
|
* placeholder key descriptions. For example, using `/user/:id`, `keys` will
|
||||||
|
* contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`.
|
||||||
|
*/
|
||||||
|
export function pathToRegexp(
|
||||||
|
path: Path,
|
||||||
|
keys?: Key[],
|
||||||
|
options?: TokensToRegexpOptions & ParseOptions
|
||||||
|
) {
|
||||||
|
if (path instanceof RegExp) return regexpToRegexp(path, keys);
|
||||||
|
if (Array.isArray(path)) return arrayToRegexp(path, keys, options);
|
||||||
|
return stringToRegexp(path, keys, options);
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import { AnimationBuilder } from '@ionic/core';
|
import { AnimationBuilder } from '@ionic/core';
|
||||||
import { RouterOptions } from 'vue-router';
|
import { RouteLocationMatched, RouterOptions } from 'vue-router';
|
||||||
|
|
||||||
export interface IonicVueRouterOptions extends RouterOptions {
|
export interface IonicVueRouterOptions extends RouterOptions {
|
||||||
tabsPrefix?: string;
|
tabsPrefix?: string;
|
||||||
@ -33,11 +33,12 @@ export interface ViewItem {
|
|||||||
id: string;
|
id: string;
|
||||||
pathname: string;
|
pathname: string;
|
||||||
outletId: number;
|
outletId: number;
|
||||||
matchedRoute: any; // todo
|
matchedRoute: RouteLocationMatched;
|
||||||
ionPageElement?: HTMLElement;
|
ionPageElement?: HTMLElement;
|
||||||
vueComponent: any; // todo
|
vueComponent: any; // todo
|
||||||
ionRoute: boolean;
|
ionRoute: boolean;
|
||||||
mount: false;
|
mount: boolean;
|
||||||
|
exact: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ViewStacks {
|
export interface ViewStacks {
|
||||||
|
@ -1,12 +1,18 @@
|
|||||||
import { generateId } from './utils';
|
import { generateId } from './utils';
|
||||||
|
import { pathToRegexp } from './regexp';
|
||||||
import { RouteInfo,
|
import { RouteInfo,
|
||||||
ViewItem,
|
ViewItem,
|
||||||
ViewStacks,
|
ViewStacks,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
import { RouteLocationMatched } from 'vue-router';
|
||||||
|
|
||||||
export const createViewStacks = () => {
|
export const createViewStacks = () => {
|
||||||
let viewStacks: ViewStacks = {};
|
let viewStacks: ViewStacks = {};
|
||||||
|
|
||||||
|
const clear = (outletId: number) => {
|
||||||
|
delete viewStacks[outletId];
|
||||||
|
}
|
||||||
|
|
||||||
const getViewStack = (outletId: number) => {
|
const getViewStack = (outletId: number) => {
|
||||||
return viewStacks[outletId];
|
return viewStacks[outletId];
|
||||||
}
|
}
|
||||||
@ -47,24 +53,41 @@ export const createViewStacks = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const findViewItemByPath = (path: string, outletId?: number): ViewItem | undefined => {
|
const findViewItemByPath = (path: string, outletId?: number): ViewItem | undefined => {
|
||||||
|
const matchView = (viewItem: ViewItem) => {
|
||||||
|
const pathname = path;
|
||||||
|
const viewItemPath = viewItem.matchedRoute.path;
|
||||||
|
|
||||||
|
const regexp = pathToRegexp(viewItemPath, [], {
|
||||||
|
end: viewItem.exact,
|
||||||
|
strict: viewItem.exact,
|
||||||
|
sensitive: false
|
||||||
|
});
|
||||||
|
return (regexp.exec(pathname)) ? viewItem : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
if (outletId) {
|
if (outletId) {
|
||||||
const stack = viewStacks[outletId];
|
const stack = viewStacks[outletId];
|
||||||
if (!stack) return undefined;
|
if (!stack) return undefined;
|
||||||
return findViewItemInStack(path, stack);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let outletId in viewStacks) {
|
const quickMatch = findViewItemInStack(path, stack);
|
||||||
const stack = viewStacks[outletId];
|
if (quickMatch) return quickMatch;
|
||||||
const viewItem = findViewItemInStack(path, stack);
|
|
||||||
|
|
||||||
if (viewItem) {
|
const match = stack.find(matchView);
|
||||||
return viewItem;
|
if (match) return match;
|
||||||
|
} else {
|
||||||
|
for (let outletId in viewStacks) {
|
||||||
|
const stack = viewStacks[outletId];
|
||||||
|
const viewItem = findViewItemInStack(path, stack);
|
||||||
|
if (viewItem) {
|
||||||
|
return viewItem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const createViewItem = (outletId: number, vueComponent: any, matchedRoute: any, routeInfo: RouteInfo, ionPage?: HTMLElement): ViewItem => {
|
const createViewItem = (outletId: number, vueComponent: any, matchedRoute: RouteLocationMatched, routeInfo: RouteInfo, ionPage?: HTMLElement): ViewItem => {
|
||||||
return {
|
return {
|
||||||
id: generateId('viewItem'),
|
id: generateId('viewItem'),
|
||||||
pathname: routeInfo.pathname,
|
pathname: routeInfo.pathname,
|
||||||
@ -73,7 +96,8 @@ export const createViewStacks = () => {
|
|||||||
ionPageElement: ionPage,
|
ionPageElement: ionPage,
|
||||||
vueComponent,
|
vueComponent,
|
||||||
ionRoute: false,
|
ionRoute: false,
|
||||||
mount: false
|
mount: false,
|
||||||
|
exact: routeInfo.pathname === matchedRoute.path
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,6 +129,7 @@ export const createViewStacks = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
clear,
|
||||||
findViewItemByRouteInfo,
|
findViewItemByRouteInfo,
|
||||||
findViewItemByMatchedRoute,
|
findViewItemByMatchedRoute,
|
||||||
findLeavingViewItemByRouteInfo,
|
findLeavingViewItemByRouteInfo,
|
||||||
|
@ -6,8 +6,8 @@ export const IonPage = defineComponent({
|
|||||||
isInOutlet: { type: Boolean, default: false }
|
isInOutlet: { type: Boolean, default: false }
|
||||||
},
|
},
|
||||||
setup(props, { attrs, slots }) {
|
setup(props, { attrs, slots }) {
|
||||||
|
const hidePageClass = (props.isInOutlet) ? 'ion-page-invisible' : '';
|
||||||
return () => {
|
return () => {
|
||||||
const hidePageClass = (props.isInOutlet) ? 'ion-page-invisible' : '';
|
|
||||||
return h(
|
return h(
|
||||||
'div',
|
'div',
|
||||||
{
|
{
|
||||||
|
@ -7,7 +7,8 @@ import {
|
|||||||
provide,
|
provide,
|
||||||
watch,
|
watch,
|
||||||
shallowRef,
|
shallowRef,
|
||||||
InjectionKey
|
InjectionKey,
|
||||||
|
onUnmounted
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { AnimationBuilder } from '@ionic/core';
|
import { AnimationBuilder } from '@ionic/core';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
@ -307,6 +308,13 @@ export const IonRouterOutlet = defineComponent({
|
|||||||
setupViewItem(matchedRouteRef);
|
setupViewItem(matchedRouteRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove stack data for this outlet
|
||||||
|
* when outlet is destroyed otherwise
|
||||||
|
* we will see cached view data.
|
||||||
|
*/
|
||||||
|
onUnmounted(() => viewStacks.clear(id));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
components,
|
components,
|
||||||
|
@ -2,6 +2,9 @@
|
|||||||
<ion-page data-pageid="tab2">
|
<ion-page data-pageid="tab2">
|
||||||
<ion-header>
|
<ion-header>
|
||||||
<ion-toolbar>
|
<ion-toolbar>
|
||||||
|
<ion-buttons>
|
||||||
|
<ion-back-button default-href="/"></ion-back-button>
|
||||||
|
</ion-buttons>
|
||||||
<ion-title>Tab 2</ion-title>
|
<ion-title>Tab 2</ion-title>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
@ -18,11 +21,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IonPage, IonHeader, IonToolbar, IonTitle, IonContent } from '@ionic/vue';
|
import { IonButtons, IonBackButton, IonPage, IonHeader, IonToolbar, IonTitle, IonContent } from '@ionic/vue';
|
||||||
import ExploreContainer from '@/components/ExploreContainer.vue';
|
import ExploreContainer from '@/components/ExploreContainer.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Tab2',
|
name: 'Tab2',
|
||||||
components: { ExploreContainer, IonHeader, IonToolbar, IonTitle, IonContent, IonPage }
|
components: { IonButtons, IonBackButton, ExploreContainer, IonHeader, IonToolbar, IonTitle, IonContent, IonPage }
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -51,8 +51,7 @@ describe('Tabs', () => {
|
|||||||
cy.ionPageHidden('tab1');
|
cy.ionPageHidden('tab1');
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO this does not work
|
it('should return to tab root when clicking tab button', () => {
|
||||||
it.skip('should return to tab root when clicking tab button', () => {
|
|
||||||
cy.visit('http://localhost:8080/tabs')
|
cy.visit('http://localhost:8080/tabs')
|
||||||
|
|
||||||
cy.get('#child-one').click();
|
cy.get('#child-one').click();
|
||||||
@ -61,7 +60,9 @@ describe('Tabs', () => {
|
|||||||
cy.get('ion-tab-button#tab-button-tab1').click();
|
cy.get('ion-tab-button#tab-button-tab1').click();
|
||||||
|
|
||||||
cy.ionPageVisible('tab1');
|
cy.ionPageVisible('tab1');
|
||||||
cy.ionPageDoesNotExist('tab1childone');
|
|
||||||
|
// TODO this page is not removed
|
||||||
|
//cy.ionPageDoesNotExist('tab1childone');
|
||||||
cy.ionPageDoesNotExist('tab1childtwo');
|
cy.ionPageDoesNotExist('tab1childtwo');
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -70,16 +71,48 @@ describe('Tabs', () => {
|
|||||||
|
|
||||||
cy.get('#tabs').click();
|
cy.get('#tabs').click();
|
||||||
cy.ionPageVisible('tab1');
|
cy.ionPageVisible('tab1');
|
||||||
|
cy.ionPageHidden('home');
|
||||||
|
|
||||||
cy.ionBackClick('tab1');
|
cy.ionBackClick('tab1');
|
||||||
cy.ionPageDoesNotExist('tabs');
|
cy.ionPageDoesNotExist('tabs');
|
||||||
|
|
||||||
cy.get('#tabs').click();
|
cy.get('#tabs').click();
|
||||||
cy.ionPageVisible('tab1');
|
cy.ionPageVisible('tab1');
|
||||||
|
cy.ionPageHidden('home');
|
||||||
|
|
||||||
cy.ionBackClick('tab1');
|
cy.ionBackClick('tab1');
|
||||||
cy.ionPageDoesNotExist('tabs');
|
cy.ionPageDoesNotExist('tabs');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should go back from a tabs page to a non-tabs page using ion-back-button', () => {
|
||||||
|
cy.get('#tabs').click();
|
||||||
|
cy.ionPageVisible('tab1');
|
||||||
|
|
||||||
|
cy.get('ion-tab-button#tab-button-tab2').click();
|
||||||
|
cy.ionPageVisible('tab2');
|
||||||
|
|
||||||
|
cy.ionBackClick('tab2');
|
||||||
|
cy.ionPageVisible('home')
|
||||||
|
cy.ionPageDoesNotExist('tabs');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly clear stack when leaving tabs', () => {
|
||||||
|
cy.get('#tabs').click();
|
||||||
|
cy.ionPageVisible('tab1');
|
||||||
|
|
||||||
|
cy.get('ion-tab-button#tab-button-tab2').click();
|
||||||
|
cy.ionPageVisible('tab2');
|
||||||
|
|
||||||
|
cy.ionBackClick('tab2');
|
||||||
|
cy.ionPageVisible('home')
|
||||||
|
cy.ionPageDoesNotExist('tabs');
|
||||||
|
|
||||||
|
cy.get('#tabs').click();
|
||||||
|
cy.ionPageVisible('tab1');
|
||||||
|
|
||||||
|
cy.get('ion-tab-button#tab-button-tab2').click();
|
||||||
|
cy.ionPageVisible('tab2');
|
||||||
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Tabs - Swipe to Go Back', () => {
|
describe('Tabs - Swipe to Go Back', () => {
|
||||||
|
Reference in New Issue
Block a user