feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Disposable, DisposableCollection, Emitter } from '@flowgram-adapter/common';
|
||||
|
||||
import { type URI } from '../common';
|
||||
|
||||
export interface HistoryState {
|
||||
/**
|
||||
* uri.toString()
|
||||
*/
|
||||
uri?: string;
|
||||
|
||||
/**
|
||||
* 在栈中的索引
|
||||
* react-router 使用「idx」,使用「fIdx」防止串数据
|
||||
*/
|
||||
fIdx?: number;
|
||||
}
|
||||
|
||||
const POP_STATE_EVENT_TYPE = 'popstate';
|
||||
|
||||
const uriToUrl = (uri: URI) => {
|
||||
let url = uri.path.toString();
|
||||
const { query } = uri;
|
||||
if (query) {
|
||||
url += `?${query}`;
|
||||
}
|
||||
const hash = uri.fragment;
|
||||
if (hash) {
|
||||
url += '#hash';
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
export class BrowserHistory implements Disposable {
|
||||
private window = document.defaultView || window;
|
||||
|
||||
private history = this.window.history;
|
||||
|
||||
private onChangeEmitter = new Emitter<HistoryState>();
|
||||
|
||||
onChange = this.onChangeEmitter.event;
|
||||
|
||||
private disposable = new DisposableCollection(this.onChangeEmitter);
|
||||
|
||||
private get state() {
|
||||
return this.history.state;
|
||||
}
|
||||
|
||||
private get idx() {
|
||||
return this.state?.fIdx || null;
|
||||
}
|
||||
|
||||
private listener = (e: any) => {
|
||||
this.onChangeEmitter.fire((e.state || {}) as HistoryState);
|
||||
};
|
||||
|
||||
init() {
|
||||
/**
|
||||
* 初始化的时候 index 必然为 null
|
||||
* 如果不是,说明有别的框架或者在直接使用 history.pushState 或者 history.replaceState 污染
|
||||
*/
|
||||
if (this.idx === null) {
|
||||
this.history.replaceState({ ...this.state, fIdx: -1, uri: '' }, '');
|
||||
}
|
||||
this.window.addEventListener(POP_STATE_EVENT_TYPE, this.listener);
|
||||
this.disposable.push(
|
||||
Disposable.create(() =>
|
||||
this.window.removeEventListener(POP_STATE_EVENT_TYPE, this.listener),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
push(to: URI, idx = this.idx + 1) {
|
||||
const state = {
|
||||
uri: to.toString(),
|
||||
fIdx: idx,
|
||||
};
|
||||
const url = uriToUrl(to);
|
||||
try {
|
||||
this.history.pushState(state, '', url);
|
||||
} catch {
|
||||
/**
|
||||
* history 还是有可能因为 state 或者浏览器等原因挂掉的
|
||||
* 降级成直接跳转
|
||||
*/
|
||||
this.window.location.assign(url);
|
||||
}
|
||||
}
|
||||
|
||||
replace(to: URI, idx = this.idx) {
|
||||
const state = {
|
||||
uri: toString(),
|
||||
fIdx: idx,
|
||||
};
|
||||
const url = uriToUrl(to);
|
||||
this.history.replaceState(state, '', url);
|
||||
}
|
||||
|
||||
go(n: number) {
|
||||
return this.history.go(n);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposable.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { bindContributions } from '@flowgram-adapter/common';
|
||||
|
||||
import { ShortcutsContribution } from '../shortcut/shortcuts-service';
|
||||
import { definePluginCreator, LifecycleContribution } from '../common';
|
||||
import { CommandContribution } from '../command';
|
||||
import { NavigationService } from './navigation-service';
|
||||
import { NavigationHistory } from './navigation-history';
|
||||
import { NavigationContribution } from './navigation-contribution';
|
||||
|
||||
export interface NavigationPluginOptions {
|
||||
uriScheme?: string;
|
||||
}
|
||||
|
||||
export const createNavigationPlugin =
|
||||
definePluginCreator<NavigationPluginOptions>({
|
||||
onBind: ({ bind }) => {
|
||||
bind(NavigationHistory).toSelf().inSingletonScope();
|
||||
bind(NavigationService).toSelf().inSingletonScope();
|
||||
bindContributions(bind, NavigationContribution, [
|
||||
LifecycleContribution,
|
||||
CommandContribution,
|
||||
ShortcutsContribution,
|
||||
]);
|
||||
},
|
||||
onInit(ctx, opts) {
|
||||
if (opts.uriScheme) {
|
||||
ctx.container.get(NavigationService).setScheme(opts.uriScheme);
|
||||
}
|
||||
},
|
||||
});
|
||||
22
frontend/packages/project-ide/core/src/navigation/index.ts
Normal file
22
frontend/packages/project-ide/core/src/navigation/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { NavigationService } from './navigation-service';
|
||||
export { NavigationHistory } from './navigation-history';
|
||||
export {
|
||||
createNavigationPlugin,
|
||||
type NavigationPluginOptions,
|
||||
} from './create-navigation-plugin';
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { DisposableCollection, addEventListener } from '@flowgram-adapter/common';
|
||||
|
||||
import {
|
||||
type ShortcutsContribution,
|
||||
type ShortcutsRegistry,
|
||||
} from '../shortcut/shortcuts-service';
|
||||
import { type LifecycleContribution } from '../common';
|
||||
import { type CommandContribution, type CommandRegistry } from '../command';
|
||||
import { NavigationService } from './navigation-service';
|
||||
|
||||
@injectable()
|
||||
class NavigationContribution
|
||||
implements LifecycleContribution, CommandContribution, ShortcutsContribution
|
||||
{
|
||||
@inject(NavigationService)
|
||||
protected readonly navigationService: NavigationService;
|
||||
|
||||
private readonly _toDispose = new DisposableCollection();
|
||||
|
||||
onLayoutInit() {
|
||||
// this.registerMouseNavigationListener();
|
||||
}
|
||||
|
||||
onStart() {
|
||||
this.navigationService.init();
|
||||
}
|
||||
|
||||
onDispose(): void {
|
||||
this.navigationService.dispose();
|
||||
this._toDispose.dispose();
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry) {
|
||||
registry.registerCommand(
|
||||
{
|
||||
id: 'navigation.forward',
|
||||
label: 'Forward',
|
||||
},
|
||||
{
|
||||
execute: () => {
|
||||
this.navigationService.forward();
|
||||
},
|
||||
isEnabled: () => this.navigationService.canGoForward(),
|
||||
},
|
||||
);
|
||||
registry.registerCommand(
|
||||
{
|
||||
id: 'navigation.back',
|
||||
label: 'Backward',
|
||||
},
|
||||
{
|
||||
execute: () => {
|
||||
this.navigationService.back();
|
||||
},
|
||||
isEnabled: () => this.navigationService.canGoBack(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
registerShortcuts(registry: ShortcutsRegistry) {
|
||||
registry.registerHandlers(
|
||||
{
|
||||
keybinding: 'control shift -',
|
||||
commandId: 'navigation.forward',
|
||||
preventDefault: true,
|
||||
},
|
||||
{
|
||||
keybinding: 'control -',
|
||||
commandId: 'navigation.back',
|
||||
preventDefault: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
registerMouseNavigationListener() {
|
||||
this._toDispose.push(
|
||||
addEventListener(document.body, 'mousedown', (e: MouseEvent) => {
|
||||
switch (e.button) {
|
||||
case 3:
|
||||
this.navigationService.back();
|
||||
break;
|
||||
case 4:
|
||||
this.navigationService.forward();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { NavigationContribution };
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { injectable } from 'inversify';
|
||||
import {
|
||||
isNumber,
|
||||
Emitter,
|
||||
logger,
|
||||
DisposableCollection,
|
||||
} from '@flowgram-adapter/common';
|
||||
|
||||
import { type URI } from '../common';
|
||||
import { BrowserHistory, type HistoryState } from './browser-history';
|
||||
|
||||
/** location 结构 */
|
||||
interface Location {
|
||||
/** 唯一标识,一般来说是 uri */
|
||||
uri: URI;
|
||||
/** 预留字段 */
|
||||
}
|
||||
|
||||
@injectable()
|
||||
class NavigationHistory {
|
||||
private stack: Location[] = [];
|
||||
|
||||
private idx = -1;
|
||||
|
||||
private history = new BrowserHistory();
|
||||
|
||||
private onChangeEmitter = new Emitter<Location>();
|
||||
|
||||
private onPopstateEmitter = new Emitter<Location>();
|
||||
|
||||
onDidHistoryChange = this.onChangeEmitter.event;
|
||||
|
||||
onPopstate = this.onPopstateEmitter.event;
|
||||
|
||||
private disposable = new DisposableCollection(
|
||||
this.history,
|
||||
this.onChangeEmitter,
|
||||
this.onPopstateEmitter,
|
||||
);
|
||||
|
||||
get location() {
|
||||
return this.stack[this.idx];
|
||||
}
|
||||
|
||||
init() {
|
||||
this.history.init();
|
||||
this.disposable.push(this.history.onChange(this.listener));
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposable.dispose();
|
||||
}
|
||||
|
||||
pushOrReplace(location: Location, replace = false) {
|
||||
// 如果处于回退状态,则清除之后所有的历史
|
||||
if (this.stack.length > this.idx + 1) {
|
||||
this.stack = this.stack.slice(0, this.idx + 1);
|
||||
}
|
||||
return replace ? this.replace(location) : this.push(location);
|
||||
}
|
||||
|
||||
push(location: Location) {
|
||||
logger.log('navigation history push');
|
||||
if (this.similar(location, this.location)) {
|
||||
logger.log('location is similar');
|
||||
return;
|
||||
}
|
||||
this.stack.push(location);
|
||||
this.idx = this.stack.length - 1;
|
||||
this.history.push(location.uri, this.idx);
|
||||
this.onChangeEmitter.fire(location);
|
||||
}
|
||||
|
||||
replace(location: Location) {
|
||||
logger.log('navigation history replace');
|
||||
|
||||
this.stack.splice(this.idx + 1, 0, location);
|
||||
this.idx = this.stack.length - 1;
|
||||
this.history.replace(location.uri, this.idx);
|
||||
this.onChangeEmitter.fire(location);
|
||||
}
|
||||
|
||||
private go(delta: number) {
|
||||
const next = this.idx + delta;
|
||||
const nextLocation = this.stack[next];
|
||||
// 越界按照无效处理
|
||||
if (next >= this.stack.length || next < 0 || !nextLocation) {
|
||||
return;
|
||||
}
|
||||
this.idx = next;
|
||||
window.history.go(delta);
|
||||
this.onChangeEmitter.fire(nextLocation);
|
||||
return this.stack[this.idx];
|
||||
}
|
||||
|
||||
back() {
|
||||
return this.go(-1);
|
||||
}
|
||||
|
||||
canGoBack() {
|
||||
return this.idx >= 1;
|
||||
}
|
||||
|
||||
forward() {
|
||||
return this.go(1);
|
||||
}
|
||||
|
||||
canGoForward() {
|
||||
return this.idx >= 0 && this.idx !== this.stack.length - 1;
|
||||
}
|
||||
|
||||
private listener = (state: HistoryState) => {
|
||||
/** 无法正确识别 state 时不做任何处理 */
|
||||
if (!state || !isNumber(state.fIdx) || !state.uri) {
|
||||
return;
|
||||
}
|
||||
const { fIdx: idx } = state;
|
||||
/** 索引越界 */
|
||||
if (idx >= this.stack.length || idx < 0) {
|
||||
return;
|
||||
}
|
||||
this.idx = idx;
|
||||
this.onChangeEmitter.fire(this.location);
|
||||
this.onPopstateEmitter.fire(this.location);
|
||||
};
|
||||
|
||||
private similar(
|
||||
left: Location | undefined,
|
||||
right: Location | undefined,
|
||||
): boolean {
|
||||
if (left === undefined || right === undefined) {
|
||||
return left === right;
|
||||
}
|
||||
return left.uri.toString() === right.uri.toString();
|
||||
}
|
||||
}
|
||||
|
||||
export { NavigationHistory, type Location };
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { DisposableCollection } from '@flowgram-adapter/common';
|
||||
|
||||
import { URI, OpenerService } from '../common';
|
||||
import { NavigationHistory } from './navigation-history';
|
||||
|
||||
@injectable()
|
||||
class NavigationService {
|
||||
@inject(OpenerService)
|
||||
protected readonly openerService: OpenerService;
|
||||
|
||||
@inject(NavigationHistory)
|
||||
protected readonly history: NavigationHistory;
|
||||
|
||||
private disposable = new DisposableCollection();
|
||||
|
||||
scheme = 'flowide';
|
||||
|
||||
init() {
|
||||
this.history.init();
|
||||
this.disposable.pushAll([
|
||||
this.history,
|
||||
this.history.onPopstate(e => {
|
||||
this.openerService.open(e.uri);
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
public async goto(uri: URI | string, replace = false, options?: any) {
|
||||
let gotoUri: URI;
|
||||
if (typeof uri === 'string') {
|
||||
gotoUri = new URI(`${this.scheme}://${uri}`);
|
||||
} else {
|
||||
gotoUri = uri;
|
||||
}
|
||||
this.history.pushOrReplace({ uri: gotoUri }, replace);
|
||||
await this.openerService.open(gotoUri, options);
|
||||
}
|
||||
|
||||
public async back() {
|
||||
const location = this.history.back();
|
||||
if (location) {
|
||||
await this.openerService.open(location.uri);
|
||||
}
|
||||
}
|
||||
|
||||
public async forward() {
|
||||
const location = this.history.forward();
|
||||
if (location) {
|
||||
await this.openerService.open(location.uri);
|
||||
}
|
||||
}
|
||||
|
||||
public canGoBack() {
|
||||
return this.history.canGoBack();
|
||||
}
|
||||
|
||||
public canGoForward() {
|
||||
return this.history.canGoForward();
|
||||
}
|
||||
|
||||
get uri() {
|
||||
return this.history.location?.uri;
|
||||
}
|
||||
|
||||
get onDidHistoryChange() {
|
||||
return this.history.onDidHistoryChange;
|
||||
}
|
||||
|
||||
setScheme(scheme: string) {
|
||||
this.scheme = scheme;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export { NavigationService };
|
||||
Reference in New Issue
Block a user