diff --git a/etc/brick-types.api.md b/etc/brick-types.api.md index 084ed2cbfa..f914def15e 100644 --- a/etc/brick-types.api.md +++ b/etc/brick-types.api.md @@ -1160,12 +1160,12 @@ export interface ExtendedHistory { // @internal (undocumented) getBlockMessage: () => string; // Warning: (ae-incompatible-release-tags) The symbol "push" is marked as @public, but its signature references "PluginHistoryState" which is marked as @internal - push?: History_2["push"]; + push?: (location: LocationDescriptor, state?: PluginHistoryState, callback?: (blocked: boolean) => void) => void; pushAnchor: UpdateAnchorFunction; pushQuery: UpdateQueryFunction; - reload: () => void; + reload: (callback?: (blocked: boolean) => void) => void; // Warning: (ae-incompatible-release-tags) The symbol "replace" is marked as @public, but its signature references "PluginHistoryState" which is marked as @internal - replace?: History_2["replace"]; + replace?: (location: LocationDescriptor, state?: PluginHistoryState, callback?: (blocked: boolean) => void) => void; replaceQuery: UpdateQueryFunction; // @internal (undocumented) setBlockMessage: (message: string) => void; @@ -2436,10 +2436,10 @@ export interface TransformMap { // Warning: (ae-incompatible-release-tags) The symbol "UpdateAnchorFunction" is marked as @public, but its signature references "PluginHistoryState" which is marked as @internal // // @public -export type UpdateAnchorFunction = (hash: string, state?: PluginHistoryState) => void; +export type UpdateAnchorFunction = (hash: string, state?: PluginHistoryState, callback?: (blocked: boolean) => void) => void; // @public -export type UpdateQueryFunction = (query: Record, options?: UpdateQueryOptions) => void; +export type UpdateQueryFunction = (query: Record, options?: UpdateQueryOptions, callback?: (blocked: boolean) => void) => void; // Warning: (ae-incompatible-release-tags) The symbol "UpdateQueryOptions" is marked as @public, but its signature references "PluginHistoryState" which is marked as @internal // diff --git a/packages/brick-kit/src/core/LocationContext.ts b/packages/brick-kit/src/core/LocationContext.ts index 6dd6fc8433..53e83c3dd6 100644 --- a/packages/brick-kit/src/core/LocationContext.ts +++ b/packages/brick-kit/src/core/LocationContext.ts @@ -72,7 +72,6 @@ import { preConstructMenus } from "../internal/menu"; import { Media } from "../internal/mediaQuery"; import { getReadOnlyProxy } from "../internal/proxyFactories"; import { customTemplateRegistry } from "./CustomTemplates/constants"; -import { CustomTemplate } from "../../../brick-types/dist/types/manifest"; import { ExpandCustomForm, formDataProperties, diff --git a/packages/brick-kit/src/history.ts b/packages/brick-kit/src/history.ts index 3b0b1a65aa..a24762d6a9 100644 --- a/packages/brick-kit/src/history.ts +++ b/packages/brick-kit/src/history.ts @@ -1,6 +1,9 @@ import { createBrowserHistory } from "history"; import { PluginHistory } from "@next-core/brick-types"; -import { historyExtended } from "./internal/historyExtended"; +import { + getUserConfirmation, + historyExtended, +} from "./internal/historyExtended"; import { getBasePath } from "./internal/getBasePath"; let history: PluginHistory; @@ -9,6 +12,7 @@ let history: PluginHistory; export function createHistory(): PluginHistory { const browserHistory = createBrowserHistory({ basename: getBasePath().replace(/\/$/, ""), + getUserConfirmation, }); Object.assign(browserHistory, historyExtended(browserHistory)); history = browserHistory as PluginHistory; diff --git a/packages/brick-kit/src/internal/bindListeners.spec.ts b/packages/brick-kit/src/internal/bindListeners.spec.ts index 0032030003..7517b696f7 100644 --- a/packages/brick-kit/src/internal/bindListeners.spec.ts +++ b/packages/brick-kit/src/internal/bindListeners.spec.ts @@ -50,8 +50,12 @@ customElements.define( ); const mockHistory = { - push: jest.fn(), - replace: jest.fn(), + push: jest.fn((loc, state, callback) => { + callback?.(true); + }), + replace: jest.fn((loc, state, callback) => { + callback?.(false); + }), pushQuery: jest.fn(), replaceQuery: jest.fn(), pushAnchor: jest.fn(), @@ -312,10 +316,40 @@ describe("bindListeners", () => { window.parent.postMessage = jest.fn(); const eventsMap: BrickEventsMap = { key1: [ - { action: "history.push" }, + { + action: "history.push", + callback: { + success: { + action: "console.info", + args: ["<% `history.push:success:${EVENT.detail.blocked}` %>"], + }, + error: { + action: "console.info", + args: ["<% `history.push:error:${EVENT.detail.blocked}` %>"], + }, + finally: { + action: "console.info", + args: ["<% `history.push:finally:${EVENT.detail.blocked}` %>"], + }, + }, + }, { action: "history.replace", args: ["specified args for history.replace"], + callback: { + success: { + action: "console.info", + args: ["<% `history.replace:success:${EVENT.detail.blocked}` %>"], + }, + error: { + action: "console.info", + args: ["<% `history.replace:error:${EVENT.detail.blocked}` %>"], + }, + finally: { + action: "console.info", + args: ["<% `history.replace:finally:${EVENT.detail.blocked}` %>"], + }, + }, }, { action: "history.pushQuery", @@ -366,7 +400,24 @@ describe("bindListeners", () => { args: [true], }, { action: "location.assign", args: ["www.baidu.com"] }, - { action: "segue.push", args: ["testSegueIdA"] }, + { + action: "segue.push", + args: ["testSegueIdA"], + callback: { + success: { + action: "console.info", + args: ["<% `segue.push:success:${EVENT.detail.blocked}` %>"], + }, + error: { + action: "console.info", + args: ["<% `segue.push:error:${EVENT.detail.blocked}` %>"], + }, + finally: { + action: "console.info", + args: ["<% `segue.push:finally:${EVENT.detail.blocked}` %>"], + }, + }, + }, { action: "segue.replace", args: ["testSegueIdB", { id: "${EVENT.detail}" }], @@ -685,8 +736,18 @@ describe("bindListeners", () => { expect(sessionStorage.removeItem).toBeCalledWith("foo"); const history = mockHistory; - expect(history.push).toHaveBeenNthCalledWith(1, "for-good"); - expect(history.push).toHaveBeenNthCalledWith(2, "/segue-target-a"); + expect(history.push).toHaveBeenNthCalledWith( + 1, + "for-good", + undefined, + expect.any(Function) + ); + expect(history.push).toHaveBeenNthCalledWith( + 2, + "/segue-target-a", + undefined, + expect.any(Function) + ); expect(history.push).toHaveBeenNthCalledWith(3, "/mock/alias/a"); expect(history.pushQuery).toBeCalledWith( { @@ -702,20 +763,27 @@ describe("bindListeners", () => { ); expect(history.replace).toHaveBeenNthCalledWith( 1, - "specified args for history.replace" + "specified args for history.replace", + undefined, + expect.any(Function) ); expect(history.replace).toHaveBeenNthCalledWith( 2, - "/segue-target-b/for-good" + "/segue-target-b/for-good", + undefined, + undefined ); expect(history.replace).toHaveBeenNthCalledWith( 3, "/mock/alias/b/for-good" ); - expect(history.replaceQuery).toBeCalledWith({ - page: 1, - }); - expect(history.pushAnchor).toBeCalledWith("yes"); + expect(history.replaceQuery).toBeCalledWith( + { + page: 1, + }, + undefined + ); + expect(history.pushAnchor).toBeCalledWith("yes", undefined); expect(history.goBack).toBeCalledWith(); expect(history.goForward).toBeCalledWith(); expect(history.reload).toBeCalled(); @@ -758,10 +826,25 @@ describe("bindListeners", () => { ); expect((console.log as jest.Mock).mock.calls[3][0].detail).toBe("resolved"); - expect(console.info).toBeCalledTimes(4); - expect(console.info).toHaveBeenNthCalledWith(1, expectEvent(event1)); + expect(console.info).toBeCalledTimes(10); + expect(console.info).toHaveBeenNthCalledWith(1, "history.push:error:true"); expect(console.info).toHaveBeenNthCalledWith( 2, + "history.push:finally:true" + ); + expect(console.info).toHaveBeenNthCalledWith( + 3, + "history.replace:success:false" + ); + expect(console.info).toHaveBeenNthCalledWith( + 4, + "history.replace:finally:false" + ); + expect(console.info).toHaveBeenNthCalledWith(5, "segue.push:error:true"); + expect(console.info).toHaveBeenNthCalledWith(6, "segue.push:finally:true"); + expect(console.info).toHaveBeenNthCalledWith(7, expectEvent(event1)); + expect(console.info).toHaveBeenNthCalledWith( + 8, expectEvent( new CustomEvent("callback.finally", { detail: undefined, @@ -769,7 +852,7 @@ describe("bindListeners", () => { ) ); expect(console.info).toHaveBeenNthCalledWith( - 3, + 9, expectEvent( new CustomEvent("callback.progress", { detail: "progressing", @@ -777,7 +860,7 @@ describe("bindListeners", () => { ) ); expect(console.info).toHaveBeenNthCalledWith( - 4, + 10, expectEvent( new CustomEvent("callback.progress", { detail: "resolved", diff --git a/packages/brick-kit/src/internal/bindListeners.ts b/packages/brick-kit/src/internal/bindListeners.ts index c84d9033f6..5ae14c22b3 100644 --- a/packages/brick-kit/src/internal/bindListeners.ts +++ b/packages/brick-kit/src/internal/bindListeners.ts @@ -30,7 +30,7 @@ import { getMessageDispatcher } from "../core/MessageDispatcher"; import { PluginWebSocketMessageTopic } from "../websocket/interfaces"; import { applyTheme, applyMode } from "../themeAndMode"; import { clearMenuTitleCache, clearMenuCache } from "./menu"; -import { PollableCallback, PollableCallbackFunction, startPoll } from "./poll"; +import { PollableCallback, startPoll } from "./poll"; import { getArgsOfCustomApi } from "../core/FlowApi"; import { getRuntime } from "../runtime"; import { @@ -125,19 +125,15 @@ export function listenerFactory( case "history.replaceQuery": case "history.pushAnchor": case "history.block": - return builtinHistoryListenerFactory( - method, - handler.args, - handler, - context - ); case "history.goBack": case "history.goForward": case "history.reload": case "history.unblock": - return builtinHistoryWithoutArgsListenerFactory( + return builtinHistoryListenerFactory( method, + handler.args, handler, + handler.callback, context ); case "segue.push": @@ -146,6 +142,7 @@ export function listenerFactory( method, handler.args, handler, + handler.callback, context ); case "alias.push": @@ -428,6 +425,7 @@ function builtinSegueListenerFactory( method: "push" | "replace", args: unknown[], ifContainer: IfContainer, + callback: BrickEventHandlerCallback, context: PluginRuntimeContext ): EventListener { return function (event: CustomEvent): void { @@ -443,7 +441,19 @@ function builtinSegueListenerFactory( ...(argsFactory(args, context, event) as Parameters< ReturnType >) - ) + ), + undefined, + callback + ? (blocked) => { + const callbackFactory = eventCallbackFactory( + callback, + () => context, + null + ); + callbackFactory(blocked ? "error" : "success")({ blocked }); + callbackFactory("finally")({ blocked }); + } + : undefined ); } as EventListener; } @@ -759,37 +769,61 @@ function builtinHistoryListenerFactory( | "pushQuery" | "replaceQuery" | "pushAnchor" - | "block", + | "block" + | "goBack" + | "goForward" + | "reload" + | "unblock", args: unknown[], ifContainer: IfContainer, + callback: BrickEventHandlerCallback, context: PluginRuntimeContext ): EventListener { return function (event: CustomEvent): void { if (!looseCheckIf(ifContainer, { ...context, event })) { return; } - ( - getHistory()[method === "block" ? "setBlockMessage" : method] as ( - ...args: unknown[] - ) => unknown - )( - ...argsFactory(args, context, event, { + let baseArgsLength = 0; + let hasCallback = false; + let overrideMethod = method as "setBlockMessage"; + switch (method) { + case "push": + case "replace": + case "pushQuery": + case "replaceQuery": + case "pushAnchor": + baseArgsLength = 2; + hasCallback = true; + break; + case "reload": + hasCallback = true; + break; + case "block": + baseArgsLength = 1; + overrideMethod = "setBlockMessage"; + break; + } + let computedArgs: unknown[] = []; + if (baseArgsLength > 0) { + computedArgs = argsFactory(args, context, event, { useEventDetailAsDefault: true, - }) - ); - } as EventListener; -} - -function builtinHistoryWithoutArgsListenerFactory( - method: "goBack" | "goForward" | "reload" | "unblock", - ifContainer: IfContainer, - context: PluginRuntimeContext -): EventListener { - return function (event: CustomEvent): void { - if (!looseCheckIf(ifContainer, { ...context, event })) { - return; + }); + computedArgs.length = baseArgsLength; } - getHistory()[method](); + if (hasCallback && callback) { + const callbackFactory = eventCallbackFactory( + callback, + () => context, + null + ); + computedArgs.push((blocked: boolean) => { + callbackFactory(blocked ? "error" : "success")({ blocked }); + callbackFactory("finally")({ blocked }); + }); + } + (getHistory()[overrideMethod] as (...args: unknown[]) => unknown)( + ...computedArgs + ); } as EventListener; } diff --git a/packages/brick-kit/src/internal/historyExtended.spec.ts b/packages/brick-kit/src/internal/historyExtended.spec.ts index ab86030c3a..e319bb1480 100644 --- a/packages/brick-kit/src/internal/historyExtended.spec.ts +++ b/packages/brick-kit/src/internal/historyExtended.spec.ts @@ -5,7 +5,7 @@ import { PluginHistoryState, } from "@next-core/brick-types"; import { History } from "history"; -import { historyExtended } from "./historyExtended"; +import { getUserConfirmation, historyExtended } from "./historyExtended"; import { _internalApiHasMatchedApp } from "../core/Runtime"; jest.mock("../core/Runtime", () => ({ @@ -41,8 +41,6 @@ describe("historyExtended", () => { let ext: ReturnType; afterEach(() => { - // history.push.mockClear(); - // history.replace.mockClear(); jest.clearAllMocks(); window.STANDALONE_MICRO_APPS = undefined; }); @@ -220,23 +218,44 @@ describe("historyExtended", () => { (callerArgs, loc) => { ext = historyExtended(history); ext.pushAnchor(...callerArgs); - expect(history.push).toBeCalledWith(loc); + expect(history.push).toBeCalledWith(loc, undefined); } ); it("should work for history.reload", () => { ext = historyExtended(history); - ext.reload(); - expect(history.replace).toBeCalledWith({ - pathname: "/a", - search: "?b=1", - hash: "#c", - key: "d", - state: { - from: "e", - notify: true, + const callback = jest.fn(); + ext.reload(callback); + expect(history.replace).toBeCalledWith( + { + pathname: "/a", + search: "?b=1", + hash: "#c", + key: "d", + state: { + from: "e", + notify: true, + }, }, - }); + undefined + ); + expect(callback).toBeCalledWith(false); + }); + + it("should work for callback of history.push", () => { + ext = historyExtended(history); + const callback = jest.fn(); + ext.push("/a", undefined, callback); + expect(history.push).toBeCalledWith("/a", undefined); + expect(callback).toBeCalledWith(false); + }); + + it("should work for callback of history.replace", () => { + ext = historyExtended(history); + const callback = jest.fn(); + ext.replace("/a", { notify: false }, callback); + expect(history.replace).toBeCalledWith("/a", { notify: false }); + expect(callback).toBeCalledWith(false); }); it.each< @@ -312,3 +331,12 @@ describe("historyExtended", () => { } ); }); + +describe("getUserConfirmation", () => { + it("should work", () => { + const callback = jest.fn(); + jest.spyOn(window, "confirm").mockReturnValueOnce(true); + getUserConfirmation("hello", callback); + expect(callback).toBeCalledWith(true); + }); +}); diff --git a/packages/brick-kit/src/internal/historyExtended.ts b/packages/brick-kit/src/internal/historyExtended.ts index deb29fd5c6..346c6f131f 100644 --- a/packages/brick-kit/src/internal/historyExtended.ts +++ b/packages/brick-kit/src/internal/historyExtended.ts @@ -1,28 +1,58 @@ -import { History, LocationDescriptorObject, parsePath } from "history"; +import { + History, + LocationDescriptor, + LocationDescriptorObject, + parsePath, +} from "history"; import { PluginHistoryState, ExtendedHistory, UpdateQueryFunction, - UpdateQueryOptions, - UpdateAnchorFunction, } from "@next-core/brick-types"; import { getBasePath } from "./getBasePath"; import { _internalApiHasMatchedApp } from "../core/Runtime"; +let blocked = false; +export function getUserConfirmation( + message: string, + callback: (result: boolean) => void +): void { + blocked = !confirm(message); + callback(!blocked); +} + export function historyExtended( browserHistory: History ): ExtendedHistory { const { push: originalPush, replace: originalReplace } = browserHistory; + + function push( + location: LocationDescriptor, + state?: PluginHistoryState, + callback?: (blocked: boolean) => void + ): void { + blocked = false; + originalPush(location, state); + callback?.(blocked); + } + + function replace( + location: LocationDescriptor, + state?: PluginHistoryState, + callback?: (blocked: boolean) => void + ): void { + blocked = false; + originalReplace(location, state); + callback?.(blocked); + } + function updateQueryFactory(method: "push" | "replace"): UpdateQueryFunction { - return function updateQuery( - query: Record, - options: UpdateQueryOptions = {} - ): void { + return function updateQuery(query, options = {}, callback?): void { const { extraQuery, clear, keepHash, ...state } = options; const urlSearchParams = new URLSearchParams( clear ? "" : browserHistory.location.search ); - const params: Record = {}; + const params: Record = {}; Object.assign(params, query, extraQuery); for (const [key, value] of Object.entries(params)) { if (Array.isArray(value)) { @@ -36,23 +66,23 @@ export function historyExtended( urlSearchParams.set(key, value); } } - (method === "push" ? originalPush : originalReplace)( + (method === "push" ? push : replace)( `?${urlSearchParams.toString()}${ keepHash ? browserHistory.location.hash : "" }`, - state + state, + callback ); }; } - function updateAnchorFactory( - method: "push" /* | "replace" */ - ): UpdateAnchorFunction { - return function updateAnchor( - hash: string, - state?: PluginHistoryState - ): void { - (method === "push" ? originalPush : originalReplace)({ + function pushAnchor( + hash: string, + state?: PluginHistoryState, + callback?: (blocked: boolean) => void + ): void { + push( + { ...browserHistory.location, key: undefined, hash, @@ -61,19 +91,25 @@ export function historyExtended( notify: false, ...state, }, - }); - }; + }, + undefined, + callback + ); } - function reload(): void { - originalReplace({ - ...browserHistory.location, - state: { - ...browserHistory.location.state, - // Always notify - notify: true, + function reload(callback?: (blocked: boolean) => void): void { + replace( + { + ...browserHistory.location, + state: { + ...browserHistory.location.state, + // Always notify + notify: true, + }, }, - }); + undefined, + callback + ); } let blockMessage: string; @@ -93,15 +129,16 @@ export function historyExtended( return { pushQuery: updateQueryFactory("push"), replaceQuery: updateQueryFactory("replace"), - pushAnchor: updateAnchorFactory("push"), - // replaceAnchor: updateAnchorFactory("replace"), + pushAnchor: pushAnchor, reload, setBlockMessage, getBlockMessage, unblock, + push, + replace, ...(window.STANDALONE_MICRO_APPS - ? standaloneHistoryOverridden(browserHistory) - : null), + ? standaloneHistoryOverridden({ ...browserHistory, push, replace }) + : {}), }; } @@ -111,13 +148,12 @@ export function historyExtended( * when `push` or `replace` to other apps, force page refresh. */ function standaloneHistoryOverridden( - browserHistory: History -): Pick, "push" | "replace"> { + browserHistory: History & + Pick +): Pick { const { push: originalPush, replace: originalReplace } = browserHistory; - function updateFactory( - method: "push" | "replace" - ): History["push"] { - return function update(path, state?) { + function updateFactory(method: "push" | "replace"): ExtendedHistory["push"] { + return function update(path, state?, callback?) { let pathname: string; const pathIsString = typeof path === "string"; if (pathIsString) { @@ -128,7 +164,8 @@ function standaloneHistoryOverridden( if (pathname === "" || _internalApiHasMatchedApp(pathname)) { return (method === "push" ? originalPush : originalReplace)( path as string, - state + state, + callback ); } // Going to outside apps. diff --git a/packages/brick-types/src/runtime.ts b/packages/brick-types/src/runtime.ts index 56d89db650..ab618c815e 100644 --- a/packages/brick-types/src/runtime.ts +++ b/packages/brick-types/src/runtime.ts @@ -102,7 +102,7 @@ export interface ExtendedHistory { pushAnchor: UpdateAnchorFunction; /** 重载当前会话,即触发页面重新渲染。与 location.reload() 不同,它不会触发浏览器页面的重载。 */ - reload: () => void; + reload: (callback?: (blocked: boolean) => void) => void; /** @internal */ setBlockMessage: (message: string) => void; @@ -114,10 +114,18 @@ export interface ExtendedHistory { unblock: () => void; /** 推入一条记录。*/ - push?: History["push"]; + push?: ( + location: LocationDescriptor, + state?: PluginHistoryState, + callback?: (blocked: boolean) => void + ) => void; /** 替换一条记录。*/ - replace?: History["replace"]; + replace?: ( + location: LocationDescriptor, + state?: PluginHistoryState, + callback?: (blocked: boolean) => void + ) => void; } /** @@ -128,7 +136,8 @@ export interface ExtendedHistory { */ export type UpdateQueryFunction = ( query: Record, - options?: UpdateQueryOptions + options?: UpdateQueryOptions, + callback?: (blocked: boolean) => void ) => void; /** 更新 query 参数时的选项 */ @@ -150,7 +159,8 @@ export interface UpdateQueryOptions extends PluginHistoryState { */ export type UpdateAnchorFunction = ( hash: string, - state?: PluginHistoryState + state?: PluginHistoryState, + callback?: (blocked: boolean) => void ) => void; /** @internal */